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 org.junit.jupiter.api.Assertions.fail;
18  
19  import java.io.BufferedInputStream;
20  import java.io.ByteArrayInputStream;
21  import java.io.ByteArrayOutputStream;
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.io.Writer;
27  import java.lang.reflect.Field;
28  import java.lang.reflect.Method;
29  import java.net.URL;
30  import java.util.ArrayList;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Map;
34  
35  import org.apache.commons.io.IOUtils;
36  import org.apache.commons.lang3.StringUtils;
37  import org.apache.http.HttpEntity;
38  import org.apache.http.HttpResponse;
39  import org.apache.http.ProtocolVersion;
40  import org.apache.http.StatusLine;
41  import org.apache.http.client.methods.HttpUriRequest;
42  import org.apache.http.entity.StringEntity;
43  import org.apache.http.entity.mime.MultipartEntityBuilder;
44  import org.apache.http.impl.client.HttpClientBuilder;
45  import org.apache.http.message.BasicHttpResponse;
46  import org.apache.http.message.BasicStatusLine;
47  import org.htmlunit.html.HtmlPage;
48  import org.htmlunit.http.HttpStatus;
49  import org.htmlunit.util.KeyDataPair;
50  import org.htmlunit.util.MimeType;
51  import org.htmlunit.util.NameValuePair;
52  import org.htmlunit.util.PrimitiveWebServer;
53  import org.htmlunit.util.ServletContentWrapper;
54  import org.junit.jupiter.api.Test;
55  
56  import jakarta.servlet.Servlet;
57  import jakarta.servlet.ServletException;
58  import jakarta.servlet.http.HttpServlet;
59  import jakarta.servlet.http.HttpServletRequest;
60  import jakarta.servlet.http.HttpServletResponse;
61  
62  /**
63   * Tests methods in {@link HttpWebConnection}.
64   *
65   * @author David D. Kilzer
66   * @author Marc Guillemot
67   * @author Ahmed Ashour
68   * @author Ronald Brill
69   * @author Carsten Steul
70   */
71  public class HttpWebConnectionTest extends WebServerTestCase {
72  
73      /**
74       * Assert that the two byte arrays are equal.
75       * @param expected the expected value
76       * @param actual the actual value
77       */
78      public static void assertEquals(final byte[] expected, final byte[] actual) {
79          assertEquals(null, expected, actual, expected.length);
80      }
81  
82      /**
83       * Assert that the two byte arrays are equal.
84       * @param message the message to display on failure
85       * @param expected the expected value
86       * @param actual the actual value
87       * @param length How many characters at the beginning of each byte array will be compared
88       */
89      public static void assertEquals(
90              final String message, final byte[] expected, final byte[] actual, final int length) {
91          if (expected == null && actual == null) {
92              return;
93          }
94          if (expected == null || expected.length < length) {
95              fail(message);
96          }
97          if (actual == null || actual.length < length) {
98              fail(message);
99          }
100         for (int i = 0; i < length; i++) {
101             assertEquals(message, expected[i], actual[i]);
102         }
103     }
104 
105     /**
106      * Assert that the two input streams are the same.
107      * @param expected the expected value
108      * @param actual the actual value
109      * @throws IOException if an IO problem occurs during comparison
110      */
111     public static void assertEquals(final InputStream expected, final InputStream actual) throws IOException {
112         assertEquals(null, expected, actual);
113     }
114 
115     /**
116      * Assert that the two input streams are the same.
117      * @param message the message to display on failure
118      * @param expected the expected value
119      * @param actual the actual value
120      * @throws IOException if an IO problem occurs during comparison
121      */
122     public static void assertEquals(final String message, final InputStream expected,
123             final InputStream actual) throws IOException {
124 
125         if (expected == null && actual == null) {
126             return;
127         }
128 
129         if (expected == null || actual == null) {
130             try {
131                 fail(message);
132             }
133             finally {
134                 try {
135                     if (expected != null) {
136                         expected.close();
137                     }
138                 }
139                 finally {
140                     if (actual != null) {
141                         actual.close();
142                     }
143                 }
144             }
145         }
146 
147         try (InputStream expectedBuf = new BufferedInputStream(expected)) {
148             try (InputStream actualBuf = new BufferedInputStream(actual)) {
149 
150                 final byte[] expectedArray = new byte[2048];
151                 final byte[] actualArray = new byte[2048];
152 
153                 int expectedLength = expectedBuf.read(expectedArray);
154                 while (true) {
155 
156                     final int actualLength = actualBuf.read(actualArray);
157                     assertEquals(message, expectedLength, actualLength);
158 
159                     if (expectedLength == -1) {
160                         break;
161                     }
162 
163                     assertEquals(message, expectedArray, actualArray, expectedLength);
164                     expectedLength = expectedBuf.read(expectedArray);
165                 }
166             }
167         }
168     }
169 
170     /**
171      * Tests Jetty.
172      * @throws Exception on failure
173      */
174     @Test
175     public void jettyProofOfConcept() throws Exception {
176         startWebServer("./");
177 
178         final WebClient client = getWebClient();
179         final Page page = client.getPage(URL_FIRST + "src/test/resources/event_coordinates.html");
180         final WebConnection defaultConnection = client.getWebConnection();
181         assertTrue(
182                 "HttpWebConnection should be the default",
183                 defaultConnection instanceof HttpWebConnection);
184         assertTrue("Response should be valid HTML", page instanceof HtmlPage);
185     }
186 
187     /**
188      * Test for feature request 1438216: HttpWebConnection should allow extension to create the HttpClient.
189      * @throws Exception if the test fails
190      */
191     @Test
192     public void designedForExtension() throws Exception {
193         startWebServer("./");
194 
195         final WebClient webClient = getWebClient();
196         final boolean[] tabCalled = {false};
197         final WebConnection myWebConnection = new HttpWebConnection(webClient) {
198             @Override
199             protected HttpClientBuilder createHttpClientBuilder() {
200                 tabCalled[0] = true;
201 
202                 final HttpClientBuilder builder = HttpClientBuilder.create();
203                 builder.setConnectionManagerShared(true);
204                 return builder;
205             }
206         };
207 
208         webClient.setWebConnection(myWebConnection);
209         webClient.getPage(URL_FIRST + "LICENSE.txt");
210         assertTrue("createHttpClient has not been called", tabCalled[0]);
211     }
212 
213     /**
214      * Test that the HttpClient is reinitialised after being shutdown.
215      * @throws Exception if the test fails
216      */
217     @Test
218     public void reinitialiseAfterClosing() throws Exception {
219         startWebServer("./");
220 
221         final WebClient webClient = getWebClient();
222         try (HttpWebConnection webConnection = new HttpWebConnection(webClient)) {
223             webClient.setWebConnection(webConnection);
224             webClient.getPage(URL_FIRST + "LICENSE.txt");
225             webConnection.close();
226             webClient.getPage(URL_FIRST + "pom.xml");
227         }
228     }
229 
230     /**
231      * Test that the right file part is built for a file that doesn't exist.
232      * @throws Exception if the test fails
233      */
234     @Test
235     public void buildFilePart() throws Exception {
236         final String encoding = "ISO8859-1";
237         final KeyDataPair pair = new KeyDataPair("myFile", new File("this/doesnt_exist.txt"), "something",
238                 MimeType.TEXT_PLAIN, encoding);
239         final MultipartEntityBuilder builder = MultipartEntityBuilder.create().setLaxMode();
240         try (HttpWebConnection webConnection = new HttpWebConnection(getWebClient())) {
241             webConnection.buildFilePart(pair, builder);
242         }
243         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
244         builder.build().writeTo(baos);
245         final String part = baos.toString(encoding);
246 
247         final String expected = "--(.*)\r\n"
248                 + "Content-Disposition: form-data; name=\"myFile\"; filename=\"doesnt_exist.txt\"\r\n"
249                 + "Content-Type: text/plain\r\n"
250                 + "\r\n"
251                 + "\r\n"
252                 + "--\\1--\r\n";
253         assertTrue(part, part.matches(expected));
254     }
255 
256     /**
257      * @throws Exception on failure
258      */
259     @Test
260     public void unicode() throws Exception {
261         startWebServer("./");
262         final WebClient client = getWebClient();
263         client.getPage(URL_FIRST + "src/test/resources/event_coordinates.html?param=\u00F6");
264     }
265 
266     /**
267      * @throws Exception if an error occurs
268      */
269     @Test
270     public void emptyPut() throws Exception {
271         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
272         servlets.put("/test", EmptyPutServlet.class);
273         startWebServer("./", servlets);
274 
275         final String[] expectedAlerts = {"1"};
276         final WebClient client = getWebClient();
277         client.setAjaxController(new NicelyResynchronizingAjaxController());
278         final List<String> collectedAlerts = new ArrayList<>();
279         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
280 
281         assertEquals(0, client.getCookieManager().getCookies().size());
282         client.getPage(URL_FIRST + "test");
283         assertEquals(expectedAlerts, collectedAlerts);
284         assertEquals(1, client.getCookieManager().getCookies().size());
285     }
286 
287     /**
288      * Servlet for {@link #emptyPut()}.
289      */
290     public static class EmptyPutServlet extends ServletContentWrapper {
291         /** Constructor. */
292         public EmptyPutServlet() {
293             super(DOCTYPE_HTML
294                 + "<html>\n"
295                 + "<head>\n"
296                 + "  <script>\n"
297                 + "    function test() {\n"
298                 + "      var xhr = window.ActiveXObject?new ActiveXObject('Microsoft.XMLHTTP'):new XMLHttpRequest();\n"
299                 + "      xhr.open('PUT', '" + URL_FIRST + "test" + "', true);\n"
300                 + "      xhr.send();\n"
301                 + "      alert(1);\n"
302                 + "    }\n"
303                 + "  </script>\n"
304                 + "</head>\n"
305                 + "<body onload='test()'></body>\n"
306                 + "</html>");
307         }
308 
309         @Override
310         protected void doGet(final HttpServletRequest request,
311                 final HttpServletResponse response)
312             throws ServletException, IOException {
313             request.getSession().setAttribute("trigger", "session");
314             super.doGet(request, response);
315         }
316     }
317 
318     /**
319      * @throws Exception if the test fails
320      */
321     @Test
322     public void cookiesEnabledAfterDisable() throws Exception {
323         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
324         servlets.put("/test1", Cookie1Servlet.class);
325         servlets.put("/test2", Cookie2Servlet.class);
326         startWebServer("./", servlets);
327 
328         final WebClient client = getWebClient();
329 
330         client.getCookieManager().setCookiesEnabled(false);
331         HtmlPage page = client.getPage(URL_FIRST + "test1");
332         assertTrue(page.asNormalizedText().contains("No Cookies"));
333 
334         client.getCookieManager().setCookiesEnabled(true);
335         page = client.getPage(URL_FIRST + "test1");
336         assertTrue(page.asNormalizedText().contains("key1=value1"));
337     }
338 
339     /**
340      * Servlet for {@link #cookiesEnabledAfterDisable()}.
341      */
342     public static class Cookie1Servlet extends HttpServlet {
343 
344         /**
345          * {@inheritDoc}
346          */
347         @Override
348         protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
349             response.addCookie(new jakarta.servlet.http.Cookie("key1", "value1"));
350             response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
351             final String location = request.getRequestURL().toString().replace("test1", "test2");
352             response.setHeader("Location", location);
353         }
354     }
355 
356     /**
357      * Servlet for {@link #cookiesEnabledAfterDisable()}.
358      */
359     public static class Cookie2Servlet extends HttpServlet {
360 
361         /**
362          * {@inheritDoc}
363          */
364         @Override
365         protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
366             response.setContentType(MimeType.TEXT_HTML);
367             final Writer writer = response.getWriter();
368             if (request.getCookies() == null || request.getCookies().length == 0) {
369                 writer.write("No Cookies");
370             }
371             else {
372                 for (final jakarta.servlet.http.Cookie c : request.getCookies()) {
373                     writer.write(c.getName() + '=' + c.getValue());
374                 }
375             }
376         }
377     }
378 
379     /**
380      * @throws Exception if the test fails
381      */
382     @Test
383     public void remotePort() throws Exception {
384         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
385         servlets.put("/test", RemotePortServlet.class);
386         startWebServer("./", servlets);
387 
388         final WebClient client = getWebClient();
389 
390         String firstPort = null;
391 
392         for (int i = 0; i < 5; i++) {
393             final HtmlPage page = client.getPage(URL_FIRST + "test");
394             final String port = page.asNormalizedText();
395             if (firstPort == null) {
396                 firstPort = port;
397             }
398             assertEquals(firstPort, port);
399         }
400     }
401 
402     /**
403      * Servlet for {@link #remotePort()}.
404      */
405     public static class RemotePortServlet extends HttpServlet {
406 
407         /**
408          * {@inheritDoc}
409          */
410         @Override
411         protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
412             response.setContentType(MimeType.TEXT_HTML);
413             response.getWriter().write(String.valueOf(request.getRemotePort()));
414         }
415     }
416 
417     /**
418      * @throws Exception if an error occurs
419      */
420     @Test
421     public void contentLengthSmallerThanContent() throws Exception {
422         final String html = DOCTYPE_HTML
423                 + "<html>\n"
424                 + "<body>\n"
425                 + "  <p>visible text</p>\n"
426                 + "  <p>missing text</p>\n"
427                 + "</body>\n"
428                 + "</html>";
429         int contentLenght = html.indexOf("<p>missing text</p>");
430         final String response = "HTTP/1.1 200 OK\r\n"
431                 + "Content-Length: " + contentLenght + "\r\n"
432                 + "Content-Type: text/html\r\n"
433                 + "\r\n"
434                 + html;
435 
436         try (PrimitiveWebServer primitiveWebServer = new PrimitiveWebServer(null, response, null)) {
437             final WebClient client = getWebClient();
438 
439             final HtmlPage page = client.getPage("http://localhost:" + primitiveWebServer.getPort());
440             assertEquals("visible text", page.asNormalizedText());
441         }
442     }
443 
444     /**
445      * @throws Exception if an error occurs
446      */
447     @Test
448     public void contentLengthSmallerThanContentLargeContent() throws Exception {
449         final String html = DOCTYPE_HTML
450                 + "<html>\n"
451                 + "<body>\n"
452                 + "  <p>"
453                 + StringUtils.repeat("HtmlUnit  ", 1024 * 1024)
454                 + "</p>\n"
455                 + "  <p>visible text</p>\n"
456                 + "  <p>missing text</p>\n"
457                 + "</body>\n"
458                 + "</html>";
459         int contentLenght = html.indexOf("<p>missing text</p>");
460         final String response = "HTTP/1.1 200 OK\r\n"
461                 + "Content-Length: " + contentLenght + "\r\n"
462                 + "Content-Type: text/html\r\n"
463                 + "\r\n"
464                 + html;
465 
466         try (PrimitiveWebServer primitiveWebServer = new PrimitiveWebServer(null, response, null)) {
467             final WebClient client = getWebClient();
468 
469             final HtmlPage page = client.getPage("http://localhost:" + primitiveWebServer.getPort());
470             assertTrue(page.asNormalizedText(), page.asNormalizedText().endsWith("visible text"));
471         }
472     }
473 
474     /**
475      * @throws Exception if an error occurs
476      */
477     @Test
478     public void contentLengthLargerThanContent() throws Exception {
479         final String response = "HTTP/1.1 200 OK\r\n"
480                 + "Content-Length: 2000\r\n"
481                 + "Content-Type: text/html\r\n"
482                 + "\r\n"
483                 + DOCTYPE_HTML + "<html><body><p>visible text</p></body></html>";
484 
485         try (PrimitiveWebServer primitiveWebServer = new PrimitiveWebServer(null, response, null)) {
486             final WebClient client = getWebClient();
487 
488             final HtmlPage page = client.getPage("http://localhost:" + primitiveWebServer.getPort());
489             assertEquals("visible text", page.asNormalizedText());
490         }
491     }
492 
493     /**
494      * Test for bug #1861.
495      *
496      * @throws Exception if the test fails
497      */
498     @Test
499     public void userAgent() throws Exception {
500         final WebClient webClient = getWebClient();
501         final HttpWebConnection connection = (HttpWebConnection) webClient.getWebConnection();
502         final HttpClientBuilder builder = connection.getHttpClientBuilder();
503         final String userAgent = get(builder, "userAgent");
504         assertEquals(webClient.getBrowserVersion().getUserAgent(), userAgent);
505     }
506 
507     @SuppressWarnings("unchecked")
508     private static <T> T get(final Object o, final String fieldName) throws Exception {
509         final Field field = o.getClass().getDeclaredField(fieldName);
510         field.setAccessible(true);
511         return (T) field.get(o);
512     }
513 
514     /**
515      * Tests creation of a web response.
516      * @throws Exception if the test fails
517      */
518     @Test
519     public void makeWebResponse() throws Exception {
520         final URL url = new URL("http://htmlunit.sourceforge.net/");
521         final String content = DOCTYPE_HTML + "<html><head></head><body></body></html>";
522         final DownloadedContent downloadedContent = new DownloadedContent.InMemory(content.getBytes());
523         final long loadTime = 500L;
524 
525         final ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 0);
526         final StatusLine statusLine = new BasicStatusLine(protocolVersion, HttpStatus.OK_200, null);
527         final HttpResponse httpResponse = new BasicHttpResponse(statusLine);
528 
529         final HttpEntity responseEntity = new StringEntity(content);
530         httpResponse.setEntity(responseEntity);
531 
532         try (HttpWebConnection connection = new HttpWebConnection(getWebClient())) {
533             final Method method = connection.getClass().getDeclaredMethod("makeWebResponse",
534                     HttpResponse.class, WebRequest.class, DownloadedContent.class, long.class);
535             final WebResponse response = (WebResponse) method.invoke(connection,
536                     httpResponse, new WebRequest(url), downloadedContent, Long.valueOf(loadTime));
537 
538             assertEquals(HttpStatus.OK_200, response.getStatusCode());
539             assertEquals(url, response.getWebRequest().getUrl());
540             assertEquals(loadTime, response.getLoadTime());
541             assertEquals(content, response.getContentAsString());
542             try (InputStream is = response.getContentAsStream()) {
543                 assertEquals(content.getBytes(), IOUtils.toByteArray(is));
544             }
545             try (InputStream is = response.getContentAsStream()) {
546                 assertEquals(new ByteArrayInputStream(content.getBytes()), is);
547             }
548         }
549     }
550 
551     /**
552      * Test for overwriting the
553      * {@link HttpWebConnection#downloadResponse(HttpUriRequest, WebRequest, HttpResponse, long)}
554      * method.
555      *
556      * @throws Exception if the test fails
557      */
558     @Test
559     public void contentBlocking() throws Exception {
560         final byte[] content = new byte[] {77, 44};
561         final List<NameValuePair> headers = new ArrayList<>();
562         headers.add(new NameValuePair("Content-Encoding", "gzip"));
563         headers.add(new NameValuePair(HttpHeader.CONTENT_LENGTH, String.valueOf(content.length)));
564 
565         final MockWebConnection conn = getMockWebConnection();
566         conn.setResponse(URL_FIRST, content, 200, "OK", MimeType.APPLICATION_JSON, headers);
567 
568         startWebServer(conn);
569 
570         final WebClient client = getWebClient();
571         client.setWebConnection(new HttpWebConnection(client) {
572             @Override
573             protected WebResponse downloadResponse(final HttpUriRequest httpMethod,
574                     final WebRequest webRequest, final HttpResponse httpResponse,
575                     final long startTime) {
576 
577                 httpMethod.abort();
578 
579                 final DownloadedContent downloaded = new DownloadedContent.InMemory(null);
580                 final long endTime = System.currentTimeMillis();
581                 final WebResponse response = makeWebResponse(httpResponse, webRequest, downloaded, endTime - startTime);
582                 response.markAsBlocked("test blocking");
583                 return response;
584             }
585         });
586 
587         final UnexpectedPage page = client.getPage(URL_FIRST);
588         assertTrue(page.getWebResponse().wasBlocked());
589         assertEquals("test blocking", page.getWebResponse().getBlockReason());
590     }
591 
592     /**
593      * Test for overwriting the
594      * {@link HttpWebConnection#downloadResponse(HttpUriRequest, WebRequest, HttpResponse, long)}
595      * method.
596      *
597      * @throws Exception if the test fails
598      */
599     @Test
600     public void contentSizeBlocking() throws Exception {
601         stopWebServer();
602 
603         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
604         servlets.put("/big", BigContentServlet.class);
605         startWebServer("./", servlets);
606 
607         BigContentServlet.CANCEL_ = false;
608 
609         final WebClient client = getWebClient();
610         client.setWebConnection(new HttpWebConnection(client) {
611             @Override
612             protected WebResponse downloadResponse(final HttpUriRequest httpMethod,
613                     final WebRequest webRequest, final HttpResponse httpResponse,
614                     final long startTime) throws IOException {
615 
616                 final int contentLength = Integer.parseInt(
617                         httpResponse.getFirstHeader(HttpHeader.CONTENT_LENGTH).getValue());
618 
619                 if (contentLength < 1_000) {
620                     return super.downloadResponse(httpMethod, webRequest, httpResponse, startTime);
621                 }
622 
623                 httpMethod.abort();
624 
625                 final DownloadedContent downloaded = new DownloadedContent.InMemory(null);
626                 final long endTime = System.currentTimeMillis();
627                 final WebResponse response = makeWebResponse(httpResponse, webRequest, downloaded, endTime - startTime);
628                 response.markAsBlocked("blocking " + contentLength);
629                 return response;
630             }
631         });
632 
633         final TextPage page = client.getPage(URL_FIRST + "big");
634         assertTrue(page.getWebResponse().wasBlocked());
635         assertEquals("blocking 10240000", page.getWebResponse().getBlockReason());
636         assertTrue("blocks sent " + BigContentServlet.SENT_, BigContentServlet.SENT_ < 5000);
637 
638         BigContentServlet.CANCEL_ = true;
639     }
640 
641     /**
642      * Servlet for bigContent().
643      */
644     public static class BigContentServlet extends HttpServlet {
645 
646         /** Helper. */
647         public static int SENT_;
648         /** Helper. */
649         public static boolean CANCEL_;
650 
651         /**
652          * {@inheritDoc}
653          */
654         @Override
655         protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
656             final int blockSize = 1024;
657             final int blockCount = 10_000;
658 
659             response.setHeader(HttpHeader.CONTENT_LENGTH, String.valueOf(blockSize * blockCount));
660 
661             final byte[] buffer = new byte[blockSize];
662             try (OutputStream out = response.getOutputStream()) {
663                 for (int i = 0; i < blockCount && !CANCEL_; i++) {
664                     SENT_++;
665                     out.write(buffer);
666                 }
667             }
668         }
669     }
670 }