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