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.appendRowFixed(slidingWindowAllRows, slidingWindowCurrentRow);
128 
129                     countAllRows = this.getCountRows(slidingWindowAllRows);
130                     this.sendProgress(countRowsToFind, countAllRows, elementDatabase);
131 
132                     // Ending condition: every expected rows have been retrieved.
133                     if (countAllRows == countRowsToFind) {
134                         break;
135                     }
136                     // Add the LIMIT statement to the next SQL query and reset variables.
137                     // Put the character cursor to the beginning of the line, and reset the result of the current query
138                     queryGetRows = this.getQuery(initialSqlQuery, countAllRows);
139                     slidingWindowCurrentRow.setLength(0);
140                 } else {
141                     this.sendProgress(countRowsToFind, countRowsToFind, elementDatabase);
142                     break;
143                 }
144             }
145             charPositionInCurrentRow = slidingWindowCurrentRow.length() + 1;
146         }
147         this.injectionModel.getMediatorUtils().threadUtil().remove(elementDatabase);
148         return slidingWindowAllRows.toString();
149     }
150 
151     private String decodeUrl(String currentChunk) {
152         if (!this.injectionModel.getMediatorUtils().preferencesUtil().isUrlDecodeDisabled()) {
153             try {
154                 return URLDecoder.decode(currentChunk, StandardCharsets.UTF_8);  // Transform %00 entities to text
155             } catch (IllegalArgumentException e) {
156                 LOGGER.log(LogLevelUtil.CONSOLE_JAVA, "Decoding fails on UT8, keeping raw result");
157             }
158         }
159         return currentChunk;
160     }
161 
162     private String decodeUnicode(String currentChunk, String initialSqlQuery) {
163         if (
164             !this.injectionModel.getMediatorUtils().preferencesUtil().isUnicodeDecodeDisabled()
165             && !"select@@plugin_dir".equals(initialSqlQuery)  // can give C:\path\
166             && initialSqlQuery != null && !initialSqlQuery.matches("(?si).*select.*sys_eval\\('.*'\\).*")
167         ) {
168             return StringEscapeUtils.unescapeJava(  // transform \u0000 entities to text
169                 currentChunk
170                 .replaceAll("\\\\u.{0,3}$", StringUtils.EMPTY)  // remove incorrect entities
171                 .replaceAll("\\\\(\\d{4})", "\\\\u$1")  // transform PDO Error 10.11.3-MariaDB-1 \0000 entities
172             );
173         }
174         return currentChunk;
175     }
176 
177     private String getQuery(String initialSqlQuery, int countAllRows) {
178         return initialSqlQuery.replace(LIMIT, this.injectionModel.getMediatorEngine().getEngine().instance().sqlLimit(countAllRows));
179     }
180 
181     private void appendRowFixed(StringBuilder slidingWindowAllRows, StringBuilder slidingWindowCurrentRow) {
182         // Check either if there is more than 1 row and if there is less than 1 complete row
183         var regexAtLeastOneRow = Pattern.compile(
184             String.format(
185                 "%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]%s%s%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]+?$",
186                 MODE,
187                 ENCLOSE_VALUE_RGX,
188                 SEPARATOR_CELL_RGX,
189                 ENCLOSE_VALUE_RGX
190             )
191         )
192         .matcher(slidingWindowCurrentRow);
193         
194         // If there is more than 1 row, delete the last incomplete one in order to restart properly from it at the next loop,
195         // else if there is 1 row but incomplete, mark it as cut with the letter c
196         if (regexAtLeastOneRow.find()) {
197             var allLine = slidingWindowAllRows.toString();
198             slidingWindowAllRows.setLength(0);
199             slidingWindowAllRows.append(
200                 Pattern.compile(
201                     MODE
202                     + ENCLOSE_VALUE_RGX
203                     + "[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]+?$"
204                 )
205                 .matcher(allLine)
206                 .replaceAll(StringUtils.EMPTY)
207             );
208             LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Chunk unreliable, reloading row part...");
209         }
210     }
211 
212     private void scrapeTrailJunk(StringBuilder slidingWindowCurrentRow) {
213         // Remove everything after chunk
214         // => \4xxxxxxxx\500\4\6\4...\4 => \1\3\3\7junk
215         var currentRow = slidingWindowCurrentRow.toString();
216         slidingWindowCurrentRow.setLength(0);
217         slidingWindowCurrentRow.append(
218             Pattern.compile(MODE + TRAIL_RGX +".*")
219             .matcher(currentRow)
220             .replaceAll(StringUtils.EMPTY)
221         );
222     }
223 
224     private int getCountRows(StringBuilder slidingWindowCurrentRow) {
225         var regexAtLeastOneRow = Pattern.compile(
226             String.format(
227                 "%s(%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?%s[^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?\\x08?%s)",
228                 MODE,
229                 ENCLOSE_VALUE_RGX,
230                 SEPARATOR_QTE_RGX,
231                 ENCLOSE_VALUE_RGX
232             )
233         )
234         .matcher(slidingWindowCurrentRow);
235         var nbCompleteLine = 0;
236         while (regexAtLeastOneRow.find()) {
237             nbCompleteLine++;
238         }
239         return nbCompleteLine;
240     }
241 
242     private void endInjection(AbstractElementDatabase searchName, Throwable e) throws InjectionFailureException {
243         // Premature end of results
244         // if it's not the root (empty tree)
245         if (searchName != null) {
246             this.injectionModel.sendToViews(new Seal.EndProgress(searchName));
247         }
248         var messageError = new StringBuilder("Fetching fails: no data to parse");
249         if (searchName != null) {
250             messageError.append(" for ").append(StringUtil.detectUtf8(searchName.toString()));
251         }
252         if (searchName instanceof Table && searchName.getChildCount() > 0) {
253             messageError.append(", check Network tab for logs");
254         }
255         throw new InjectionFailureException(messageError.toString(), e);
256     }
257 
258     private void sendChunk(String currentChunk) {
259         this.injectionModel.sendToViews(new Seal.MessageChunk(
260             Pattern.compile(MODE + TRAIL_RGX +".*")
261             .matcher(currentChunk)
262             .replaceAll(StringUtils.EMPTY)
263             .replace("\\n", "\\\\\\n")
264             .replace("\\r", "\\\\\\r")
265             .replace("\\t", "\\\\\\t")
266         ));
267     }
268 
269     // TODO pb for same char string like aaaaaaaaaaaaa...aaaaaaaaaaaaa
270     // detected as infinite
271     private int checkInfinite(
272         int loop,
273         String previousChunk,
274         String currentChunk,
275         StringBuilder slidingWindowCurrentRow,
276         StringBuilder slidingWindowAllRows
277     ) throws LoopDetectedSlidingException {
278         int infiniteLoop = loop;
279         if (previousChunk.equals(currentChunk)) {
280             infiniteLoop++;
281             if (infiniteLoop >= 20) {
282                 this.stop();
283                 throw new LoopDetectedSlidingException(
284                     slidingWindowAllRows.toString(),
285                     slidingWindowCurrentRow.toString()
286                 );
287             }
288         }
289         return infiniteLoop;
290     }
291 
292     private Matcher parseTrailOnlyFound(String sourcePage) {
293         String sourcePageUnicodeDecoded = this.decodeUnicode(sourcePage, null);
294         // TODO: prevent to find the last line directly: MODE + LEAD + .* + TRAIL_RGX
295         // It creates extra query which can be endless if not nullified
296         return Pattern.compile(
297             String.format("(?s)%s(?i)%s", LEAD, TRAIL_RGX)
298         )
299         .matcher(sourcePageUnicodeDecoded);
300     }
301 
302     /**
303      * After ${lead} tag, gets characters between 1 and maxPerf
304      * performanceQuery() gets 65536 characters or fewer
305      * [${lead}blahblah1337      ] : end or limit+1
306      * [${lead}blahblah      blah] : continue substr()
307      */
308     private Matcher parseLeadFound(String sourcePage, String performanceLength) throws InjectionFailureException {
309         Matcher regexAtLeastOneRow;
310         try {
311             regexAtLeastOneRow = Pattern.compile(
312                 String.format("(?s)%s(?i)(.{1,%s})", LEAD, performanceLength)
313             )
314             .matcher(sourcePage);
315         } catch (PatternSyntaxException e) {
316             // Fix #35382 : PatternSyntaxException null on SQLi(.{1,null})
317             throw new InjectionFailureException("Row parsing failed using capacity", e);
318         }
319         return regexAtLeastOneRow;
320     }
321 
322     private void checkSuspend(
323         AbstractStrategy strategy,
324         StringBuilder slidingWindowAllRows,
325         StringBuilder slidingWindowCurrentRow
326     ) throws StoppedByUserSlidingException, InjectionFailureException {
327         if (this.isSuspended()) {
328             throw new StoppedByUserSlidingException(
329                 slidingWindowAllRows.toString(),
330                 slidingWindowCurrentRow.toString()
331             );
332         } else if (strategy == null) {
333             // Fix #1905 : NullPointerException on injectionStrategy.inject()
334             throw new InjectionFailureException("Undefined strategy");
335         }
336     }
337 
338     private void sendProgress(int numberToFind, int countProgress, AbstractElementDatabase searchName) {
339         if (numberToFind > 0 && searchName != null) {
340             this.injectionModel.sendToViews(new Seal.UpdateProgress(searchName, countProgress));
341         }
342     }
343     
344     public static List<List<String>> parse(String rows) throws InjectionFailureException {
345         // Parse all the data we have retrieved
346         var regexSearch = Pattern.compile(
347                 String.format(
348                     "%s%s([^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?)%s([^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*?)(\\x08)?%s",
349                     MODE,
350                     ENCLOSE_VALUE_RGX,
351                     SEPARATOR_QTE_RGX,
352                     ENCLOSE_VALUE_RGX
353                 )
354             )
355             .matcher(rows);
356         if (!regexSearch.find()) {
357             throw new InjectionFailureException();
358         }
359         regexSearch.reset();
360         var rowsFound = 0;
361         List<List<String>> listValues = new ArrayList<>();
362 
363         // Build a 2D array of strings from the data we have parsed
364         // => row number, occurrence, value1, value2...
365         while (regexSearch.find()) {
366             String values = regexSearch.group(1);
367             var instances = Integer.parseInt(regexSearch.group(2));
368 
369             listValues.add(new ArrayList<>());
370             listValues.get(rowsFound).add(Integer.toString(rowsFound + 1));
371             listValues.get(rowsFound).add("x"+ instances);
372             for (String cellValue: values.split("\\x7F", -1)) {
373                 listValues.get(rowsFound).add(cellValue);
374             }
375             rowsFound++;
376         }
377         return listValues;
378     }
379 }