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