1 /* 2 * Created on Jun 14, 2004 3 * 4 * Paros and its related class files. 5 * 6 * Paros is an HTTP/HTTPS proxy for assessing web application security. 7 * Copyright (C) 2003-2004 Chinotec Technologies Company 8 * 9 * This program is free software; you can redistribute it and/or 10 * modify it under the terms of the Clarified Artistic License 11 * as published by the Free Software Foundation. 12 * 13 * This program is distributed in the hope that it will be useful, 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 * Clarified Artistic License for more details. 17 * 18 * You should have received a copy of the Clarified Artistic License 19 * along with this program; if not, write to the Free Software 20 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 21 */ 22 // ZAP: 2012/03/15 Added the @Override annotation to the appropriate methods. 23 // Moved to this class the method getCookieParams(). 24 // ZAP: 2012/06/24 Added new method of getting cookies from the request header. 25 // ZAP: 2012/07/11 Added method to check if response type is text/html (isHtml()) 26 // ZAP: 2012/08/06 Modified isText() to also consider javascript as text 27 // ZAP: 2013/02/12 Modified isText() to also consider atom+xml as text 28 // ZAP: 2013/03/08 Improved parse error reporting 29 // ZAP: 2014/02/21 i1046: The getHttpCookies() method in the HttpResponseHeader does not properly 30 // set the domain 31 // ZAP: 2014/04/09 i1145: Cookie parsing error if a comma is used 32 // ZAP: 2015/02/26 Include json as a text content type 33 // ZAP: 2016/06/17 Remove redundant initialisations of instance variables 34 // ZAP: 2017/03/21 Add method to check if response type is json (isJson()) 35 // ZAP: 2017/11/10 Allow to set the status code and reason. 36 // ZAP: 2018/02/06 Make the lower/upper case changes locale independent (Issue 4327). 37 // ZAP: 2018/07/23 Add CSP headers. 38 // ZAP: 2018/08/15 Add Server header. 39 // ZAP: 2019/06/01 Normalise line endings. 40 // ZAP: 2019/06/05 Normalise format/style. 41 // ZAP: 2019/12/09 Address deprecation of getHeaders(String) Vector method. 42 // ZAP: 2020/11/10 Add convenience method isCss(). 43 // ZAP: 2020/11/26 Use Log4j 2 classes for logging. 44 // ZAP: 2021/05/14 Remove redundant type arguments. 45 package org.parosproxy.paros.network; 46 47 import java.net.HttpCookie; 48 import java.util.ArrayList; 49 import java.util.Iterator; 50 import java.util.LinkedList; 51 import java.util.List; 52 import java.util.Locale; 53 import java.util.TreeSet; 54 import java.util.regex.Matcher; 55 import java.util.regex.Pattern; 56 import org.apache.logging.log4j.LogManager; 57 import org.apache.logging.log4j.Logger; 58 59 public class HttpResponseHeader extends HttpHeader { 60 61 /** 62 * The {@code Content-Security-Policy} response header. 63 * 64 * @since 2.8.0 65 */ 66 public static final String CSP = "Content-Security-Policy"; 67 68 /** 69 * The {@code Content-Security-Policy-Report-Only} response header. 70 * 71 * @since 2.8.0 72 */ 73 public static final String CSP_REPORT_ONLY = "Content-Security-Policy-Report-Only"; 74 75 /** 76 * The {@code X-Content-Security-Policy} response header. 77 * 78 * @since 2.8.0 79 */ 80 public static final String XCSP = "X-Content-Security-Policy"; 81 82 /** 83 * The {@code X-WebKit-CSP} response header. 84 * 85 * @since 2.8.0 86 */ 87 public static final String WEBKIT_CSP = "X-WebKit-CSP"; 88 89 /** 90 * The {@code Server} response header. 91 * 92 * @since 2.8.0 93 */ 94 public static final String SERVER = "Server"; 95 96 private static final long serialVersionUID = 2812716126742059785L; 97 private static final Logger log = LogManager.getLogger(HttpResponseHeader.class); 98 99 public static final String HTTP_CLIENT_BAD_REQUEST = "HTTP/1.0 400 Bad request" + CRLF + CRLF; 100 private static final String _CONTENT_TYPE_CSS = "css"; 101 private static final String _CONTENT_TYPE_IMAGE = "image"; 102 private static final String _CONTENT_TYPE_TEXT = "text"; 103 private static final String _CONTENT_TYPE_HTML = "html"; 104 private static final String _CONTENT_TYPE_JAVASCRIPT = "javascript"; 105 private static final String _CONTENT_TYPE_JSON = "json"; 106 private static final String _CONTENT_TYPE_XML = "xml"; 107 108 static final Pattern patternStatusLine = 109 Pattern.compile( 110 p_VERSION + p_SP + p_STATUS_CODE + " *" + p_REASON_PHRASE, 111 Pattern.CASE_INSENSITIVE); 112 private static final Pattern patternPartialStatusLine = 113 Pattern.compile("\\A *" + p_VERSION, Pattern.CASE_INSENSITIVE); 114 115 private String mStatusCodeString; 116 private int mStatusCode; 117 private String mReasonPhrase; 118 HttpResponseHeader()119 public HttpResponseHeader() { 120 mStatusCodeString = ""; 121 mReasonPhrase = ""; 122 } 123 HttpResponseHeader(String data)124 public HttpResponseHeader(String data) throws HttpMalformedHeaderException { 125 super(data); 126 } 127 128 @Override clear()129 public void clear() { 130 super.clear(); 131 mStatusCodeString = ""; 132 mStatusCode = 0; 133 mReasonPhrase = ""; 134 } 135 136 @Override setMessage(String data)137 public void setMessage(String data) throws HttpMalformedHeaderException { 138 super.setMessage(data); 139 try { 140 parse(); 141 } catch (HttpMalformedHeaderException e) { 142 mMalformedHeader = true; 143 throw e; 144 } 145 } 146 147 @Override setVersion(String version)148 public void setVersion(String version) { 149 mVersion = version.toUpperCase(Locale.ROOT); 150 } 151 152 /** 153 * Gets the status code. 154 * 155 * @return the status code. 156 * @see #setStatusCode(int) 157 */ getStatusCode()158 public int getStatusCode() { 159 return mStatusCode; 160 } 161 162 /** 163 * Sets the status code. 164 * 165 * <p><code>status-code = 3DIGIT</code> 166 * 167 * @param statusCode the new status code. 168 * @throws IllegalArgumentException if the given status code is not a (positive) 3 digit number. 169 * @see #getStatusCode() 170 * @since 2.7.0 171 */ setStatusCode(int statusCode)172 public void setStatusCode(int statusCode) { 173 if (statusCode < 100 || statusCode > 999) { 174 throw new IllegalArgumentException( 175 "The status code must be a (positive) 3 digit number."); 176 } 177 this.mStatusCode = statusCode; 178 } 179 180 /** 181 * Gets the reason phrase. 182 * 183 * @return the reason phrase. 184 * @see #setReasonPhrase(String) 185 */ getReasonPhrase()186 public String getReasonPhrase() { 187 return mReasonPhrase; 188 } 189 190 /** 191 * Sets the reason phrase. 192 * 193 * <p>If {@code null} it's set an empty string. 194 * 195 * @param reasonPhrase the new reason phrase. 196 * @see #getReasonPhrase() 197 * @since 2.7.0 198 */ setReasonPhrase(String reasonPhrase)199 public void setReasonPhrase(String reasonPhrase) { 200 this.mReasonPhrase = reasonPhrase != null ? reasonPhrase : ""; 201 } 202 parse()203 private void parse() throws HttpMalformedHeaderException { 204 205 Matcher matcher = patternStatusLine.matcher(mStartLine); 206 if (!matcher.find()) { 207 mMalformedHeader = true; 208 throw new HttpMalformedHeaderException("Failed to find pattern: " + patternStatusLine); 209 } 210 211 mVersion = matcher.group(1); 212 mStatusCodeString = matcher.group(2); 213 setReasonPhrase(matcher.group(3)); 214 215 if (!mVersion.equalsIgnoreCase(HTTP10) && !mVersion.equalsIgnoreCase(HTTP11)) { 216 mMalformedHeader = true; 217 throw new HttpMalformedHeaderException("Unexpected version: " + mVersion); 218 // return false; 219 } 220 221 try { 222 mStatusCode = Integer.parseInt(mStatusCodeString); 223 } catch (NumberFormatException e) { 224 mMalformedHeader = true; 225 throw new HttpMalformedHeaderException("Unexpected status code: " + mStatusCodeString); 226 } 227 } 228 229 @Override getContentLength()230 public int getContentLength() { 231 int len = super.getContentLength(); 232 233 if ((mStatusCode >= 100 && mStatusCode < 200) 234 || mStatusCode == HttpStatusCode.NO_CONTENT 235 || mStatusCode == HttpStatusCode.NOT_MODIFIED) { 236 return 0; 237 } else if (mStatusCode >= 200 && mStatusCode < 300) { 238 return len; 239 } else if (len > 0) { 240 return len; 241 } else { 242 return 0; 243 } 244 } 245 getError(String msg)246 public static HttpResponseHeader getError(String msg) { 247 HttpResponseHeader res = null; 248 try { 249 res = new HttpResponseHeader(msg); 250 } catch (HttpMalformedHeaderException e) { 251 } 252 return res; 253 } 254 255 @Override isImage()256 public boolean isImage() { 257 return hasContentType(_CONTENT_TYPE_IMAGE); 258 } 259 260 @Override isText()261 public boolean isText() { 262 return hasContentType( 263 _CONTENT_TYPE_TEXT, 264 _CONTENT_TYPE_HTML, 265 _CONTENT_TYPE_JAVASCRIPT, 266 _CONTENT_TYPE_JSON, 267 _CONTENT_TYPE_XML); 268 } 269 isHtml()270 public boolean isHtml() { 271 return hasContentType(_CONTENT_TYPE_HTML); 272 } 273 isXml()274 public boolean isXml() { 275 return hasContentType(_CONTENT_TYPE_XML); 276 } 277 isJson()278 public boolean isJson() { 279 return hasContentType(_CONTENT_TYPE_JSON); 280 } 281 isJavaScript()282 public boolean isJavaScript() { 283 return hasContentType(_CONTENT_TYPE_JAVASCRIPT); 284 } 285 isCss()286 public boolean isCss() { 287 return hasContentType(_CONTENT_TYPE_CSS); 288 } 289 isStatusLine(String data)290 public static boolean isStatusLine(String data) { 291 return patternPartialStatusLine.matcher(data).find(); 292 } 293 294 @Override getPrimeHeader()295 public String getPrimeHeader() { 296 String prime = getVersion() + " " + getStatusCode(); 297 if (getReasonPhrase() != null && !getReasonPhrase().equals("")) { 298 prime = prime + " " + getReasonPhrase(); 299 } 300 return prime; 301 } 302 303 // ZAP: Added method for working directly with HttpCookie 304 305 /** 306 * Parses the response headers and build a lis of all the http cookies set. For the cookies 307 * whose domain could not be determined, the {@code defaultDomain} is set. 308 * 309 * @param defaultDomain the default domain 310 * @return the http cookies 311 */ getHttpCookies(String defaultDomain)312 public List<HttpCookie> getHttpCookies(String defaultDomain) { 313 List<HttpCookie> cookies = new LinkedList<>(); 314 315 List<String> cookiesS = getHeaderValues(HttpHeader.SET_COOKIE); 316 317 for (String c : cookiesS) { 318 cookies.addAll(parseCookieString(c, defaultDomain)); 319 } 320 321 cookiesS = getHeaderValues(HttpHeader.SET_COOKIE2); 322 for (String c : cookiesS) { 323 cookies.addAll(parseCookieString(c, defaultDomain)); 324 } 325 326 return cookies; 327 } 328 parseCookieString(String c, String defaultDomain)329 private List<HttpCookie> parseCookieString(String c, String defaultDomain) { 330 try { 331 List<HttpCookie> parsedCookies = HttpCookie.parse(c); 332 if (defaultDomain != null) { 333 for (HttpCookie cookie : parsedCookies) { 334 if (cookie.getDomain() == null) { 335 cookie.setDomain(defaultDomain); 336 } 337 } 338 } 339 return parsedCookies; 340 } catch (IllegalArgumentException e) { 341 if (c.indexOf(',') >= 0) { 342 try { 343 // Some sites seem to use comma separators, which HttpCookie doesn't like, try 344 // replacing them 345 List<HttpCookie> parsedCookies = HttpCookie.parse(c.replace(',', ';')); 346 if (defaultDomain != null) { 347 for (HttpCookie cookie : parsedCookies) { 348 if (cookie.getDomain() == null) { 349 cookie.setDomain(defaultDomain); 350 } 351 } 352 } 353 return parsedCookies; 354 } catch (IllegalArgumentException e2) { 355 log.error("Failed to parse cookie: " + c, e); 356 } 357 } 358 } 359 return new ArrayList<>(); 360 } 361 362 /** 363 * Parses the response headers and build a lis of all the http cookies set. <br> 364 * NOTE: For the cookies whose domain could not be determined, no domain is set, so this must be 365 * taken into account. 366 * 367 * @return the http cookies 368 * @deprecated Use the {@link #getHttpCookies(String)} method to take into account the default 369 * domain for cookie 370 */ 371 @Deprecated getHttpCookies()372 public List<HttpCookie> getHttpCookies() { 373 return getHttpCookies(null); 374 } 375 376 // ZAP: Added method. getCookieParams()377 public TreeSet<HtmlParameter> getCookieParams() { 378 TreeSet<HtmlParameter> set = new TreeSet<>(); 379 380 Iterator<String> cookiesIt = getHeaderValues(HttpHeader.SET_COOKIE).iterator(); 381 while (cookiesIt.hasNext()) { 382 set.add(new HtmlParameter(cookiesIt.next())); 383 } 384 385 Iterator<String> cookies2It = getHeaderValues(HttpHeader.SET_COOKIE2).iterator(); 386 while (cookies2It.hasNext()) { 387 set.add(new HtmlParameter(cookies2It.next())); 388 } 389 return set; 390 } 391 } 392