View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit;
16  
17  import static java.nio.charset.StandardCharsets.ISO_8859_1;
18  
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.Serializable;
22  import java.util.Collections;
23  import java.util.List;
24  import java.util.zip.GZIPInputStream;
25  import java.util.zip.Inflater;
26  import java.util.zip.InflaterInputStream;
27  
28  import org.apache.commons.io.ByteOrderMark;
29  import org.apache.commons.io.IOUtils;
30  import org.apache.commons.io.input.BOMInputStream;
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.htmlunit.util.ArrayUtils;
34  import org.htmlunit.util.MimeType;
35  import org.htmlunit.util.NameValuePair;
36  import org.htmlunit.util.StringUtils;
37  import org.htmlunit.util.brotli.BrotliInputStream;
38  
39  /**
40   * Simple data object to simplify WebResponse creation.
41   *
42   * @author Brad Clarke
43   * @author Daniel Gredler
44   * @author Ahmed Ashour
45   * @author Ronald Brill
46   * @author Sven Strickroth
47   */
48  public class WebResponseData implements Serializable {
49      private static final Log LOG = LogFactory.getLog(WebResponseData.class);
50  
51      private static final String CONTENT_ENCODING_ERROR_HTML = """
52              <!DOCTYPE html><html>
53              <head><title>Problem loading page</title></head>
54              <body>
55              <h1>Content Encoding Error</h1>
56              <p>The page you are trying to view cannot be shown because\
57               it uses an invalid or unsupported form of compression.</p>
58              </body>
59              </html>""";
60  
61      private final int statusCode_;
62      private final String statusMessage_;
63      private final List<NameValuePair> responseHeaders_;
64      private final DownloadedContent downloadedContent_;
65  
66      /**
67       * Constructs with a raw byte[] (mostly for testing).
68       *
69       * @param body              Body of this response
70       * @param statusCode        Status code from the server
71       * @param statusMessage     Status message from the server
72       * @param responseHeaders   Headers in this response
73       */
74      public WebResponseData(final byte[] body, final int statusCode, final String statusMessage,
75              final List<NameValuePair> responseHeaders) {
76          this(new DownloadedContent.InMemory(body), statusCode, statusMessage, responseHeaders);
77      }
78  
79      /**
80       * Constructs without data stream for subclasses that override getBody().
81       *
82       * @param statusCode        Status code from the server
83       * @param statusMessage     Status message from the server
84       * @param responseHeaders   Headers in this response
85       */
86      protected WebResponseData(final int statusCode,
87              final String statusMessage, final List<NameValuePair> responseHeaders) {
88          this(ArrayUtils.EMPTY_BYTE_ARRAY, statusCode, statusMessage, responseHeaders);
89      }
90  
91      /**
92       * Constructor.
93       * @param downloadedContent the downloaded content
94       * @param statusCode        Status code from the server
95       * @param statusMessage     Status message from the server
96       * @param responseHeaders   Headers in this response
97       */
98      public WebResponseData(final DownloadedContent downloadedContent, final int statusCode, final String statusMessage,
99              final List<NameValuePair> responseHeaders) {
100         statusCode_ = statusCode;
101         statusMessage_ = statusMessage;
102         responseHeaders_ = Collections.unmodifiableList(responseHeaders);
103         downloadedContent_ = downloadedContent;
104     }
105 
106     private InputStream getStream(final ByteOrderMark... bomHeaders) throws IOException {
107         InputStream stream = downloadedContent_.getInputStream();
108         if (downloadedContent_.isEmpty()) {
109             return stream;
110         }
111 
112         final List<NameValuePair> headers = getResponseHeaders();
113         final String encoding = getHeader(headers, "content-encoding");
114         if (encoding != null) {
115             boolean isGzip = StringUtils.containsIgnoreCase(encoding, "gzip") && !"no-gzip".equals(encoding);
116             if ("gzip-only-text/html".equals(encoding)) {
117                 isGzip = MimeType.TEXT_HTML.equals(getHeader(headers, "content-type"));
118             }
119 
120             if (isGzip) {
121                 try {
122                     stream = new GZIPInputStream(stream);
123                 }
124                 catch (final IOException e) {
125                     LOG.error("Reading gzip encoded content failed.", e);
126                     stream.close();
127                     stream = IOUtils.toInputStream(CONTENT_ENCODING_ERROR_HTML, ISO_8859_1);
128                 }
129             }
130             else if ("br".equals(encoding)) {
131                 try {
132                     stream = new BrotliInputStream(stream);
133                 }
134                 catch (final IOException e) {
135                     LOG.error("Reading Brotli encoded content failed.", e);
136                     stream.close();
137                     stream = IOUtils.toInputStream(CONTENT_ENCODING_ERROR_HTML, ISO_8859_1);
138                 }
139             }
140             else if (StringUtils.containsIgnoreCase(encoding, "deflate")) {
141                 boolean zlibHeader = false;
142                 if (stream.markSupported()) {
143                     stream.mark(2);
144                     final byte[] buffer = new byte[2];
145                     final int byteCount = IOUtils.read(stream, buffer, 0, 2);
146                     if (byteCount == 2) {
147                         final int header = ((buffer[0] & 0xff) << 8) | (buffer[1] & 0xff);
148                         zlibHeader = (header & 0x7800) == 0x7800 && (header % 31 == 0);
149                     }
150                     stream.reset();
151                 }
152                 stream = zlibHeader ? new InflaterInputStream(stream)
153                                     : new InflaterInputStream(stream, new Inflater(true));
154             }
155         }
156 
157         if (stream != null && bomHeaders != null && bomHeaders.length > 0) {
158             stream = BOMInputStream.builder().setInputStream(stream).setByteOrderMarks(bomHeaders).get();
159         }
160         return stream;
161     }
162 
163     private static String getHeader(final List<NameValuePair> headers, final String name) {
164         for (final NameValuePair header : headers) {
165             final String headerName = header.getName().trim();
166             if (name.equalsIgnoreCase(headerName)) {
167                 return header.getValue();
168             }
169         }
170 
171         return null;
172     }
173 
174     /**
175      * Returns the response body.
176      * This may cause memory problem for very large responses.
177      * @return response body
178      */
179     public byte[] getBody() {
180         try (InputStream is = getInputStream()) {
181             return IOUtils.toByteArray(is);
182         }
183         catch (final IOException e) {
184             throw new RuntimeException(e); // shouldn't we allow the method to throw IOException?
185         }
186     }
187 
188     /**
189      * Returns a new {@link InputStream} allowing to read the downloaded content.
190      * @return the associated InputStream
191      * @throws IOException in case of IO problems
192      */
193     public InputStream getInputStream() throws IOException {
194         return getStream((ByteOrderMark[]) null);
195     }
196 
197     /**
198      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
199      *
200      * @param bomHeaders the supported bomHeaders
201      * @return the associated InputStream wrapped with a bom input stream if applicable
202      * @throws IOException in case of IO problems
203      */
204     public InputStream getInputStreamWithBomIfApplicable(final ByteOrderMark... bomHeaders) throws IOException {
205         return getStream(bomHeaders);
206     }
207 
208     /**
209      * Returns the response headers.
210      *
211      * @return the response headers
212      */
213     public List<NameValuePair> getResponseHeaders() {
214         return responseHeaders_;
215     }
216 
217     /**
218      * Returns the HTTP status code.
219      *
220      * @return the HTTP status code
221      */
222     public int getStatusCode() {
223         return statusCode_;
224     }
225 
226     /**
227      * Returns the HTTP status message.
228      *
229      * @return the HTTP status message
230      */
231     public String getStatusMessage() {
232         return statusMessage_;
233     }
234 
235     /**
236      * Returns length of the content data.
237      * @return the length
238      */
239     public long getContentLength() {
240         return downloadedContent_.length();
241     }
242 
243     /**
244      * Clean up the downloaded content.
245      */
246     public void cleanUp() {
247         downloadedContent_.cleanUp();
248     }
249 }