View Javadoc
1   package com.jsql.util;
2   
3   import com.jsql.model.InjectionModel;
4   import com.jsql.view.subscriber.Seal;
5   import com.jsql.model.exception.InjectionFailureException;
6   import com.jsql.model.exception.JSqlException;
7   import com.jsql.model.injection.method.AbstractMethodInjection;
8   import org.apache.commons.lang3.StringUtils;
9   import org.apache.logging.log4j.LogManager;
10  import org.apache.logging.log4j.Logger;
11  
12  import java.io.IOException;
13  import java.net.Authenticator;
14  import java.net.CookieManager;
15  import java.net.PasswordAuthentication;
16  import java.net.URI;
17  import java.net.http.HttpClient;
18  import java.net.http.HttpClient.Version;
19  import java.net.http.HttpHeaders;
20  import java.net.http.HttpRequest;
21  import java.net.http.HttpRequest.BodyPublishers;
22  import java.net.http.HttpRequest.Builder;
23  import java.net.http.HttpResponse;
24  import java.net.http.HttpResponse.BodyHandlers;
25  import java.security.SecureRandom;
26  import java.time.Duration;
27  import java.util.*;
28  import java.util.AbstractMap.SimpleEntry;
29  import java.util.Map.Entry;
30  import java.util.stream.Collectors;
31  import java.util.stream.Stream;
32  
33  /**
34   * Utility class in charge of connection to web resources and management
35   * of source page and request and response headers.
36   */
37  public class ConnectionUtil {
38      
39      private static final Logger LOGGER = LogManager.getRootLogger();
40      
41      private String urlByUser;  // URL entered by user
42      private String urlBase;  // URL entered by user without the query string
43      private AbstractMethodInjection methodInjection;  // Method of injection: by query string, request or header
44      private String typeRequest = StringUtil.GET;  // Default HTTP method. It can be changed to a custom method
45      private final Random randomForUserAgent = new SecureRandom();
46      private final InjectionModel injectionModel;
47      private final CookieManager cookieManager = new CookieManager();
48      
49      public ConnectionUtil(InjectionModel injectionModel) {
50          this.injectionModel = injectionModel;
51      }
52      
53      public HttpClient.Builder getHttpClient() {
54          var httpClientBuilder = HttpClient.newBuilder()
55              .connectTimeout(Duration.ofSeconds(this.getTimeout()))
56              .sslContext(this.injectionModel.getMediatorUtils().certificateUtil().getSslContext())
57              .followRedirects(
58                  this.injectionModel.getMediatorUtils().preferencesUtil().isFollowingRedirection()
59                  ? HttpClient.Redirect.ALWAYS
60                  : HttpClient.Redirect.NEVER
61              );
62          if (this.injectionModel.getMediatorUtils().preferencesUtil().isHttp2Disabled()) {
63              httpClientBuilder.version(Version.HTTP_1_1);
64          }
65          if (!this.injectionModel.getMediatorUtils().preferencesUtil().isNotProcessingCookies()) {
66              httpClientBuilder.cookieHandler(this.cookieManager);
67          }
68          if (this.injectionModel.getMediatorUtils().authenticationUtil().isAuthentEnabled()) {
69              httpClientBuilder.authenticator(new Authenticator() {
70                  @Override
71                  protected PasswordAuthentication getPasswordAuthentication() {
72                      return new PasswordAuthentication(
73                          ConnectionUtil.this.injectionModel.getMediatorUtils().authenticationUtil().getUsernameAuthentication(),
74                          ConnectionUtil.this.injectionModel.getMediatorUtils().authenticationUtil().getPasswordAuthentication().toCharArray()
75                      );
76                  }
77              });
78          }
79          return httpClientBuilder;
80      }
81  
82      public static <T> Map<String, String> getHeadersMap(HttpResponse<T> httpResponse) {
83          Map<String, String> sortedMap = ConnectionUtil.getHeadersMap(httpResponse.headers());
84          String responseCodeHttp = String.valueOf(httpResponse.statusCode());
85          sortedMap.put(":status", responseCodeHttp);
86          return sortedMap;
87      }
88      
89      public static Map<String, String> getHeadersMap(HttpHeaders httpHeaders) {
90          Map<String, String> unsortedMap = httpHeaders.map()
91              .entrySet()
92              .stream()
93              .sorted(Entry.comparingByKey())
94              .map(entrySet -> new SimpleEntry<>(
95                  entrySet.getKey(),
96                  String.join(", ", entrySet.getValue())
97              ))
98              .collect(Collectors.toMap(
99                  SimpleEntry::getKey,
100                 SimpleEntry::getValue
101             ));
102         return new TreeMap<>(unsortedMap);
103     }
104 
105     /**
106      * Check that the connection to the website is working correctly.
107      * It uses authentication defined by user, with fixed timeout, and warn
108      * user in case of authentication detected.
109      */
110     public HttpResponse<String> checkConnectionResponse() throws IOException, InterruptedException, JSqlException {
111         var queryString = this.injectionModel.getMediatorUtils().parameterUtil().getQueryStringFromEntries();
112         var testUrl = this.getUrlBase().replaceAll("\\?$", StringUtils.EMPTY);
113 
114         if (StringUtils.isNotEmpty(queryString)) {
115             testUrl += "?"+ queryString;
116         }
117 
118         String contentTypeRequest = "text/plain";
119 
120         var body = this.injectionModel.getMediatorUtils().parameterUtil().getRawRequest();
121 
122         if (this.injectionModel.getMediatorUtils().parameterUtil().isMultipartRequest()) {
123             body = body.replaceAll("(?s)\\\\n", "\r\n");
124         } else if (this.injectionModel.getMediatorUtils().parameterUtil().isRequestSoap()) {
125             contentTypeRequest = "text/xml";
126         } else if (!this.injectionModel.getMediatorUtils().parameterUtil().getListRequest().isEmpty()) {
127             contentTypeRequest = "application/x-www-form-urlencoded";
128         }
129 
130         // Test the HTTP connection
131         Builder httpRequest = HttpRequest.newBuilder();
132         try {
133             httpRequest.uri(
134                 URI.create(
135                     testUrl  // Get encoded params without fragment
136                     .replace(InjectionModel.STAR, StringUtils.EMPTY)  // Ignore injection point during the test
137                 )
138             );
139         } catch (IllegalArgumentException e) {
140             throw new JSqlException(e);
141         }
142         httpRequest.setHeader(HeaderUtil.CONTENT_TYPE_REQUEST, contentTypeRequest)
143             .timeout(Duration.ofSeconds(this.getTimeout()));
144         
145         this.injectionModel.getMediatorUtils().csrfUtil().addHeaderToken(httpRequest);
146         this.injectionModel.getMediatorUtils().digestUtil().addHeaderToken(httpRequest);
147 
148         httpRequest.method(this.typeRequest, BodyPublishers.ofString(body));
149 
150         // Add headers if exists (Authorization:Basic, etc.)
151         for (SimpleEntry<String, String> header: this.injectionModel.getMediatorUtils().parameterUtil().getListHeader()) {
152             HeaderUtil.sanitizeHeaders(httpRequest, header);
153         }
154 
155         return this.injectionModel.getMediatorUtils().headerUtil().checkResponseHeader(httpRequest, body);
156     }
157 
158     public void testConnection() throws IOException, InterruptedException, JSqlException {
159         // Check connection is working: define Cookie management, check HTTP status, parse <form> parameters, process CSRF
160         LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, () -> I18nUtil.valueByKey("LOG_CONNECTION_TEST"));
161         this.getCookieManager().getCookieStore().removeAll();
162         HttpResponse<String> httpResponse = this.checkConnectionResponse();
163 
164         if (
165             (httpResponse.statusCode() == 401 || httpResponse.statusCode() == 403)
166             && !this.injectionModel.getMediatorUtils().preferencesUtil().isNotProcessingCookies()
167             && (
168                 this.injectionModel.getMediatorUtils().csrfUtil().isCsrf()
169                 || this.injectionModel.getMediatorUtils().digestUtil().isDigest()
170             )
171         ) {
172             if (this.injectionModel.getMediatorUtils().preferencesUtil().isProcessingCsrf()) {
173                 LOGGER.log(LogLevelUtil.CONSOLE_INFORM, () -> "Testing CSRF handshake from previous connection...");
174             } else if (StringUtils.isNotEmpty(this.injectionModel.getMediatorUtils().digestUtil().getTokenDigest())) {
175                 LOGGER.log(LogLevelUtil.CONSOLE_INFORM, () -> "Testing Digest handshake from previous connection...");
176             }
177             httpResponse = this.checkConnectionResponse();
178         }
179 
180         if (httpResponse.statusCode() >= 400 && !this.injectionModel.getMediatorUtils().preferencesUtil().isNotTestingConnection()) {
181             throw new InjectionFailureException(String.format("Connection failed: problem when calling %s", httpResponse.uri().toURL()));
182         }
183     }
184 
185     /**
186      * Call a URL and return the source page.
187      * @param url to call
188      * @return the source page of the URL
189      */
190     public String getSourceLineFeed(String url) {
191         return this.getSource(url, true, false);
192     }
193 
194     public String getSource(String url) {
195         return this.getSource(url, false, false);
196     }
197 
198     public String getSource(String url, boolean isConnectIssueIgnored) {  // reverse init result can be ignored
199         return this.getSource(url, false, isConnectIssueIgnored);
200     }
201 
202     public String getSource(String url, boolean lineFeed, boolean isConnectIssueIgnored) {
203         String pageSource = StringUtils.EMPTY;
204         Map<String, String> requestHeaders = Map.of();
205         Map<String, String> responseHeaders = Map.of();
206 
207         try (var httpClient = this.getHttpClient().build()) {
208             var httpRequest = HttpRequest.newBuilder()
209                 .uri(URI.create(url))
210                 .timeout(Duration.ofSeconds(this.getTimeout()))
211                 .build();
212 
213             if (lineFeed) {
214                 HttpResponse<Stream<String>> response = httpClient.send(httpRequest, BodyHandlers.ofLines());
215                 pageSource = response.body().collect(Collectors.joining("\n"));
216                 responseHeaders = ConnectionUtil.getHeadersMap(response.headers());
217             } else {
218                 HttpResponse<String> response = httpClient.send(httpRequest, BodyHandlers.ofString());
219                 pageSource = response.body();
220                 responseHeaders = ConnectionUtil.getHeadersMap(response.headers());
221             }
222             requestHeaders = ConnectionUtil.getHeadersMap(httpRequest.headers());
223 
224         } catch (IOException e) {
225             if (!isConnectIssueIgnored) {  // ignoring reverse connection timeout
226                 LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
227             }
228         } catch (InterruptedException e) {
229             LOGGER.log(LogLevelUtil.IGNORE, e, e);
230             Thread.currentThread().interrupt();
231         } finally {
232             // Inform the view about the log infos
233             this.injectionModel.sendToViews(new Seal.MessageHeader(
234                 url,
235                 null,
236                 requestHeaders,
237                 responseHeaders,
238                 pageSource,
239                 null,
240                 null,
241                 null,
242                 null
243             ));
244         }
245         
246         return pageSource.trim();
247     }
248 
249     public void setCustomUserAgent(Builder httpRequest) {
250         if (this.injectionModel.getMediatorUtils().preferencesUtil().isUserAgentRandom()) {
251             String agents = this.injectionModel.getMediatorUtils().userAgentUtil().getCustomUserAgent();
252             List<String> listAgents = Stream.of(agents.split("[\\r\\n]+"))
253                 .filter(q -> !q.matches("^#.*"))
254                 .toList();
255             String randomElement = listAgents.get(this.randomForUserAgent.nextInt(listAgents.size()));
256             httpRequest.setHeader("User-Agent", randomElement);
257         }
258     }
259     
260     
261     // Builder
262 
263     public ConnectionUtil withMethodInjection(AbstractMethodInjection methodInjection) {
264         this.methodInjection = methodInjection;
265         return this;
266     }
267     
268     public ConnectionUtil withTypeRequest(String typeRequest) {
269         this.typeRequest = typeRequest;
270         return this;
271     }
272     
273     
274     // Getters and setters
275     
276     public String getUrlByUser() {
277         return this.urlByUser;
278     }
279 
280     public void setUrlByUser(String urlByUser) {
281         this.urlByUser = urlByUser;
282     }
283     
284     public String getUrlBase() {
285         return this.urlBase;
286     }
287 
288     public void setUrlBase(String urlBase) {
289         this.urlBase = urlBase;
290     }
291     
292     public AbstractMethodInjection getMethodInjection() {
293         return this.methodInjection;
294     }
295 
296     public void setMethodInjection(AbstractMethodInjection methodInjection) {
297         this.methodInjection = methodInjection;
298     }
299     
300     public String getTypeRequest() {
301         return this.typeRequest;
302     }
303 
304     public void setTypeRequest(String typeRequest) {
305         this.typeRequest = typeRequest;
306     }
307 
308     /**
309      * Default timeout used by the jcifs fix. It's the default value used usually by the JVM.
310      */
311     public Integer getTimeout() {
312         return this.injectionModel.getMediatorUtils().preferencesUtil().countConnectionTimeout();
313     }
314 
315     public CookieManager getCookieManager() {
316         return this.cookieManager;
317     }
318 }