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