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