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      /**
46       * Log4j logger sent to view.
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          // TODO Map class
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          // Fix #14417
69          if (strategy == null) {
70              return StringUtils.EMPTY;
71          }
72          
73          // Stop injection if all rows are found, skip rows and characters collected
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              // Parse all the data we have retrieved
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             // Add the result to the data already found.
102             // Fix #40947: OutOfMemoryError on append()
103             // Fix #95382: IllegalArgumentException on URLDecoder.decode()
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             // Check how many rows we have collected from the beginning of that chunk
119             int countChunkRows = this.getCountRows(slidingWindowCurrentRow);
120             this.sendProgress(countRowsToFind, countAllRows + countChunkRows, elementDatabase);
121 
122             // End of rows detected: \1\3\3\7
123             // => \4xxxxxxxx\500\4\6\4...\4\1\3\3\7
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                     // Ending condition: every expected rows have been retrieved.
140                     if (countAllRows == countRowsToFind) {
141                         break;
142                     }
143                     // Add the LIMIT statement to the next SQL query and reset variables.
144                     // Put the character cursor to the beginning of the line, and reset the result of the current query
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);  // Transform %00 entities to text
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)  // can give C:\path\
173             && initialSqlQuery != null && !initialSqlQuery.matches("(?si).*select.*sys_eval\\('.*'\\).*")
174         ) {
175             return StringEscapeUtils.unescapeJava(  // transform \u0000 entities to text
176                 currentChunk
177                 .replaceAll("\\\\u.{0,3}$", StringUtils.EMPTY)  // remove incorrect entities
178                 .replaceAll("\\\\(\\d{4})", "\\\\u$1")  // transform PDO Error 10.11.3-MariaDB-1 \0000 entities
179             );
180         }
181         return currentChunk;
182     }
183 
184     private String getQuery(String initialSqlQuery, int countAllRows) {
185         return initialSqlQuery.replace(LIMIT, this.injectionModel.getMediatorVendor().getVendor().instance().sqlLimit(countAllRows));
186     }
187 
188     private void appendRowFixed(StringBuilder slidingWindowAllRows, StringBuilder slidingWindowCurrentRow) {
189         // Check either if there is more than 1 row and if there is less than 1 complete row
190         var regexAtLeastOneRow = Pattern.compile(
191             String.format(
192                 "%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]%s%s%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]+?$",
193                 MODE,
194                 ENCLOSE_VALUE_RGX,
195                 SEPARATOR_CELL_RGX,
196                 ENCLOSE_VALUE_RGX
197             )
198         )
199         .matcher(slidingWindowCurrentRow);
200         
201         var regexRowIncomplete = Pattern.compile(
202             MODE
203             + ENCLOSE_VALUE_RGX
204             + "[^\\x01-\\x03\\x05-\\x09\\x0B-\\x0C\\x0E-\\x1F]+?$"
205         )
206         .matcher(slidingWindowCurrentRow);
207 
208         // If there is more than 1 row, delete the last incomplete one in order to restart properly from it at the next loop,
209         // else if there is 1 row but incomplete, mark it as cut with the letter c
210         if (regexAtLeastOneRow.find()) {
211             var allLine = slidingWindowAllRows.toString();
212             slidingWindowAllRows.setLength(0);
213             slidingWindowAllRows.append(
214                 Pattern.compile(
215                     MODE
216                     + ENCLOSE_VALUE_RGX
217                     + "[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]+?$"
218                 )
219                 .matcher(allLine)
220                 .replaceAll(StringUtils.EMPTY)
221             );
222             LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Chunk unreliable, reloading row part...");
223         } else if (regexRowIncomplete.find()) {
224             slidingWindowAllRows.append(StringUtil.hexstr("05")).append("1").append(StringUtil.hexstr("0804"));
225             LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Chunk unreliable, keeping row parts only");
226         }
227     }
228 
229     private void scrapeTrailJunk(StringBuilder slidingWindowCurrentRow) {
230         // Remove everything after chunk
231         // => \4xxxxxxxx\500\4\6\4...\4 => \1\3\3\7junk
232         var currentRow = slidingWindowCurrentRow.toString();
233         slidingWindowCurrentRow.setLength(0);
234         slidingWindowCurrentRow.append(
235             Pattern.compile(MODE + TRAIL_RGX +".*")
236             .matcher(currentRow)
237             .replaceAll(StringUtils.EMPTY)
238         );
239     }
240 
241     private int getCountRows(StringBuilder slidingWindowCurrentRow) {
242         var regexAtLeastOneRow = Pattern.compile(
243             String.format(
244                 "%s(%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?\\x08?%s)",
245                 MODE,
246                 ENCLOSE_VALUE_RGX,
247                 SEPARATOR_QTE_RGX,
248                 ENCLOSE_VALUE_RGX
249             )
250         )
251         .matcher(slidingWindowCurrentRow);
252         var nbCompleteLine = 0;
253         while (regexAtLeastOneRow.find()) {
254             nbCompleteLine++;
255         }
256         return nbCompleteLine;
257     }
258 
259     private void endInjection(AbstractElementDatabase searchName, Throwable e) throws InjectionFailureException {
260         // Premature end of results
261         // if it's not the root (empty tree)
262         if (searchName != null) {
263             var request = new Request();
264             request.setMessage(Interaction.END_PROGRESS);
265             request.setParameters(searchName);
266             this.injectionModel.sendToViews(request);
267         }
268         var messageError = new StringBuilder("Fetching fails: no data to parse");
269         if (searchName != null) {
270             messageError.append(" for ").append(StringUtil.detectUtf8(searchName.toString()));
271         }
272         if (searchName instanceof Table && searchName.getChildCount() > 0) {
273             messageError.append(", check Network tab for logs");
274         }
275         throw new InjectionFailureException(messageError.toString(), e);
276     }
277 
278     private void sendChunk(String currentChunk) {
279         var request = new Request();
280         request.setMessage(Interaction.MESSAGE_CHUNK);
281         request.setParameters(
282             Pattern.compile(MODE + TRAIL_RGX +".*")
283             .matcher(currentChunk)
284             .replaceAll(StringUtils.EMPTY)
285             .replace("\\n", "\\\\\\n")
286             .replace("\\r", "\\\\\\r")
287             .replace("\\t", "\\\\\\t")
288         );
289         this.injectionModel.sendToViews(request);
290     }
291 
292     // TODO pb for same char string like aaaaaaaaaaaaa...aaaaaaaaaaaaa
293     // detected as infinite
294     private int checkInfinite(
295         int loop,
296         String previousChunk,
297         String currentChunk,
298         StringBuilder slidingWindowCurrentRow,
299         StringBuilder slidingWindowAllRows
300     ) throws LoopDetectedSlidingException {
301         int infiniteLoop = loop;
302         if (previousChunk.equals(currentChunk)) {
303             infiniteLoop++;
304             if (infiniteLoop >= 20) {
305                 this.stop();
306                 throw new LoopDetectedSlidingException(
307                     slidingWindowAllRows.toString(),
308                     slidingWindowCurrentRow.toString()
309                 );
310             }
311         }
312         return infiniteLoop;
313     }
314 
315     private Matcher parseTrailOnlyFound(String sourcePage) {
316         String sourcePageUnicodeDecoded = this.decodeUnicode(sourcePage, null);
317         // TODO: prevent to find the last line directly: MODE + LEAD + .* + TRAIL_RGX
318         // It creates extra query which can be endless if not nullified
319         return Pattern.compile(
320             String.format("(?s)%s(?i)%s", LEAD, TRAIL_RGX)
321         )
322         .matcher(sourcePageUnicodeDecoded);
323     }
324 
325     /**
326      * After ${lead} tag, gets characters between 1 and maxPerf
327      * performanceQuery() gets 65536 characters or fewer
328      * ${lead}blahblah1337      ] : end or limit+1
329      * ${lead}blahblah      blah] : continue substr()
330      */
331     private Matcher parseLeadFound(String sourcePage, String performanceLength) throws InjectionFailureException {
332         Matcher regexAtLeastOneRow;
333         try {
334             regexAtLeastOneRow = Pattern.compile(
335                 String.format("(?s)%s(?i)(.{1,%s})", LEAD, performanceLength)
336             )
337             .matcher(sourcePage);
338         } catch (PatternSyntaxException e) {
339             // Fix #35382 : PatternSyntaxException null on SQLi(.{1,null})
340             throw new InjectionFailureException("Row parsing failed using capacity", e);
341         }
342         return regexAtLeastOneRow;
343     }
344 
345     private void checkSuspend(
346         AbstractStrategy strategy,
347         StringBuilder slidingWindowAllRows,
348         StringBuilder slidingWindowCurrentRow
349     ) throws StoppedByUserSlidingException, InjectionFailureException {
350         if (this.isSuspended()) {
351             throw new StoppedByUserSlidingException(
352                 slidingWindowAllRows.toString(),
353                 slidingWindowCurrentRow.toString()
354             );
355         } else if (strategy == null) {
356             // Fix #1905 : NullPointerException on injectionStrategy.inject()
357             throw new InjectionFailureException("Undefined strategy");
358         }
359     }
360 
361     private void scrap(StringBuilder slidingWindowAllRows) {
362         // Remove everything not properly attached to the last row:
363         // 1. very start of a new row: XXXXX\4[\6\4]$
364         // 2. very end of the last row: XXXXX[\500]$
365         var allRowsLimit = slidingWindowAllRows.toString();
366         slidingWindowAllRows.setLength(0);
367         slidingWindowAllRows.append(
368             Pattern.compile(
369                 String.format(
370                     "%s(%s%s|%s\\d*)$",
371                     MODE,
372                     SEPARATOR_CELL_RGX,
373                     ENCLOSE_VALUE_RGX,
374                     SEPARATOR_QTE_RGX
375                 )
376             )
377             .matcher(allRowsLimit)
378             .replaceAll(StringUtils.EMPTY)
379         );
380     }
381 
382     private void sendProgress(int numberToFind, int countProgress, AbstractElementDatabase searchName) {
383         if (numberToFind > 0 && searchName != null) {
384             var request = new Request();
385             request.setMessage(Interaction.UPDATE_PROGRESS);
386             request.setParameters(searchName, countProgress);
387             this.injectionModel.sendToViews(request);
388         }
389     }
390     
391     public static List<List<String>> parse(String rows) throws InjectionFailureException {
392         // Parse all the data we have retrieved
393         var regexSearch = Pattern.compile(
394                 String.format(
395                     "%s%s([^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?)%s([^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?)(\\x08)?%s",
396                     MODE,
397                     ENCLOSE_VALUE_RGX,
398                     SEPARATOR_QTE_RGX,
399                     ENCLOSE_VALUE_RGX
400                 )
401             )
402             .matcher(rows);
403         if (!regexSearch.find()) {
404             throw new InjectionFailureException();
405         }
406         regexSearch.reset();
407         var rowsFound = 0;
408         List<List<String>> listValues = new ArrayList<>();
409 
410         // Build a 2D array of strings from the data we have parsed
411         // => row number, occurrence, value1, value2...
412         while (regexSearch.find()) {
413             String values = regexSearch.group(1);
414             var instances = Integer.parseInt(regexSearch.group(2));
415 
416             listValues.add(new ArrayList<>());
417             listValues.get(rowsFound).add(Integer.toString(rowsFound + 1));
418             listValues.get(rowsFound).add("x"+ instances);
419             for (String cellValue: values.split("\\x7F", -1)) {
420                 listValues.get(rowsFound).add(cellValue);
421             }
422             rowsFound++;
423         }
424         return listValues;
425     }
426 }