View Javadoc
1   package com.jsql.util;
2   
3   import com.jsql.model.InjectionModel;
4   import com.jsql.model.bean.util.Header;
5   import com.jsql.model.bean.util.Interaction;
6   import com.jsql.model.bean.util.Request;
7   import com.jsql.model.exception.JSqlException;
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.HttpCookie;
14  import java.net.URLEncoder;
15  import java.net.http.HttpRequest.Builder;
16  import java.net.http.HttpResponse;
17  import java.net.http.HttpResponse.BodyHandlers;
18  import java.nio.charset.StandardCharsets;
19  import java.text.DecimalFormat;
20  import java.util.AbstractMap.SimpleEntry;
21  import java.util.EnumMap;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.regex.Pattern;
25  import java.util.stream.Collectors;
26  import java.util.stream.Stream;
27  
28  public class HeaderUtil {
29      
30      private static final Logger LOGGER = LogManager.getRootLogger();
31      
32      public static final String CONTENT_TYPE_REQUEST = "Content-Type";
33      public static final String WWW_AUTHENTICATE_RESPONSE = "www-authenticate";
34      private static final String REGEX_HTTP_STATUS = "4\\d\\d";
35      private static final String FOUND_STATUS_HTTP = "Found status HTTP";
36  
37      private final InjectionModel injectionModel;
38      
39      public HeaderUtil(InjectionModel injectionModel) {
40          this.injectionModel = injectionModel;
41      }
42  
43      /**
44       * Parse the header component and decode any character of the form %xy
45       * except for cookie
46       * @param httpRequest where decoded value will be set
47       * @param header string to decode
48       */
49      public static void sanitizeHeaders(Builder httpRequest, SimpleEntry<String, String> header) throws JSqlException {
50          String keyHeader = header.getKey().trim();
51          String valueHeader = header.getValue().trim();
52  
53          if ("cookie".equalsIgnoreCase(keyHeader) && Pattern.compile(".+=.*").matcher(valueHeader).find()) {
54              // Encode cookies to double quotes: Cookie: key="<value>"
55              List<String> cookies = Stream.of(valueHeader.split(";"))
56                  .filter(value -> value != null && value.contains("="))
57                  .map(cookie -> cookie.split("=", 2))
58                  .map(arrayEntry -> arrayEntry[0].trim() + "=" + (
59                      arrayEntry[1] == null
60                      ? StringUtils.EMPTY
61                      // Url encode: new cookie RFC restricts chars to non ()<>@,;:\"/[]?={} => server must url decode the request
62                      : URLEncoder.encode(arrayEntry[1].trim().replace("+", "%2B"), StandardCharsets.UTF_8)
63                  ))
64                  .collect(Collectors.toList());
65              valueHeader = String.join("; ", cookies);
66          }
67  
68          try {
69              httpRequest.setHeader(
70                  keyHeader,
71                  valueHeader.replaceAll("[^\\p{ASCII}]", StringUtils.EMPTY)
72              );
73          } catch (IllegalArgumentException e) {
74              throw new JSqlException(e);
75          }
76      }
77  
78      /**
79       * Verify the headers received after a request, detect authentication response and
80       * send the headers to the view.
81       * @param httpRequestBuilder calls URL
82       * @return httpResponse with response headers
83       * @throws IOException when an error occurs during connection
84       */
85      public HttpResponse<String> checkResponseHeader(Builder httpRequestBuilder, String body) throws IOException, InterruptedException {
86          var httpRequest = httpRequestBuilder.build();
87          HttpResponse<String> httpResponse = this.injectionModel.getMediatorUtils().getConnectionUtil().getHttpClient().build().send(
88              httpRequest,
89              BodyHandlers.ofString()
90          );
91          String pageSource = httpResponse.body();
92          
93          List<HttpCookie> cookies = this.injectionModel.getMediatorUtils().getConnectionUtil().getCookieManager().getCookieStore().getCookies();
94          if (!cookies.isEmpty()) {
95              LOGGER.info("Cookies set by host: {}", cookies);
96          }
97  
98          var responseCode = Integer.toString(httpResponse.statusCode());
99          Map<String, String> mapResponseHeaders = ConnectionUtil.getHeadersMap(httpResponse);
100         this.checkResponse(responseCode, mapResponseHeaders);
101         this.checkStatus(httpResponse);
102         
103         this.injectionModel.getMediatorUtils().getFormUtil().parseForms(httpResponse.statusCode(), pageSource);
104         this.injectionModel.getMediatorUtils().getCsrfUtil().parseForCsrfToken(pageSource, mapResponseHeaders);
105         this.injectionModel.getMediatorUtils().getDigestUtil().parseWwwAuthenticate(mapResponseHeaders);
106 
107         Map<Header, Object> msgHeader = new EnumMap<>(Header.class);
108         
109         int sizeHeaders = mapResponseHeaders.keySet()
110             .stream()
111             .map(key -> mapResponseHeaders.get(key).length() + key.length())
112             .mapToInt(Integer::intValue)
113             .sum();
114         
115         float size = (float) (pageSource.length() + sizeHeaders) / 1024;
116         var decimalFormat = new DecimalFormat("0.000");
117         msgHeader.put(Header.PAGE_SIZE, decimalFormat.format(size));
118 
119         msgHeader.put(Header.URL, httpRequest.uri().toURL().toString());
120         msgHeader.put(Header.POST, body);
121         msgHeader.put(Header.HEADER, ConnectionUtil.getHeadersMap(httpRequest.headers()));
122         msgHeader.put(Header.RESPONSE, mapResponseHeaders);
123         msgHeader.put(Header.SOURCE, pageSource);
124         msgHeader.put(Header.METADATA_STRATEGY, "#none");
125         msgHeader.put(Header.METADATA_PROCESS, "test#conn");
126         
127         // Inform the view about the log info
128         var request = new Request();
129         request.setMessage(Interaction.MESSAGE_HEADER);
130         request.setParameters(msgHeader);
131         this.injectionModel.sendToViews(request);
132         
133         return httpResponse;
134     }
135 
136     private void checkStatus(HttpResponse<String> response) {
137         if (response.statusCode() >= 400) {
138             if (this.injectionModel.getMediatorUtils().getPreferencesUtil().isNotTestingConnection()) {
139                 LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "Connection test disabled, skipping error {}...", response.statusCode());
140             } else {
141                 LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Try with option 'Disable connection test' to skip HTTP error {}", response.statusCode());
142             }
143         }
144     }
145 
146     private void checkResponse(String responseCode, Map<String, String> mapResponse) {
147         if (this.isBasicAuth(responseCode, mapResponse)) {
148             LOGGER.log(
149                 LogLevelUtil.CONSOLE_ERROR,
150                 "Basic Authentication detected: "
151                 + "set authentication in preferences, "
152                 + "or add header 'Authorization: Basic b3N..3Jk', with b3N..3Jk as "
153                 + "'osUserName:osPassword' encoded in Base64 (use the Coder in jSQL to encode the string)."
154             );
155         } else if (this.isNtlm(responseCode, mapResponse)) {
156             LOGGER.log(
157                 LogLevelUtil.CONSOLE_ERROR,
158                 "NTLM Authentication detected: "
159                 + "set authentication in preferences, "
160                 + "or add username, password and domain information to the URL, e.g. http://domain\\user:password@127.0.0.1/[..]"
161             );
162         } else if (this.isDigest(responseCode, mapResponse)) {
163             LOGGER.log(
164                 LogLevelUtil.CONSOLE_ERROR,
165                 "Digest Authentication detected: set authentication in preferences."
166             );
167         } else if (this.isNegotiate(responseCode, mapResponse)) {
168             LOGGER.log(
169                 LogLevelUtil.CONSOLE_ERROR,
170                 "Negotiate Authentication detected: "
171                 + "add username, password and domain information to the URL, e.g. http://domain\\user:password@127.0.0.1/[..]"
172             );
173         } else if (Pattern.matches("1\\d\\d", responseCode)) {
174             LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "{} {} Informational", HeaderUtil.FOUND_STATUS_HTTP, responseCode);
175         } else if (Pattern.matches("2\\d\\d", responseCode)) {
176             LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, "{} {} Success", HeaderUtil.FOUND_STATUS_HTTP, responseCode);
177         } else if (Pattern.matches("3\\d\\d", responseCode)) {
178             
179             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "{} {} Redirection", HeaderUtil.FOUND_STATUS_HTTP, responseCode);
180             
181             if (!this.injectionModel.getMediatorUtils().getPreferencesUtil().isFollowingRedirection()) {
182                 LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "If injection fails retry with option 'Follow HTTP redirection' activated");
183             } else {
184                 LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Redirecting to the next page...");
185             }
186         } else if (Pattern.matches(HeaderUtil.REGEX_HTTP_STATUS, responseCode)) {
187             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "{} {} Client Error", HeaderUtil.FOUND_STATUS_HTTP, responseCode);
188         } else if (Pattern.matches("5\\d\\d", responseCode)) {
189             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "{} {} Server Error", HeaderUtil.FOUND_STATUS_HTTP, responseCode);
190         } else {
191             LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "{} {} Unknown", HeaderUtil.FOUND_STATUS_HTTP, responseCode);
192         }
193     }
194     
195     private boolean isNegotiate(String responseCode, Map<String, String> mapResponse) {
196         return Pattern.matches(HeaderUtil.REGEX_HTTP_STATUS, responseCode)
197             && mapResponse.containsKey(HeaderUtil.WWW_AUTHENTICATE_RESPONSE)
198             && "Negotiate".equals(mapResponse.get(HeaderUtil.WWW_AUTHENTICATE_RESPONSE));
199     }
200 
201     private boolean isDigest(String responseCode, Map<String, String> mapResponse) {
202         return Pattern.matches(HeaderUtil.REGEX_HTTP_STATUS, responseCode)
203             && mapResponse.containsKey(HeaderUtil.WWW_AUTHENTICATE_RESPONSE)
204             && mapResponse.get(HeaderUtil.WWW_AUTHENTICATE_RESPONSE) != null
205             && mapResponse.get(HeaderUtil.WWW_AUTHENTICATE_RESPONSE).startsWith("Digest ");
206     }
207 
208     private boolean isNtlm(String responseCode, Map<String, String> mapResponse) {
209         return Pattern.matches(HeaderUtil.REGEX_HTTP_STATUS, responseCode)
210             && mapResponse.containsKey(HeaderUtil.WWW_AUTHENTICATE_RESPONSE)
211             && "NTLM".equals(mapResponse.get(HeaderUtil.WWW_AUTHENTICATE_RESPONSE));
212     }
213 
214     private boolean isBasicAuth(String responseCode, Map<String, String> mapResponse) {
215         return Pattern.matches(HeaderUtil.REGEX_HTTP_STATUS, responseCode)
216             && mapResponse.containsKey(HeaderUtil.WWW_AUTHENTICATE_RESPONSE)
217             && mapResponse.get(HeaderUtil.WWW_AUTHENTICATE_RESPONSE) != null
218             && mapResponse.get(HeaderUtil.WWW_AUTHENTICATE_RESPONSE).startsWith("Basic ");
219     }
220 }