1
2
3
4
5
6
7
8
9
10
11
12
13
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
64
65
66
67
68
69
70
71 public class HttpWebConnectionTest extends WebServerTestCase {
72
73
74
75
76
77
78 public static void assertEquals(final byte[] expected, final byte[] actual) {
79 assertEquals(null, expected, actual, expected.length);
80 }
81
82
83
84
85
86
87
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
107
108
109
110
111 public static void assertEquals(final InputStream expected, final InputStream actual) throws IOException {
112 assertEquals(null, expected, actual);
113 }
114
115
116
117
118
119
120
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
172
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
189
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
215
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
232
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
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
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
289
290 public static class EmptyPutServlet extends ServletContentWrapper {
291
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
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
341
342 public static class Cookie1Servlet extends HttpServlet {
343
344
345
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
358
359 public static class Cookie2Servlet extends HttpServlet {
360
361
362
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
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
404
405 public static class RemotePortServlet extends HttpServlet {
406
407
408
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
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
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
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
495
496
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
516
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
553
554
555
556
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
594
595
596
597
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
643
644 public static class BigContentServlet extends HttpServlet {
645
646
647 public static int SENT_;
648
649 public static boolean CANCEL_;
650
651
652
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 }