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