View Javadoc
1   /*
2    * Copyright (c) 2002-2025 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.Assert.fail;
18  
19  import java.io.IOException;
20  import java.io.Writer;
21  import java.net.SocketTimeoutException;
22  import java.util.HashMap;
23  import java.util.Map;
24  
25  import javax.servlet.Servlet;
26  import javax.servlet.http.HttpServlet;
27  import javax.servlet.http.HttpServletRequest;
28  import javax.servlet.http.HttpServletResponse;
29  
30  import org.htmlunit.html.HtmlPage;
31  import org.htmlunit.junit.BrowserRunner;
32  import org.htmlunit.util.Cookie;
33  import org.htmlunit.util.MimeType;
34  import org.junit.Test;
35  import org.junit.runner.RunWith;
36  
37  /**
38   * Tests for {@link WebClient} that run with BrowserRunner.
39   *
40   * @author Ahmed Ashour
41   * @author Ronald Brill
42   */
43  @RunWith(BrowserRunner.class)
44  public class WebClient4Test extends WebServerTestCase {
45  
46      /**
47       * Verifies that a WebClient can be serialized and deserialized after it has been used.
48       * @throws Exception if an error occurs
49       */
50      @Test
51      public void serialization_afterUse() throws Exception {
52          startWebServer("./");
53  
54          try (WebClient client = getWebClient()) {
55              TextPage textPage = client.getPage(URL_FIRST + "LICENSE.txt");
56              assertTrue(textPage.getContent().contains("Apache License"));
57  
58              try (WebClient copy = clone(client)) {
59                  assertNotNull(copy);
60  
61                  final WebWindow window = copy.getCurrentWindow();
62                  assertNotNull(window);
63  
64                  final WebWindow topWindow = window.getTopWindow();
65                  assertNotNull(topWindow);
66  
67                  final Page page = topWindow.getEnclosedPage();
68                  assertNotNull(page);
69  
70                  final WebResponse response = page.getWebResponse();
71                  assertNotNull(response);
72  
73                  final String content = response.getContentAsString();
74                  assertNotNull(content);
75                  assertTrue(content.contains("Apache License"));
76  
77                  textPage = copy.getPage(URL_FIRST + "LICENSE.txt");
78                  assertTrue(textPage.getContent().contains("Apache License"));
79              }
80          }
81      }
82  
83      /**
84       * Verifies that a redirect limit kicks in even if the redirects aren't for the same URL
85       * and don't use the same redirect HTTP status code (see bug 2915453).
86       * @throws Exception if an error occurs
87       */
88      @Test
89      public void redirectInfinite303And307() throws Exception {
90          final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
91          servlets.put(RedirectServlet307.URL, RedirectServlet307.class);
92          servlets.put(RedirectServlet303.URL, RedirectServlet303.class);
93          startWebServer("./", new String[0], servlets);
94  
95          final WebClient client = getWebClient();
96  
97          try {
98              client.getPage("http://localhost:" + PORT + RedirectServlet307.URL);
99          }
100         catch (final Exception e) {
101             assertTrue(e.getMessage(), e.getMessage().contains("Too much redirect"));
102         }
103     }
104 
105     /**
106      * Helper class for {@link #redirectInfinite303And307}.
107      */
108     public static class RedirectServlet extends HttpServlet {
109         private int count_;
110         private int status_;
111         private String location_;
112 
113         /**
114          * Creates a new instance.
115          * @param status the HTTP status to return
116          * @param location the location to redirect to
117          */
118         public RedirectServlet(final int status, final String location) {
119             count_ = 0;
120             status_ = status;
121             location_ = location;
122         }
123 
124         /**
125          * {@inheritDoc}
126          */
127         @Override
128         protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
129             count_++;
130             resp.setStatus(status_);
131             resp.setHeader("Location", location_);
132             resp.getWriter().write(status_ + " " + req.getContextPath() + " " + count_);
133         }
134     }
135 
136     /**
137      * Helper class for {@link #redirectInfinite303And307}.
138      */
139     public static class RedirectServlet303 extends RedirectServlet {
140         static final String URL = "/test";
141         /** Creates a new instance. */
142         public RedirectServlet303() {
143             super(303, RedirectServlet307.URL);
144         }
145     }
146 
147     /**
148      * Helper class for {@link #redirectInfinite303And307}.
149      */
150     public static class RedirectServlet307 extends RedirectServlet {
151         static final String URL = "/test2";
152         /** Creates a new instance. */
153         public RedirectServlet307() {
154             super(307, RedirectServlet303.URL);
155         }
156     }
157 
158     /**
159      * Regression test for bug 2903223: body download time was not taken into account
160      * in {@link WebResponse#getLoadTime()}.
161      * @throws Exception if an error occurs
162      */
163     @Test
164     public void bodyDowloadTime() throws Exception {
165         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
166         servlets.put("/*", ServeBodySlowlyServlet.class);
167         startWebServer("./", new String[0], servlets);
168 
169         final Page page = getWebClient().getPage(URL_FIRST);
170         final long loadTime = page.getWebResponse().getLoadTime();
171         assertTrue("Load time: " + loadTime + ", last request time: " + ServeBodySlowlyServlet.LastRequestTime_,
172                 loadTime >= ServeBodySlowlyServlet.LastRequestTime_);
173     }
174 
175     /**
176      * Helper class for {@link #bodyDowloadTime}.
177      */
178     public static class ServeBodySlowlyServlet extends HttpServlet {
179         private static volatile long LastRequestTime_ = -1;
180 
181         @Override
182         protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
183             final long before = System.currentTimeMillis();
184             final Writer writer = resp.getWriter();
185             writeSomeContent(writer); // some content quickly
186             writer.flush();
187             try {
188                 Thread.sleep(500);
189             }
190             catch (final InterruptedException e) {
191                 throw new RuntimeException(e);
192             }
193             writeSomeContent(writer); // and some content later
194             LastRequestTime_ = System.currentTimeMillis() - before;
195         }
196 
197         private static void writeSomeContent(final Writer writer) throws IOException {
198             for (int i = 0; i < 1000; i++) {
199                 writer.append((char) ('a' + (i % 26)));
200             }
201         }
202     }
203 
204     /**
205      * @throws Exception if the test fails
206      */
207     @Test
208     public void useProxy() throws Exception {
209         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
210         servlets.put("/test", UseProxyHeaderServlet.class);
211         startWebServer("./", null, servlets);
212 
213         final WebClient client = getWebClient();
214         final HtmlPage page = client.getPage(URL_FIRST + "test");
215         assertEquals("Going anywhere?", page.asNormalizedText());
216     }
217 
218     /**
219      * Servlet for {@link #useProxy()}.
220      */
221     public static class UseProxyHeaderServlet extends HttpServlet {
222         /**
223          * {@inheritDoc}
224          */
225         @Override
226         protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
227             response.setStatus(HttpServletResponse.SC_USE_PROXY);
228             //Won't matter!
229             //response.setHeader("Location", "http://www.google.com");
230             response.setContentType(MimeType.TEXT_HTML);
231             final Writer writer = response.getWriter();
232             writer.write(DOCTYPE_HTML + "<html><body>Going anywhere?</body></html>");
233         }
234     }
235 
236     /**
237      * Regression test for bug 2803378: GET or POST to a URL that returns HTTP 204 (No Content).
238      * @throws Exception if an error occurs
239      */
240     @Test
241     public void noContent() throws Exception {
242         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
243         servlets.put("/test1", NoContentServlet1.class);
244         servlets.put("/test2", NoContentServlet2.class);
245         startWebServer("./", null, servlets);
246         final WebClient client = getWebClient();
247         final HtmlPage page = client.getPage(URL_FIRST + "test1");
248         final HtmlPage page2 = page.getHtmlElementById("submit").click();
249         assertEquals(page, page2);
250     }
251 
252     /**
253      * First servlet for {@link #noContent()}.
254      */
255     public static class NoContentServlet1 extends HttpServlet {
256         /**
257          * {@inheritDoc}
258          */
259         @Override
260         protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
261             res.setContentType(MimeType.TEXT_HTML);
262             final Writer writer = res.getWriter();
263             writer.write(DOCTYPE_HTML
264                     + "<html><body><form action='test2'>\n"
265                     + "<input id='submit' type='submit' value='submit'></input>\n"
266                     + "</form></body></html>");
267         }
268     }
269 
270     /**
271      * Second servlet for {@link #noContent()}.
272      */
273     public static class NoContentServlet2 extends HttpServlet {
274         /**
275          * {@inheritDoc}
276          */
277         @Override
278         protected void doGet(final HttpServletRequest req, final HttpServletResponse res) {
279             res.setStatus(HttpServletResponse.SC_NO_CONTENT);
280         }
281     }
282 
283     /**
284      * Regression test for bug 2821888: HTTP 304 (Not Modified) was being treated as a redirect. Note that a 304
285      * response doesn't really make sense because we're not sending any If-Modified-Since headers, but we want to
286      * at least make sure that we're not throwing exceptions when we receive one of these responses.
287      * @throws Exception if an error occurs
288      */
289     @Test
290     public void notModified() throws Exception {
291         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
292         servlets.put("/test", NotModifiedServlet.class);
293         startWebServer("./", null, servlets);
294         final WebClient client = getWebClient();
295         final HtmlPage page = client.getPage(URL_FIRST + "test");
296         final TextPage page2 = client.getPage(URL_FIRST + "test");
297         assertNotNull(page);
298         assertNotNull(page2);
299     }
300 
301     /**
302      * Servlet for {@link #notModified()}.
303      */
304     public static class NotModifiedServlet extends HttpServlet {
305         private boolean first_ = true;
306 
307         /**
308          * {@inheritDoc}
309          */
310         @Override
311         protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
312             if (first_) {
313                 first_ = false;
314                 res.setContentType(MimeType.TEXT_HTML);
315                 final Writer writer = res.getWriter();
316                 writer.write(DOCTYPE_HTML + "<html><body>foo</body></html>");
317             }
318             else {
319                 res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
320             }
321         }
322     }
323 
324     /**
325      * @throws Exception if an error occurs
326      */
327     @Test
328     public void timeout() throws Exception {
329         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
330         servlets.put("/*", DelayDeliverServlet.class);
331         startWebServer("./", null, servlets);
332 
333         final WebClient client = getWebClient();
334         client.getOptions().setTimeout(500);
335 
336         try {
337             client.getPage(URL_FIRST);
338             fail("timeout expected!");
339         }
340         catch (final SocketTimeoutException e) {
341             // as expected
342         }
343 
344         // now configure higher timeout allowing to get the page
345         client.getOptions().setTimeout(5000);
346         client.getPage(URL_FIRST);
347     }
348 
349     /**
350      * Make sure cookies set for the request are overwriting the cookieManager.
351      *
352      * @throws Exception if something goes wrong
353      */
354     @Test
355     public void requestHeaderCookieFromRequest() throws Exception {
356         final String content = DOCTYPE_HTML + "<html></html>";
357         final MockWebConnection webConnection = new MockWebConnection();
358         webConnection.setDefaultResponse(content);
359 
360         startWebServer(webConnection);
361 
362         final WebClient client = getWebClient();
363 
364         // no cookie sent
365         client.getPage(URL_FIRST);
366         assertNull(webConnection.getLastAdditionalHeaders().get(HttpHeader.COOKIE));
367 
368         // process web request with cookie
369         WebRequest wr = new WebRequest(URL_FIRST, HttpMethod.GET);
370         wr.setAdditionalHeader(HttpHeader.COOKIE, "yummy_cookie=choco");
371         client.getPage(wr);
372         assertEquals("yummy_cookie=choco", webConnection.getLastAdditionalHeaders().get(HttpHeader.COOKIE));
373 
374         // add cookie to the cookie manager and test if sent
375         final CookieManager mgr = client.getCookieManager();
376         mgr.addCookie(new Cookie(URL_FIRST.getHost(), "my_key", "my_value", "/", null, false));
377         wr = new WebRequest(URL_FIRST, HttpMethod.GET);
378         client.getPage(wr);
379         assertEquals("my_key=my_value", webConnection.getLastAdditionalHeaders().get(HttpHeader.COOKIE));
380 
381         // request page again, now the the request provides his own cookies
382         wr = new WebRequest(URL_FIRST, HttpMethod.GET);
383         wr.setAdditionalHeader(HttpHeader.COOKIE, "yummy_cookie=choco");
384         client.getPage(wr);
385         assertEquals("yummy_cookie=choco", webConnection.getLastAdditionalHeaders().get(HttpHeader.COOKIE));
386 
387         // request page again, now the the request provides his own cookies as part of a map
388         wr = new WebRequest(URL_FIRST, HttpMethod.GET);
389         final Map<String, String> headers = new HashMap<>();
390         headers.put("accept-language", "es-ES,es;q=0.9");
391         headers.put(HttpHeader.COOKIE, "tasty_cookie=strawberry");
392         wr.setAdditionalHeaders(headers);
393         client.getPage(wr);
394         assertEquals("tasty_cookie=strawberry", webConnection.getLastAdditionalHeaders().get(HttpHeader.COOKIE));
395 
396         // and finally to make sure the cookies from the store are still there
397         wr = new WebRequest(URL_FIRST, HttpMethod.GET);
398         client.getPage(wr);
399         assertEquals("my_key=my_value", webConnection.getLastAdditionalHeaders().get(HttpHeader.COOKIE));
400     }
401 
402     /**
403      * Servlet for {@link #timeout()}.
404      */
405     public static class DelayDeliverServlet extends HttpServlet {
406         /**
407          * {@inheritDoc}
408          */
409         @Override
410         protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
411             try {
412                 Thread.sleep(1000);
413             }
414             catch (final InterruptedException e) {
415                 throw new RuntimeException(e);
416             }
417             res.setContentType(MimeType.TEXT_HTML);
418             final Writer writer = res.getWriter();
419             writer.write(DOCTYPE_HTML + "<html><head><title>hello</title></head><body>foo</body></html>");
420         }
421     }
422 
423     /**
424      * @throws Exception if an error occurs
425      */
426     @Test
427     public void redirectInfiniteMeta() throws Exception {
428         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
429         servlets.put("/test1", RedirectMetaServlet1.class);
430         servlets.put("/test2", RedirectMetaServlet2.class);
431         startWebServer("./", new String[0], servlets);
432 
433         final WebClient client = getWebClient();
434 
435         try {
436             client.getPage(URL_FIRST + "test1");
437         }
438         catch (final Exception e) {
439             assertTrue(e.getMessage(), e.getMessage().contains("Too much redirect"));
440         }
441     }
442 
443     /**
444      * Servlet for {@link #redirectInfiniteMeta()}.
445      */
446     public static class RedirectMetaServlet1 extends HttpServlet {
447         /**
448          * {@inheritDoc}
449          */
450         @Override
451         protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
452             res.setContentType(MimeType.TEXT_HTML);
453             final Writer writer = res.getWriter();
454             writer.write(DOCTYPE_HTML
455                     + "<html><head>\n"
456                     + "  <meta http-equiv='refresh' content='0;URL=test2'>\n"
457                     + "</head><body>foo</body></html>");
458         }
459     }
460 
461     /**
462      * Servlet for {@link #redirectInfiniteMeta()}.
463      */
464     public static class RedirectMetaServlet2 extends HttpServlet {
465         /**
466          * {@inheritDoc}
467          */
468         @Override
469         protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
470             res.setContentType(MimeType.TEXT_HTML);
471             final Writer writer = res.getWriter();
472             writer.write(DOCTYPE_HTML
473                     + "<html><head>\n"
474                     + "  <meta http-equiv='refresh' content='0;URL=test1'>\n"
475                     + "</head><body>foo</body></html>");
476         }
477     }
478 }