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