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/01/12 Changed the method parse to use only CRLF as line separator. 23 // ZAP: 2012/03/15 Removed an unnecessary try catch block and unnecessary casting. 24 // Reworked the method getCharset. 25 // ZAP: 2012/04/23 Added @Override annotation to the appropriate method. 26 // ZAP: 2012/10/04 Changed to initialise the instance variable mVersion with a 27 // valid version (HttpHeader.HTTP10). 28 // ZAP: 2012/11/01 Issue 410: charset wrapped in quotation marks 29 // ZAP: 2013/04/08 Issue 605: Force intercepts via header 30 // ZAP: 2013/05/02 Re-arranged all modifiers into Java coding standard order 31 // ZAP: 2013/09/02 Resolved header value setting on setHeader() which manage wrongly the "-" char 32 // ZAP: 2013/11/16 Issue 867: HttpMessage#getFormParams should return an empty TreeSet if 33 // the request body is not "x-www-form-urlencoded" 34 // ZAP: 2015/03/26 Issue 1573: Add option to inject plugin ID in header for all ascan requests 35 // ZAP: 2016/06/17 Be lenient when parsing charset and accept single quote chars around the value 36 // ZAP: 2016/06/17 Remove redundant initialisations of instance variables 37 // ZAP: 2017/02/08 Change isEmpty to check start line instead of headers (if it has the 38 // status/request line it's not empty). 39 // ZAP: 2017/03/02 Issue 3226: Added API Key and Nonce headers 40 // ZAP: 2018/02/06 Make the lower/upper case changes locale independent (Issue 4327). 41 // ZAP: 2018/04/24 Add JSON Content-Type. 42 // ZAP: 2019/06/01 Normalise line endings. 43 // ZAP: 2019/06/05 Normalise format/style. 44 // ZAP: 2019/12/09 Added getHeaderValues(String) method (returning List) and deprecated 45 // getHeaders(String) method (returning Vector). 46 package org.parosproxy.paros.network; 47 48 import java.util.ArrayList; 49 import java.util.Collections; 50 import java.util.Hashtable; 51 import java.util.List; 52 import java.util.Locale; 53 import java.util.Vector; 54 import java.util.regex.Matcher; 55 import java.util.regex.Pattern; 56 import java.util.regex.PatternSyntaxException; 57 58 public abstract class HttpHeader implements java.io.Serializable { 59 60 private static final long serialVersionUID = 7922279497679304778L; 61 public static final String CRLF = "\r\n"; 62 public static final String LF = "\n"; 63 public static final String CONTENT_LENGTH = "Content-Length"; 64 public static final String TRANSFER_ENCODING = "Transfer-Encoding"; 65 public static final String CONTENT_ENCODING = "Content-Encoding"; 66 public static final String CONTENT_TYPE = "Content-Type"; 67 public static final String PROXY_CONNECTION = "Proxy-Connection"; 68 public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; 69 public static final String CONNECTION = "Connection"; 70 public static final String AUTHORIZATION = "Authorization"; 71 public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; 72 public static final String LOCATION = "Location"; 73 public static final String IF_MODIFIED_SINCE = "If-Modified-Since"; 74 public static final String IF_NONE_MATCH = "If-None-Match"; 75 public static final String USER_AGENT = "User-Agent"; 76 public static final String ACCEPT_ENCODING = "Accept-Encoding"; 77 // ZAP: the correct case is "Cache-Control", not "Cache-control" 78 public static final String CACHE_CONTROL = "Cache-Control"; 79 public static final String PRAGMA = "Pragma"; 80 public static final String REFERER = "Referer"; 81 public static final String X_ZAP_REQUESTID = "X-ZAP-RequestID"; 82 public static final String X_SECURITY_PROXY = "X-Security-Proxy"; 83 // ZAP: Added cookie headers 84 public static final String COOKIE = "Cookie"; 85 public static final String SET_COOKIE = "Set-Cookie"; 86 public static final String SET_COOKIE2 = "Set-Cookie2"; 87 public static final String X_XSS_PROTECTION = "X-XSS-Protection"; 88 public static final String X_FRAME_OPTION = "X-Frame-Options"; 89 public static final String X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options"; 90 public static final String HTTP09 = "HTTP/0.9"; 91 public static final String HTTP10 = "HTTP/1.0"; 92 public static final String HTTP11 = "HTTP/1.1"; 93 public static final String _CLOSE = "Close"; 94 public static final String _KEEP_ALIVE = "Keep-Alive"; 95 public static final String _CHUNKED = "Chunked"; 96 public static final String FORM_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"; 97 public static final String JSON_CONTENT_TYPE = "application/json"; 98 public static final String SCHEME_HTTP = "http://"; 99 public static final String SCHEME_HTTPS = "https://"; 100 public static final String HTTP = "http"; 101 public static final String HTTPS = "https"; 102 public static final String DEFLATE = "deflate"; 103 public static final String GZIP = "gzip"; 104 public static final String IDENTITY = "identity"; 105 public static final String SEC_PROXY_INTERCEPT = "intercept"; 106 public static final String SEC_PROXY_RECORD = "record"; 107 public static final Pattern patternCRLF = Pattern.compile("\\r\\n", Pattern.MULTILINE); 108 public static final Pattern patternLF = Pattern.compile("\\n", Pattern.MULTILINE); 109 // ZAP: Issue 410: charset wrapped in quotation marks 110 private static final Pattern patternCharset = 111 Pattern.compile( 112 "charset *= *(?:(?:'([^';\\s]+))|(?:\"?([^\";\\s]+)\"?))", 113 Pattern.CASE_INSENSITIVE); 114 protected static final String p_TEXT = "[^\\x00-\\x1f\\r\\n]*"; 115 protected static final String p_METHOD = "(\\w+)"; 116 protected static final String p_SP = " +"; 117 // protected static final String p_URI = "(\\S+)"; 118 // allow space in URI for encoding to %20 119 protected static final String p_URI = "([^\\r\\n]+)"; 120 protected static final String p_VERSION = "(HTTP/\\d+\\.\\d+)"; 121 protected static final String p_STATUS_CODE = "(\\d{3})"; 122 protected static final String p_REASON_PHRASE = "(" + p_TEXT + ")"; 123 protected String mStartLine; 124 protected String mMsgHeader; 125 protected boolean mMalformedHeader; 126 protected Hashtable<String, Vector<String>> mHeaderFields; 127 protected int mContentLength; 128 protected String mLineDelimiter; 129 protected String mVersion; 130 // ZAP: added CORS headers 131 public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; 132 public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; 133 public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; 134 public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; 135 // ZAP: added "Allow" and "Public" Headers, for response to "OPTIONS" method 136 public static final String METHODS_ALLOW = "Allow"; 137 public static final String METHODS_PUBLIC = "Public"; // IIS specific? 138 public static final String X_ZAP_SCAN_ID = "X-ZAP-Scan-ID"; 139 public static final String X_ZAP_API_KEY = "X-ZAP-API-Key"; 140 public static final String X_ZAP_API_NONCE = "X-ZAP-API-Nonce"; 141 // ZAP: additional standard/defacto headers 142 public static final String PROXY_AUTHORIZATION = "Proxy-Authorization"; 143 public static final String X_CSRF_TOKEN = "X-Csrf-Token"; 144 public static final String X_CSRFTOKEN = "X-CsrfToken"; 145 public static final String X_XSRF_TOKEN = "X-Xsrf-Token"; 146 HttpHeader()147 public HttpHeader() { 148 init(); 149 } 150 151 /** 152 * Construct a HttpHeader from a given String. 153 * 154 * @param data 155 * @throws HttpMalformedHeaderException 156 */ HttpHeader(String data)157 public HttpHeader(String data) throws HttpMalformedHeaderException { 158 setMessage(data); 159 } 160 161 /** Inititialization. */ init()162 private void init() { 163 mHeaderFields = new Hashtable<>(); 164 mStartLine = ""; 165 mMsgHeader = ""; 166 mMalformedHeader = false; 167 mContentLength = -1; 168 mLineDelimiter = CRLF; 169 mVersion = HttpHeader.HTTP10; 170 } 171 172 /** 173 * Set and parse this HTTP header with the string given. 174 * 175 * @param data 176 * @throws HttpMalformedHeaderException 177 */ setMessage(String data)178 public void setMessage(String data) throws HttpMalformedHeaderException { 179 clear(); 180 try { 181 if (!this.parse(data)) { 182 mMalformedHeader = true; 183 } 184 } catch (Exception e) { 185 mMalformedHeader = true; 186 } 187 188 if (mMalformedHeader) { 189 throw new HttpMalformedHeaderException(); 190 } 191 } 192 clear()193 public void clear() { 194 init(); 195 } 196 197 /** 198 * Get the first header value using the name given. If there are multiple occurrence, only the 199 * first one will be returned as String. 200 * 201 * @param name 202 * @return the header value. null if not found. 203 */ getHeader(String name)204 public String getHeader(String name) { 205 List<String> headers = getHeaderValues(name); 206 if (headers.isEmpty()) { 207 return null; 208 } 209 210 return headers.get(0); 211 } 212 213 /** 214 * Get headers with the name. Multiple value can be returned. 215 * 216 * @param name 217 * @return a vector holding the value as string. 218 * @deprecated since 2.9.0. See {@link #getHeaderValues(String)} instead 219 */ 220 @Deprecated getHeaders(String name)221 public Vector<String> getHeaders(String name) { 222 return mHeaderFields.get(normalisedHeaderName(name)); 223 } 224 225 /** 226 * Get header(s) with the name. Multiple values can be returned. 227 * 228 * @param name the name of the header(s) to return. 229 * @return a {@code List} holding the value(s) as String(s). 230 * @since 2.9.0 231 */ getHeaderValues(String name)232 public List<String> getHeaderValues(String name) { 233 List<String> values = mHeaderFields.get(normalisedHeaderName(name)); 234 return values == null ? Collections.emptyList() : Collections.unmodifiableList(values); 235 } 236 getHeaders()237 public List<HttpHeaderField> getHeaders() { 238 List<HttpHeaderField> headerFields = new ArrayList<>(); 239 String[] headers = mMsgHeader.split(Pattern.quote(mLineDelimiter)); 240 241 for (int i = 0; i < headers.length; ++i) { 242 String[] headerField = headers[i].split(":", 2); 243 if (headerField.length == 2) { 244 headerFields.add(new HttpHeaderField(headerField[0].trim(), headerField[1].trim())); 245 } 246 } 247 return headerFields; 248 } 249 250 /** 251 * Add a header with the name and value given. It will be appended to the header string. 252 * 253 * @param name 254 * @param val 255 */ addHeader(String name, String val)256 public void addHeader(String name, String val) { 257 mMsgHeader = mMsgHeader + name + ": " + val + mLineDelimiter; 258 addInternalHeaderFields(name, val); 259 } 260 261 /** 262 * Set a header name and value. If the name is not found, it will be added. If the value is 263 * null, the header will be removed. 264 * 265 * @param name 266 * @param value 267 */ setHeader(String name, String value)268 public void setHeader(String name, String value) { 269 // int pos = 0; 270 // int crlfpos = 0; 271 Pattern pattern = null; 272 273 if (getHeaderValues(name).isEmpty() && value != null) { 274 // header value not found, append to end 275 addHeader(name, value); 276 } else { 277 pattern = getHeaderRegex(name); 278 Matcher matcher = pattern.matcher(mMsgHeader); 279 if (value == null) { 280 // delete header 281 mMsgHeader = matcher.replaceAll(""); 282 } else { 283 // replace header 284 String newString = name + ": " + value + mLineDelimiter; 285 mMsgHeader = matcher.replaceAll(Matcher.quoteReplacement(newString)); 286 } 287 288 // set into hashtable 289 replaceInternalHeaderFields(name, value); 290 } 291 } 292 getHeaderRegex(String name)293 private Pattern getHeaderRegex(String name) throws PatternSyntaxException { 294 // Added character quoting to avoid troubles with "-" char or similar 295 return Pattern.compile( 296 "^ *\\Q" + name + "\\E *: *[^\\r\\n]*" + mLineDelimiter, 297 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); 298 } 299 300 /** 301 * Return the HTTP version (e.g. HTTP/1.0, HTTP/1.1) 302 * 303 * @return 304 */ getVersion()305 public String getVersion() { 306 return mVersion; 307 } 308 309 /** 310 * Set the HTTP version of this header. 311 * 312 * @param version 313 */ setVersion(String version)314 public abstract void setVersion(String version); 315 316 /** 317 * Get the content length of this header. 318 * 319 * @return content length. -1 means content length not set. 320 */ getContentLength()321 public int getContentLength() { 322 return mContentLength; 323 } 324 325 /** 326 * Set the content length of this header. 327 * 328 * @param len 329 */ setContentLength(int len)330 public void setContentLength(int len) { 331 if (mContentLength != len) { 332 setHeader(CONTENT_LENGTH, Integer.toString(len)); 333 mContentLength = len; 334 } 335 } 336 337 /** 338 * Check if this header expect connection to be closed. HTTP/1.0 default to close. HTTP/1.1 339 * default to keep-alive. 340 * 341 * @return 342 */ isConnectionClose()343 public boolean isConnectionClose() { 344 boolean result = true; 345 if (mMalformedHeader) { 346 return true; 347 } 348 349 if (isHttp10()) { 350 // HTTP 1.0 default to close unless keep alive. 351 result = true; 352 try { 353 if (getHeader(CONNECTION).equalsIgnoreCase(_KEEP_ALIVE) 354 || getHeader(PROXY_CONNECTION).equalsIgnoreCase(_KEEP_ALIVE)) { 355 return false; 356 } 357 } catch (NullPointerException e) { 358 } 359 360 } else if (isHttp11()) { 361 // HTTP 1.1 default to keep alive unless close. 362 result = false; 363 try { 364 if (getHeader(CONNECTION).equalsIgnoreCase(_CLOSE)) { 365 return true; 366 } else if (getHeader(PROXY_CONNECTION).equalsIgnoreCase(_CLOSE)) { 367 return true; 368 } 369 } catch (NullPointerException e) { 370 } 371 } 372 return result; 373 } 374 375 /** 376 * Check if this header is HTTP 1.0. 377 * 378 * @return true if HTTP 1.0. 379 */ isHttp10()380 public boolean isHttp10() { 381 if (mVersion.equalsIgnoreCase(HTTP10)) { 382 return true; 383 } 384 return false; 385 } 386 387 /** 388 * Check if this header is HTTP 1.1. 389 * 390 * @return true if HTTP 1.0. 391 */ isHttp11()392 public boolean isHttp11() { 393 if (mVersion.equalsIgnoreCase(HTTP11)) { 394 return true; 395 } 396 return false; 397 } 398 399 /** 400 * Check if Transfer Encoding Chunked is set in this header. 401 * 402 * @return true if transfer encoding chunked is set. 403 */ isTransferEncodingChunked()404 public boolean isTransferEncodingChunked() { 405 String transferEncoding = getHeader(TRANSFER_ENCODING); 406 if (transferEncoding != null && transferEncoding.equalsIgnoreCase(_CHUNKED)) { 407 return true; 408 } 409 return false; 410 } 411 412 /** 413 * Parse this Http header using the String given. 414 * 415 * @param data String to be parsed to form this header. 416 * @return 417 * @throws Exception 418 */ parse(String data)419 protected boolean parse(String data) throws Exception { 420 if (data == null || data.isEmpty()) { 421 return true; 422 } 423 424 // ZAP: Replace all "\n" with "\r\n" to parse correctly 425 String newData = data.replaceAll("(?<!\r)\n", CRLF); 426 // ZAP: always use CRLF to comply with HTTP specification 427 // even if the data it's not directly used. 428 mLineDelimiter = CRLF; 429 430 String[] split = patternCRLF.split(newData); 431 mStartLine = split[0]; 432 433 String token = null, name = null, value = null; 434 int pos = 0; 435 436 StringBuilder sb = new StringBuilder(2048); 437 for (int i = 1; i < split.length; i++) { 438 token = split[i]; 439 if (token.equals("")) { 440 continue; 441 } 442 443 if ((pos = token.indexOf(":")) < 0) { 444 mMalformedHeader = true; 445 return false; 446 } 447 name = token.substring(0, pos).trim(); 448 value = token.substring(pos + 1).trim(); 449 450 if (name.equalsIgnoreCase(CONTENT_LENGTH)) { 451 try { 452 mContentLength = Integer.parseInt(value); 453 } catch (NumberFormatException nfe) { 454 } 455 } 456 457 /* 458 if (name.equalsIgnoreCase(PROXY_CONNECTION)) { 459 sb.append(name + ": " + _CLOSE + mLineDelimiter); 460 } else if (name.equalsIgnoreCase(CONNECTION)) { 461 sb.append(name + ": " + _CLOSE + mLineDelimiter); 462 } else { 463 */ 464 sb.append(name + ": " + value + mLineDelimiter); 465 // } 466 467 addInternalHeaderFields(name, value); 468 } 469 470 mMsgHeader = sb.toString(); 471 return true; 472 } 473 474 /** 475 * Replace the header stored in internal hashtable 476 * 477 * @param name 478 * @param value 479 */ replaceInternalHeaderFields(String name, String value)480 private void replaceInternalHeaderFields(String name, String value) { 481 String key = normalisedHeaderName(name); 482 Vector<String> v = getHeaders(key); 483 if (v == null) { 484 v = new Vector<>(); 485 mHeaderFields.put(key, v); 486 } 487 488 if (value != null) { 489 v.clear(); 490 v.add(value); 491 } else { 492 mHeaderFields.remove(key); 493 } 494 } 495 496 /** 497 * Add the header stored in internal hashtable 498 * 499 * @param name 500 * @param value 501 */ addInternalHeaderFields(String name, String value)502 private void addInternalHeaderFields(String name, String value) { 503 String key = normalisedHeaderName(name); 504 Vector<String> v = getHeaders(key); 505 if (v == null) { 506 v = new Vector<>(); 507 mHeaderFields.put(key, v); 508 } 509 510 if (value != null) { 511 v.add(value); 512 } else { 513 mHeaderFields.remove(key); 514 } 515 } 516 517 /** 518 * Gets the header name normalised, to obtain the value(s) from {@link #mHeaderFields}. 519 * 520 * <p>The normalisation is done by changing all characters to upper case. 521 * 522 * @param name the name of the header to normalise. 523 * @return the normalised header name. 524 */ normalisedHeaderName(String name)525 private static String normalisedHeaderName(String name) { 526 return name.toUpperCase(Locale.ROOT); 527 } 528 529 /** 530 * Get if this is a malformed header. 531 * 532 * @return 533 */ isMalformedHeader()534 public boolean isMalformedHeader() { 535 return mMalformedHeader; 536 } 537 538 /** Get a string representation of this header. */ 539 @Override toString()540 public String toString() { 541 return getPrimeHeader() + mLineDelimiter + mMsgHeader + mLineDelimiter; 542 } 543 544 /** 545 * Get the prime header. 546 * 547 * @return startline for request, statusline for response. 548 */ getPrimeHeader()549 public abstract String getPrimeHeader(); 550 551 /** 552 * Get if this is a image header. 553 * 554 * @return true if image. 555 */ isImage()556 public boolean isImage() { 557 return false; 558 } 559 560 /** 561 * Get if this is a text header. 562 * 563 * @return true if text. 564 */ isText()565 public boolean isText() { 566 return true; 567 } 568 569 /** 570 * Tells whether or not the HTTP header contains any of the given {@code Content-Type} values. 571 * 572 * <p>The values are expected to be in lower case. 573 * 574 * @param contentTypes the values to check. 575 * @return {@code true} if any of the given values is contained in the (first) {@code 576 * Content-Type} header, {@code false} otherwise. 577 * @since 2.8.0 578 * @see #getNormalisedContentTypeValue() 579 */ hasContentType(String... contentTypes)580 public boolean hasContentType(String... contentTypes) { 581 if (contentTypes == null || contentTypes.length == 0) { 582 return true; 583 } 584 585 String normalisedContentType = getNormalisedContentTypeValue(); 586 if (normalisedContentType == null) { 587 return false; 588 } 589 590 for (String contentType : contentTypes) { 591 if (normalisedContentType.contains(contentType)) { 592 return true; 593 } 594 } 595 return false; 596 } 597 598 /** 599 * Gets the normalised value of the (first) {@code Content-Type} header. 600 * 601 * <p>The normalisation is done by changing all characters to lower case. 602 * 603 * @return the value normalised, might be {@code null}. 604 * @since 2.8.0 605 * @see #hasContentType(String...) 606 */ getNormalisedContentTypeValue()607 public String getNormalisedContentTypeValue() { 608 String contentType = getHeader(CONTENT_TYPE); 609 if (contentType != null) { 610 return contentType.toLowerCase(Locale.ROOT); 611 } 612 return null; 613 } 614 615 /** 616 * Get the line delimiter of this header. 617 * 618 * @return 619 */ getLineDelimiter()620 public String getLineDelimiter() { 621 return mLineDelimiter; 622 } 623 624 /** 625 * Get the headers as string. All the headers name value pair is concatenated and delimited. 626 * 627 * @return Eg "Host: www.example.com\r\nUser-agent: some agent\r\n" 628 */ getHeadersAsString()629 public String getHeadersAsString() { 630 return mMsgHeader; 631 } 632 633 /** 634 * Tells whether or not the header is empty. 635 * 636 * <p>A header is empty if it has no content (for example, no start line nor headers). 637 * 638 * @return {@code true} if the header is empty, {@code false} otherwise. 639 */ isEmpty()640 public boolean isEmpty() { 641 if (mStartLine == null || mStartLine.isEmpty()) { 642 return true; 643 } 644 645 return false; 646 } 647 getCharset()648 public String getCharset() { 649 String contentType = getHeader(CONTENT_TYPE); 650 if (contentType == null) { 651 return null; 652 } 653 654 Matcher matcher = patternCharset.matcher(contentType); 655 if (matcher.find()) { 656 String charset = matcher.group(2); 657 if (charset == null) { 658 return matcher.group(1); 659 } 660 return charset; 661 } 662 return null; 663 } 664 } 665