1 package com.jsql.model.suspendable;
2
3 import com.jsql.model.InjectionModel;
4 import com.jsql.model.bean.database.AbstractElementDatabase;
5 import com.jsql.model.bean.database.Table;
6 import com.jsql.model.bean.util.Interaction;
7 import com.jsql.model.bean.util.Request;
8 import com.jsql.model.exception.AbstractSlidingException;
9 import com.jsql.model.exception.InjectionFailureException;
10 import com.jsql.model.exception.LoopDetectedSlidingException;
11 import com.jsql.model.exception.StoppedByUserSlidingException;
12 import com.jsql.model.injection.strategy.AbstractStrategy;
13 import com.jsql.util.LogLevelUtil;
14 import com.jsql.util.StringUtil;
15 import org.apache.commons.lang3.StringUtils;
16 import org.apache.commons.text.StringEscapeUtils;
17 import org.apache.logging.log4j.LogManager;
18 import org.apache.logging.log4j.Logger;
19
20 import java.net.URLDecoder;
21 import java.nio.charset.StandardCharsets;
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26 import java.util.regex.PatternSyntaxException;
27
28 import static com.jsql.model.accessible.DataAccess.*;
29 import static com.jsql.model.injection.vendor.model.VendorYaml.LIMIT;
30
31
32
33
34
35
36
37
38
39
40
41
42
43 public class SuspendableGetRows extends AbstractSuspendable {
44
45 private static final Logger LOGGER = LogManager.getRootLogger();
46
47 public SuspendableGetRows(InjectionModel injectionModel) {
48 super(injectionModel);
49 }
50
51 @Override
52 public String run(Object... args) throws AbstractSlidingException {
53
54 String initialSqlQuery = (String) args[0];
55 String[] sourcePage = (String[]) args[1];
56 boolean isMultipleRows = (Boolean) args[2];
57 int countRowsToFind = (Integer) args[3];
58 AbstractElementDatabase elementDatabase = (AbstractElementDatabase) args[4];
59 String metadataInjectionProcess = (String) args[5];
60
61 this.injectionModel.getMediatorUtils().getThreadUtil().put(elementDatabase, this);
62
63 AbstractStrategy strategy = this.injectionModel.getMediatorStrategy().getStrategy();
64
65
66 if (strategy == null) {
67 return StringUtils.EMPTY;
68 }
69
70
71 var slidingWindowAllRows = new StringBuilder();
72 var slidingWindowCurrentRow = new StringBuilder();
73
74 String previousChunk = StringUtils.EMPTY;
75 var countAllRows = 0;
76 var charPositionInCurrentRow = 1;
77 var countInfiniteLoop = 0;
78
79 String queryGetRows = this.getQuery(initialSqlQuery, countAllRows);
80
81 while (true) {
82 this.checkSuspend(strategy, slidingWindowAllRows, slidingWindowCurrentRow);
83
84 sourcePage[0] = strategy.inject(queryGetRows, Integer.toString(charPositionInCurrentRow), this, metadataInjectionProcess);
85
86 Matcher regexLeadFound = this.parseLeadFound(sourcePage[0], strategy.getPerformanceLength());
87 Matcher regexTrailOnlyFound = this.parseTrailOnlyFound(sourcePage[0]);
88
89 if (
90 (!regexLeadFound.find() || regexTrailOnlyFound.find())
91 && isMultipleRows
92 && StringUtils.isNotEmpty(slidingWindowAllRows.toString())
93 ) {
94 this.sendProgress(countRowsToFind, countRowsToFind, elementDatabase);
95 break;
96 }
97
98
99
100
101 try {
102 String currentChunk = regexLeadFound.group(1);
103 currentChunk = this.decodeUnicode(currentChunk, initialSqlQuery);
104 currentChunk = this.decodeUrl(currentChunk);
105
106 countInfiniteLoop = this.checkInfinite(countInfiniteLoop, previousChunk, currentChunk, slidingWindowCurrentRow, slidingWindowAllRows);
107
108 previousChunk = currentChunk;
109 slidingWindowCurrentRow.append(currentChunk);
110 this.sendChunk(currentChunk);
111 } catch (IllegalArgumentException | IllegalStateException | OutOfMemoryError e) {
112 this.endInjection(elementDatabase, e);
113 }
114
115
116 int countChunkRows = this.getCountRows(slidingWindowCurrentRow);
117 this.sendProgress(countRowsToFind, countAllRows + countChunkRows, elementDatabase);
118
119
120
121 if (
122 countChunkRows > 0
123 || slidingWindowCurrentRow.toString().matches("(?s).*"+ TRAIL_RGX +".*")
124 ) {
125 this.scrapeTrailJunk(slidingWindowCurrentRow);
126 slidingWindowAllRows.append(slidingWindowCurrentRow);
127
128 if (isMultipleRows) {
129 this.scrap(slidingWindowAllRows);
130 this.scrap(slidingWindowCurrentRow);
131 this.appendRowFixed(slidingWindowAllRows, slidingWindowCurrentRow);
132
133 countAllRows = this.getCountRows(slidingWindowAllRows);
134 this.sendProgress(countRowsToFind, countAllRows, elementDatabase);
135
136
137 if (countAllRows == countRowsToFind) {
138 break;
139 }
140
141
142 queryGetRows = this.getQuery(initialSqlQuery, countAllRows);
143 slidingWindowCurrentRow.setLength(0);
144 } else {
145 this.sendProgress(countRowsToFind, countRowsToFind, elementDatabase);
146 break;
147 }
148 }
149 charPositionInCurrentRow = slidingWindowCurrentRow.length() + 1;
150 }
151 this.injectionModel.getMediatorUtils().getThreadUtil().remove(elementDatabase);
152 return slidingWindowAllRows.toString();
153 }
154
155 private String decodeUrl(String currentChunk) {
156 if (!this.injectionModel.getMediatorUtils().getPreferencesUtil().isUrlDecodeDisabled()) {
157 try {
158 return URLDecoder.decode(currentChunk, StandardCharsets.UTF_8);
159 } catch (IllegalArgumentException e) {
160 LOGGER.log(LogLevelUtil.CONSOLE_JAVA, "Decoding fails on UT8, keeping raw result");
161 }
162 }
163 return currentChunk;
164 }
165
166 private String decodeUnicode(String currentChunk, String initialSqlQuery) {
167 if (
168 !this.injectionModel.getMediatorUtils().getPreferencesUtil().isUnicodeDecodeDisabled()
169 && !"select@@plugin_dir".equals(initialSqlQuery)
170 && initialSqlQuery != null && !initialSqlQuery.matches("(?si).*select.*sys_eval\\('.*'\\).*")
171 ) {
172 return StringEscapeUtils.unescapeJava(
173 currentChunk
174 .replaceAll("\\\\u.{0,3}$", StringUtils.EMPTY)
175 .replaceAll("\\\\(\\d{4})", "\\\\u$1")
176 );
177 }
178 return currentChunk;
179 }
180
181 private String getQuery(String initialSqlQuery, int countAllRows) {
182 return initialSqlQuery.replace(LIMIT, this.injectionModel.getMediatorVendor().getVendor().instance().sqlLimit(countAllRows));
183 }
184
185 private void appendRowFixed(StringBuilder slidingWindowAllRows, StringBuilder slidingWindowCurrentRow) {
186
187 var regexAtLeastOneRow = Pattern.compile(
188 String.format(
189 "%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]%s%s%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]+?$",
190 MODE,
191 ENCLOSE_VALUE_RGX,
192 SEPARATOR_CELL_RGX,
193 ENCLOSE_VALUE_RGX
194 )
195 )
196 .matcher(slidingWindowCurrentRow);
197
198 var regexRowIncomplete = Pattern.compile(
199 MODE
200 + ENCLOSE_VALUE_RGX
201 + "[^\\x01-\\x03\\x05-\\x09\\x0B-\\x0C\\x0E-\\x1F]+?$"
202 )
203 .matcher(slidingWindowCurrentRow);
204
205
206
207 if (regexAtLeastOneRow.find()) {
208 var allLine = slidingWindowAllRows.toString();
209 slidingWindowAllRows.setLength(0);
210 slidingWindowAllRows.append(
211 Pattern.compile(
212 MODE
213 + ENCLOSE_VALUE_RGX
214 + "[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]+?$"
215 )
216 .matcher(allLine)
217 .replaceAll(StringUtils.EMPTY)
218 );
219 LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Chunk unreliable, reloading row part...");
220 } else if (regexRowIncomplete.find()) {
221 slidingWindowAllRows.append(StringUtil.hexstr("05")).append("1").append(StringUtil.hexstr("0804"));
222 LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Chunk unreliable, keeping row parts only");
223 }
224 }
225
226 private void scrapeTrailJunk(StringBuilder slidingWindowCurrentRow) {
227
228
229 var currentRow = slidingWindowCurrentRow.toString();
230 slidingWindowCurrentRow.setLength(0);
231 slidingWindowCurrentRow.append(
232 Pattern.compile(MODE + TRAIL_RGX +".*")
233 .matcher(currentRow)
234 .replaceAll(StringUtils.EMPTY)
235 );
236 }
237
238 private int getCountRows(StringBuilder slidingWindowCurrentRow) {
239 var regexAtLeastOneRow = Pattern.compile(
240 String.format(
241 "%s(%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?\\x08?%s)",
242 MODE,
243 ENCLOSE_VALUE_RGX,
244 SEPARATOR_QTE_RGX,
245 ENCLOSE_VALUE_RGX
246 )
247 )
248 .matcher(slidingWindowCurrentRow);
249 var nbCompleteLine = 0;
250 while (regexAtLeastOneRow.find()) {
251 nbCompleteLine++;
252 }
253 return nbCompleteLine;
254 }
255
256 private void endInjection(AbstractElementDatabase searchName, Throwable e) throws InjectionFailureException {
257
258
259 if (searchName != null) {
260 var request = new Request();
261 request.setMessage(Interaction.END_PROGRESS);
262 request.setParameters(searchName);
263 this.injectionModel.sendToViews(request);
264 }
265 var messageError = new StringBuilder("Fetching fails: no data to parse");
266 if (searchName != null) {
267 messageError.append(" for ").append(StringUtil.detectUtf8(searchName.toString()));
268 }
269 if (searchName instanceof Table && searchName.getChildCount() > 0) {
270 messageError.append(", check Network tab for logs");
271 }
272 throw new InjectionFailureException(messageError.toString(), e);
273 }
274
275 private void sendChunk(String currentChunk) {
276 var request = new Request();
277 request.setMessage(Interaction.MESSAGE_CHUNK);
278 request.setParameters(
279 Pattern.compile(MODE + TRAIL_RGX +".*")
280 .matcher(currentChunk)
281 .replaceAll(StringUtils.EMPTY)
282 .replace("\\n", "\\\\\\n")
283 .replace("\\r", "\\\\\\r")
284 .replace("\\t", "\\\\\\t")
285 );
286 this.injectionModel.sendToViews(request);
287 }
288
289
290
291 private int checkInfinite(
292 int loop,
293 String previousChunk,
294 String currentChunk,
295 StringBuilder slidingWindowCurrentRow,
296 StringBuilder slidingWindowAllRows
297 ) throws LoopDetectedSlidingException {
298 int infiniteLoop = loop;
299 if (previousChunk.equals(currentChunk)) {
300 infiniteLoop++;
301 if (infiniteLoop >= 20) {
302 this.stop();
303 throw new LoopDetectedSlidingException(
304 slidingWindowAllRows.toString(),
305 slidingWindowCurrentRow.toString()
306 );
307 }
308 }
309 return infiniteLoop;
310 }
311
312 private Matcher parseTrailOnlyFound(String sourcePage) {
313 String sourcePageUnicodeDecoded = this.decodeUnicode(sourcePage, null);
314
315
316 return Pattern.compile(
317 String.format("(?s)%s(?i)%s", LEAD, TRAIL_RGX)
318 )
319 .matcher(sourcePageUnicodeDecoded);
320 }
321
322
323
324
325
326
327
328 private Matcher parseLeadFound(String sourcePage, String performanceLength) throws InjectionFailureException {
329 Matcher regexAtLeastOneRow;
330 try {
331 regexAtLeastOneRow = Pattern.compile(
332 String.format("(?s)%s(?i)(.{1,%s})", LEAD, performanceLength)
333 )
334 .matcher(sourcePage);
335 } catch (PatternSyntaxException e) {
336
337 throw new InjectionFailureException("Row parsing failed using capacity", e);
338 }
339 return regexAtLeastOneRow;
340 }
341
342 private void checkSuspend(
343 AbstractStrategy strategy,
344 StringBuilder slidingWindowAllRows,
345 StringBuilder slidingWindowCurrentRow
346 ) throws StoppedByUserSlidingException, InjectionFailureException {
347 if (this.isSuspended()) {
348 throw new StoppedByUserSlidingException(
349 slidingWindowAllRows.toString(),
350 slidingWindowCurrentRow.toString()
351 );
352 } else if (strategy == null) {
353
354 throw new InjectionFailureException("Undefined strategy");
355 }
356 }
357
358 private void scrap(StringBuilder slidingWindowAllRows) {
359
360
361
362 var allRowsLimit = slidingWindowAllRows.toString();
363 slidingWindowAllRows.setLength(0);
364 slidingWindowAllRows.append(
365 Pattern.compile(
366 String.format(
367 "%s(%s%s|%s\\d*)$",
368 MODE,
369 SEPARATOR_CELL_RGX,
370 ENCLOSE_VALUE_RGX,
371 SEPARATOR_QTE_RGX
372 )
373 )
374 .matcher(allRowsLimit)
375 .replaceAll(StringUtils.EMPTY)
376 );
377 }
378
379 private void sendProgress(int numberToFind, int countProgress, AbstractElementDatabase searchName) {
380 if (numberToFind > 0 && searchName != null) {
381 var request = new Request();
382 request.setMessage(Interaction.UPDATE_PROGRESS);
383 request.setParameters(searchName, countProgress);
384 this.injectionModel.sendToViews(request);
385 }
386 }
387
388 public static List<List<String>> parse(String rows) throws InjectionFailureException {
389
390 var regexSearch = Pattern.compile(
391 String.format(
392 "%s%s([^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?)%s([^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?)(\\x08)?%s",
393 MODE,
394 ENCLOSE_VALUE_RGX,
395 SEPARATOR_QTE_RGX,
396 ENCLOSE_VALUE_RGX
397 )
398 )
399 .matcher(rows);
400 if (!regexSearch.find()) {
401 throw new InjectionFailureException();
402 }
403 regexSearch.reset();
404 var rowsFound = 0;
405 List<List<String>> listValues = new ArrayList<>();
406
407
408
409 while (regexSearch.find()) {
410 String values = regexSearch.group(1);
411 var instances = Integer.parseInt(regexSearch.group(2));
412
413 listValues.add(new ArrayList<>());
414 listValues.get(rowsFound).add(Integer.toString(rowsFound + 1));
415 listValues.get(rowsFound).add("x"+ instances);
416 for (String cellValue: values.split("\\x7F", -1)) {
417 listValues.get(rowsFound).add(cellValue);
418 }
419 rowsFound++;
420 }
421 return listValues;
422 }
423 }