View Javadoc
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   * Get data as chunks by performance query from SQL request.
33   * 
34   * <pre>
35   * Single row format: \4[0-9A-F]*\5[0-9A-F]*c?\4
36   * Row separator: \6
37   * Tape example: \4xxRow#Xxx\5x\4\6\4xxRow#X+1xx\5x\4\6...\4\1\3\3\7</pre>
38   * 
39   * MID and LIMIT move two sliding windows in a 2D array tape in that order.
40   * MID skips characters when collected, then LIMIT skips lines when collected.
41   * The process can be interrupted by the user (stop/pause).
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          // TODO Map class
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          // Fix #14417
66          if (strategy == null) {
67              return StringUtils.EMPTY;
68          }
69          
70          // Stop injection if all rows are found, skip rows and characters collected
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              // Parse all the data we have retrieved
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              // Add the result to the data already found.
99              // Fix #40947: OutOfMemoryError on append()
100             // Fix #95382: IllegalArgumentException on URLDecoder.decode()
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             // Check how many rows we have collected from the beginning of that chunk
116             int countChunkRows = this.getCountRows(slidingWindowCurrentRow);
117             this.sendProgress(countRowsToFind, countAllRows + countChunkRows, elementDatabase);
118 
119             // End of rows detected: \1\3\3\7
120             // => \4xxxxxxxx\500\4\6\4...\4\1\3\3\7
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                     // Ending condition: every expected rows have been retrieved.
137                     if (countAllRows == countRowsToFind) {
138                         break;
139                     }
140                     // Add the LIMIT statement to the next SQL query and reset variables.
141                     // Put the character cursor to the beginning of the line, and reset the result of the current query
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);  // Transform %00 entities to text
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)  // can give C:\path\
170             && initialSqlQuery != null && !initialSqlQuery.matches("(?si).*select.*sys_eval\\('.*'\\).*")
171         ) {
172             return StringEscapeUtils.unescapeJava(  // transform \u0000 entities to text
173                 currentChunk
174                 .replaceAll("\\\\u.{0,3}$", StringUtils.EMPTY)  // remove incorrect entities
175                 .replaceAll("\\\\(\\d{4})", "\\\\u$1")  // transform PDO Error 10.11.3-MariaDB-1 \0000 entities
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         // Check either if there is more than 1 row and if there is less than 1 complete row
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         // If there is more than 1 row, delete the last incomplete one in order to restart properly from it at the next loop,
206         // else if there is 1 row but incomplete, mark it as cut with the letter c
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         // Remove everything after chunk
228         // => \4xxxxxxxx\500\4\6\4...\4 => \1\3\3\7junk
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         // Premature end of results
258         // if it's not the root (empty tree)
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     // TODO pb for same char string like aaaaaaaaaaaaa...aaaaaaaaaaaaa
290     // detected as infinite
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         // TODO: prevent to find the last line directly: MODE + LEAD + .* + TRAIL_RGX
315         // It creates extra query which can be endless if not nullified
316         return Pattern.compile(
317             String.format("(?s)%s(?i)%s", LEAD, TRAIL_RGX)
318         )
319         .matcher(sourcePageUnicodeDecoded);
320     }
321 
322     /**
323      * After ${lead} tag, gets characters between 1 and maxPerf
324      * performanceQuery() gets 65536 characters or fewer
325      * ${lead}blahblah1337      ] : end or limit+1
326      * ${lead}blahblah      blah] : continue substr()
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             // Fix #35382 : PatternSyntaxException null on SQLi(.{1,null})
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             // Fix #1905 : NullPointerException on injectionStrategy.inject()
354             throw new InjectionFailureException("Undefined strategy");
355         }
356     }
357 
358     private void scrap(StringBuilder slidingWindowAllRows) {
359         // Remove everything not properly attached to the last row:
360         // 1. very start of a new row: XXXXX\4[\6\4]$
361         // 2. very end of the last row: XXXXX[\500]$
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         // Parse all the data we have retrieved
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         // Build a 2D array of strings from the data we have parsed
408         // => row number, occurrence, value1, value2...
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 }