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