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.appendRowFixed(slidingWindowAllRows, slidingWindowCurrentRow);
128
129 countAllRows = this.getCountRows(slidingWindowAllRows);
130 this.sendProgress(countRowsToFind, countAllRows, elementDatabase);
131
132
133 if (countAllRows == countRowsToFind) {
134 break;
135 }
136
137
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);
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)
166 && initialSqlQuery != null && !initialSqlQuery.matches("(?si).*select.*sys_eval\\('.*'\\).*")
167 ) {
168 return StringEscapeUtils.unescapeJava(
169 currentChunk
170 .replaceAll("\\\\u.{0,3}$", StringUtils.EMPTY)
171 .replaceAll("\\\\(\\d{4})", "\\\\u$1")
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
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
195
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
214
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
244
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
270
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
295
296 return Pattern.compile(
297 String.format("(?s)%s(?i)%s", LEAD, TRAIL_RGX)
298 )
299 .matcher(sourcePageUnicodeDecoded);
300 }
301
302
303
304
305
306
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
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
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
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
364
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 }