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 java.io.IOException;
18  import java.io.ObjectInputStream;
19  import java.io.ObjectOutputStream;
20  import java.io.Serializable;
21  import java.net.IDN;
22  import java.net.MalformedURLException;
23  import java.net.URL;
24  import java.nio.charset.Charset;
25  import java.nio.charset.StandardCharsets;
26  import java.util.ArrayList;
27  import java.util.Collections;
28  import java.util.EnumSet;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Set;
33  import java.util.regex.Pattern;
34  
35  import org.apache.http.auth.Credentials;
36  import org.htmlunit.http.HttpUtils;
37  import org.htmlunit.httpclient.HtmlUnitUsernamePasswordCredentials;
38  import org.htmlunit.util.NameValuePair;
39  import org.htmlunit.util.UrlUtils;
40  
41  /**
42   * Parameter object for making web requests.
43   *
44   * @author Brad Clarke
45   * @author Hans Donner
46   * @author Ahmed Ashour
47   * @author Marc Guillemot
48   * @author Rodney Gitzel
49   * @author Ronald Brill
50   * @author Adam Afeltowicz
51   * @author Joerg Werner
52   * @author Michael Lueck
53   * @author Lai Quang Duong
54   * @author Kristof Neirynck
55   */
56  @SuppressWarnings("PMD.TooManyFields")
57  public class WebRequest implements Serializable {
58  
59      /**
60       * Enum to configure request creation.
61       */
62      public enum HttpHint {
63          /** Force to include the charset. */
64          IncludeCharsetInContentTypeHeader,
65  
66          /** Disable sending of stored cookies and receiving of new cookies. */
67          BlockCookies
68      }
69  
70      /**
71       * The destination of a request, as defined by the Fetch spec
72       * (<a href="https://fetch.spec.whatwg.org/#concept-request-destination">
73       * https://fetch.spec.whatwg.org/#concept-request-destination</a>).
74       * <p>
75       * This is used to compute the value of the {@code Sec-Fetch-Dest} request
76       * header (see <a href="https://www.w3.org/TR/fetch-metadata/">
77       * https://www.w3.org/TR/fetch-metadata/</a>) and also determines the default
78       * {@link FetchMode} to be used, unless explicitly overridden via
79       * {@link #setFetchModeOverride(FetchMode)}.
80       * </p>
81       */
82      public enum FetchDestination {
83          /** A top-level document, e.g. loaded by the address bar, a link, or a form submission. */
84          DOCUMENT("document"),
85          /** A nested browsing context of type {@code <iframe>}. */
86          IFRAME("iframe"),
87          /** A nested browsing context of type {@code <frame>}. */
88          FRAME("frame"),
89          /** An {@code <object>} element. */
90          OBJECT("object"),
91          /** An {@code <embed>} element. */
92          EMBED("embed"),
93          /** An image, e.g. {@code <img>}, a CSS background-image, or a favicon. */
94          IMAGE("image"),
95          /** A classic or module script, e.g. {@code <script src>}. */
96          SCRIPT("script"),
97          /** A stylesheet, e.g. {@code <link rel=stylesheet>} or a CSS {@code @import}. */
98          STYLE("style"),
99          /** A font resource, e.g. loaded via {@code @font-face}. */
100         FONT("font"),
101         /** An {@code <audio>} resource. */
102         AUDIO("audio"),
103         /** A {@code <video>} resource. */
104         VIDEO("video"),
105         /** A {@code <track>} resource. */
106         TRACK("track"),
107         /** A dedicated worker script. */
108         WORKER("worker"),
109         /** A shared worker script. */
110         SHARED_WORKER("sharedworker"),
111         /** A service worker script. */
112         SERVICE_WORKER("serviceworker"),
113         /** A web app manifest, e.g. {@code <link rel=manifest>}. */
114         MANIFEST("manifest"),
115         /** A reporting endpoint request. */
116         REPORT("report"),
117         /** A WebSocket handshake request. */
118         WEBSOCKET("websocket"),
119         /** No specific destination, e.g. {@code XMLHttpRequest} or {@code fetch()}. */
120         EMPTY("empty");
121 
122         private final String value_;
123 
124         FetchDestination(final String value) {
125             value_ = value;
126         }
127 
128         /**
129          * Returns the value to be used for the {@code Sec-Fetch-Dest} header.
130          *
131          * @return the value to be used for the {@code Sec-Fetch-Dest} header
132          */
133         public String getValue() {
134             return value_;
135         }
136     }
137 
138     /**
139      * The mode of a request, as defined by the Fetch spec
140      * (<a href="https://fetch.spec.whatwg.org/#concept-request-mode">
141      * https://fetch.spec.whatwg.org/#concept-request-mode</a>).
142      * <p>
143      * This is used to compute the value of the {@code Sec-Fetch-Mode} request
144      * header. Most {@link FetchDestination}s imply a fixed mode; this only needs
145      * to be set explicitly to override that default - e.g. for a {@code fetch()}
146      * call using an explicit {@code mode} option, or a subresource request using
147      * the {@code crossorigin} attribute.
148      * </p>
149      */
150     public enum FetchMode {
151         /** Same-origin requests, e.g. worker scripts, or {@code fetch()} with {@code mode: 'same-origin'}. */
152         SAME_ORIGIN("same-origin"),
153         /** Subresource requests without CORS, e.g. {@code <img>} without {@code crossorigin}. */
154         NO_CORS("no-cors"),
155         /** CORS-enabled requests, e.g. {@code XMLHttpRequest}, {@code fetch()} default, or module scripts. */
156         CORS("cors"),
157         /** Navigations, e.g. top-level document loads, {@code <iframe>}/{@code <frame>} loads. */
158         NAVIGATE("navigate"),
159         /** WebSocket handshake requests. */
160         WEBSOCKET("websocket");
161 
162         private final String value_;
163 
164         FetchMode(final String value) {
165             value_ = value;
166         }
167 
168         /**
169          * Returns the value to be used for the {@code Sec-Fetch-Mode} header.
170          *
171          * @return the value to be used for the {@code Sec-Fetch-Mode} header
172          */
173         public String getValue() {
174             return value_;
175         }
176     }
177 
178     private static final Pattern DOT_PATTERN = Pattern.compile("/\\./");
179     private static final Pattern DOT_DOT_PATTERN = Pattern.compile("/(?!\\.\\.)[^/]*/\\.\\./");
180     private static final Pattern REMOVE_DOTS_PATTERN = Pattern.compile("^/(\\.\\.?/)*");
181 
182     // String instead of java.net.URL because "about:blank" URLs don't serialize correctly
183     private String url_;
184 
185     private String proxyHost_;
186     private int proxyPort_;
187     private String proxyScheme_;
188     private boolean isSocksProxy_;
189     private HttpMethod httpMethod_ = HttpMethod.GET;
190     private FormEncodingType encodingType_ = FormEncodingType.URL_ENCODED;
191     private Map<String, String> additionalHeaders_ = new HashMap<>();
192     private Credentials urlCredentials_;
193     private Credentials credentials_;
194     private int timeout_;
195     private transient Set<HttpHint> httpHints_;
196 
197     private transient Charset charset_ = StandardCharsets.ISO_8859_1;
198     // https://datatracker.ietf.org/doc/html/rfc6838#section-4.2.1
199     // private transient Charset defaultResponseContentCharset_ = StandardCharsets.UTF_8;
200     private transient Charset defaultResponseContentCharset_ = StandardCharsets.ISO_8859_1;
201 
202     /*
203      * These two are mutually exclusive; additionally, requestBody_ should only be
204      * set for POST requests.
205      */
206     private List<NameValuePair> requestParameters_ = Collections.emptyList();
207     private String requestBody_;
208 
209     // Sec-Fetch-* support; see https://www.w3.org/TR/fetch-metadata/
210     private FetchDestination fetchDestination_ = FetchDestination.EMPTY;
211     private FetchMode fetchModeOverride_;
212     private boolean userActivation_;
213 
214     // String instead of java.net.URL for the same serialization reason as url_ above;
215     // null means "no initiator" (e.g. a browser-chrome-initiated navigation), which
216     // maps to Sec-Fetch-Site: none.
217     private String requestingUrl_;
218 
219     /**
220      * Creates or updates this object..
221      *
222      * Instantiates a {@link WebRequest} for the specified URL.
223      *
224      * @param url                  the target URL
225      * @param acceptHeader         the accept header to use
226      * @param acceptEncodingHeader the accept encoding header to use
227      */
228     public WebRequest(final URL url, final String acceptHeader, final String acceptEncodingHeader) {
229         setUrl(url);
230         if (acceptHeader != null) {
231             setAdditionalHeader(HttpHeader.ACCEPT, acceptHeader);
232         }
233         if (acceptEncodingHeader != null) {
234             setAdditionalHeader(HttpHeader.ACCEPT_ENCODING, acceptEncodingHeader);
235         }
236         timeout_ = -1;
237 
238         // for backward compatibility
239         setFetchDestination(FetchDestination.DOCUMENT);
240         setFetchModeOverride(FetchMode.NAVIGATE);
241         setUserActivation(true);
242     }
243 
244     /**
245      * Creates or updates this object..
246      *
247      * Instantiates a {@link WebRequest} for the specified URL.
248      *
249      * @param url        the target URL
250      * @param charset    the charset to use
251      * @param refererUrl the url be used by the referer header
252      */
253     public WebRequest(final URL url, final Charset charset, final URL refererUrl) {
254         setUrl(url);
255         setCharset(charset);
256         setRefererHeader(refererUrl);
257     }
258 
259     /**
260      * Returns a new request for about:blank.
261      *
262      * @return a new request for about:blank
263      */
264     public static WebRequest newAboutBlankRequest() {
265         return new WebRequest(UrlUtils.URL_ABOUT_BLANK, "*/*", "gzip, deflate");
266     }
267 
268     /**
269      * Creates or updates this object..
270      *
271      * Instantiates a {@link WebRequest} for the specified URL.
272      *
273      * @param url the target URL
274      */
275     public WebRequest(final URL url) {
276         this(url, "*/*", "gzip, deflate");
277     }
278 
279     /**
280      * Creates or updates this object..
281      *
282      * Instantiates a {@link WebRequest} for the specified URL using the specified
283      * HTTP submit method.
284      *
285      * @param url          the target URL
286      * @param submitMethod the HTTP submit method to use
287      */
288     public WebRequest(final URL url, final HttpMethod submitMethod) {
289         this(url);
290         setHttpMethod(submitMethod);
291     }
292 
293     /**
294      * Returns the target URL.
295      *
296      * Returns the target URL.
297      *
298      * @return the target URL
299      */
300     public URL getUrl() {
301         return UrlUtils.toUrlSafe(url_);
302     }
303 
304     /**
305      * Creates or updates this object.
306      *
307      * Sets the target URL. The URL may be simplified if needed (for instance
308      * eliminating irrelevant path portions like "/./").
309      *
310      * @param url the target URL
311      */
312     public void setUrl(URL url) {
313         if (url == null) {
314             url_ = null;
315             return;
316         }
317 
318         final String path = url.getPath();
319         if (path.isEmpty()) {
320             if (!url.getFile().isEmpty() || url.getProtocol().startsWith("http")) {
321                 url = buildUrlWithNewPath(url, "/");
322             }
323         }
324         else if (path.contains("/.")) {
325             url = buildUrlWithNewPath(url, removeDots(path));
326         }
327 
328         try {
329             final String idn = IDN.toASCII(url.getHost());
330             if (!idn.equals(url.getHost())) {
331                 url = UrlUtils.getUrlWithNewHost(url, idn);
332             }
333         }
334         catch (final Exception e) {
335             throw new IllegalArgumentException(
336                     "Cannot convert the hostname of URL: '" + url.toExternalForm() + "' to ASCII.", e);
337         }
338 
339         try {
340             url_ = UrlUtils.removeRedundantPort(url).toExternalForm();
341         }
342         catch (final MalformedURLException e) {
343             throw new RuntimeException("Cannot strip default port of URL: " + url.toExternalForm(), e);
344         }
345 
346         // http://john.smith:secret@localhost
347         final String userInfo = url.getUserInfo();
348         if (userInfo != null) {
349             final int splitPos = userInfo.indexOf(':');
350             if (splitPos == -1) {
351                 urlCredentials_ = new HtmlUnitUsernamePasswordCredentials(userInfo, new char[0]);
352             }
353             else {
354                 final String username = userInfo.substring(0, splitPos);
355                 final String password = userInfo.substring(splitPos + 1);
356                 urlCredentials_ = new HtmlUnitUsernamePasswordCredentials(username, password.toCharArray());
357             }
358         }
359     }
360 
361     /*
362      * Strip a URL string of "/./" and "/../" occurrences. <p> One trick here is to
363      * repeatedly create new matchers on a given pattern, so that we can see whether
364      * it needs to be re-applied; unfortunately .replaceAll() doesn't re-process its
365      * own output, so if we create a new match with a replacement, it is missed.
366      */
367     private static String removeDots(final String path) {
368         String newPath = path;
369 
370         // remove occurrences at the beginning
371         newPath = REMOVE_DOTS_PATTERN.matcher(newPath).replaceAll("/");
372         if ("/..".equals(newPath)) {
373             newPath = "/";
374         }
375 
376         // single dots have no effect, so just remove them
377         while (DOT_PATTERN.matcher(newPath).find()) {
378             newPath = DOT_PATTERN.matcher(newPath).replaceAll("/");
379         }
380 
381         // mid-path double dots should be removed WITH the previous subdirectory and replaced
382         //  with "/" BUT ONLY IF that subdirectory's not also ".." (a regex lookahead helps with this)
383         while (DOT_DOT_PATTERN.matcher(newPath).find()) {
384             newPath = DOT_DOT_PATTERN.matcher(newPath).replaceAll("/");
385         }
386 
387         return newPath;
388     }
389 
390     private static URL buildUrlWithNewPath(URL url, final String newPath) {
391         try {
392             url = UrlUtils.getUrlWithNewPath(url, newPath);
393         }
394         catch (final Exception e) {
395             throw new RuntimeException("Cannot change path of URL: " + url.toExternalForm(), e);
396         }
397         return url;
398     }
399 
400     /**
401      * Returns the proxy host to use.
402      *
403      * Returns the proxy host to use.
404      *
405      * @return the proxy host to use
406      */
407     public String getProxyHost() {
408         return proxyHost_;
409     }
410 
411     /**
412      * Creates or updates this object.
413      *
414      * Sets the proxy host to use.
415      *
416      * @param proxyHost the proxy host to use
417      */
418     public void setProxyHost(final String proxyHost) {
419         proxyHost_ = proxyHost;
420     }
421 
422     /**
423      * Returns the proxy port to use.
424      *
425      * Returns the proxy port to use.
426      *
427      * @return the proxy port to use
428      */
429     public int getProxyPort() {
430         return proxyPort_;
431     }
432 
433     /**
434      * Creates or updates this object.
435      *
436      * Sets the proxy port to use.
437      *
438      * @param proxyPort the proxy port to use
439      */
440     public void setProxyPort(final int proxyPort) {
441         proxyPort_ = proxyPort;
442     }
443 
444     /**
445      * Returns the proxy scheme to use.
446      *
447      * Returns the proxy scheme to use.
448      *
449      * @return the proxy scheme to use
450      */
451     public String getProxyScheme() {
452         return proxyScheme_;
453     }
454 
455     /**
456      * Creates or updates this object..
457      *
458      * Sets the proxy scheme to use.
459      *
460      * @param proxyScheme the proxy scheme to use
461      *
462      */
463     public void setProxyScheme(final String proxyScheme) {
464         proxyScheme_ = proxyScheme;
465     }
466 
467     /**
468      * Returns whether SOCKS proxy or not.
469      *
470      * Returns whether SOCKS proxy or not.
471      *
472      * @return whether SOCKS proxy or not
473      *
474      */
475     public boolean isSocksProxy() {
476         return isSocksProxy_;
477     }
478 
479     /**
480      * Creates or updates this object..
481      *
482      * Sets whether SOCKS proxy or not.
483      *
484      * @param isSocksProxy whether SOCKS proxy or not
485      *
486      */
487     public void setSocksProxy(final boolean isSocksProxy) {
488         isSocksProxy_ = isSocksProxy;
489     }
490 
491     /**
492      * Returns the timeout to use.
493      *
494      * @return the timeout to use
495      *
496      */
497     public int getTimeout() {
498         return timeout_;
499     }
500 
501     /**
502      * Creates or updates this object..
503      *
504      * Sets the timeout to use.
505      *
506      * @param timeout the timeout to use
507      *
508      */
509     public void setTimeout(final int timeout) {
510         timeout_ = timeout;
511     }
512 
513     /**
514      * Returns the form encoding type to use.
515      *
516      * Returns the form encoding type to use.
517      *
518      * @return the form encoding type to use
519      *
520      */
521     public FormEncodingType getEncodingType() {
522         return encodingType_;
523     }
524 
525     /**
526      * Creates or updates this object..
527      *
528      * Sets the form encoding type to use.
529      *
530      * @param encodingType the form encoding type to use
531      *
532      */
533     public void setEncodingType(final FormEncodingType encodingType) {
534         encodingType_ = encodingType;
535     }
536 
537     /**
538      * Returns the request parameters to use.
539      *
540      * <p>
541      * Retrieves the request parameters used. Similar to the servlet api function
542      * getParameterMap() this works depending on the request type and collects the
543      * url parameters and the body stuff.<br>
544      * The value is also normalized - null is converted to an empty string.</p>
545      * <p>In contrast to the servlet api this creates a separate KeyValuePair for every
546      * parameter. This means that pairs with the same name can be part of the list. The
547      * servlet api will return a string[] as value for the key in this case.<br>
548      * Additionally this method includes also the uploaded files for multipart post
549      * requests.</p>
550      *
551      * @return the request parameters to use
552      */
553     public List<NameValuePair> getParameters() {
554         // developer note:
555         // this has to be in sync with org.htmlunit.HttpWebConnection.makeHttpMethod(WebRequest, HttpClientBuilder)
556 
557         // developer note:
558         // the spring org.springframework.test.web.servlet.htmlunitHtmlUnitRequestBuilder uses
559         // this method and is sensitive to all the details of the current implementation.
560 
561         final List<NameValuePair> allParameters = new ArrayList<>(
562                 HttpUtils.parseUrlQuery(getUrl().getQuery(), getCharset()));
563 
564         // the servlet api ignores these parameters but to make spring happy we include them
565         final HttpMethod httpMethod = getHttpMethod();
566         if (httpMethod == HttpMethod.POST
567             || httpMethod == HttpMethod.PUT
568             || httpMethod == HttpMethod.PATCH
569             || httpMethod == HttpMethod.DELETE
570             || httpMethod == HttpMethod.OPTIONS) {
571             if (FormEncodingType.URL_ENCODED == getEncodingType()
572                 && httpMethod != HttpMethod.OPTIONS) {
573                 // spring ignores URL_ENCODED parameters for OPTIONS requests
574                 // getRequestParameters and getRequestBody are mutually exclusive
575                 if (getRequestBody() == null) {
576                     allParameters.addAll(getRequestParameters());
577                 }
578                 else {
579                     allParameters.addAll(HttpUtils.parseUrlQuery(getRequestBody(), getCharset()));
580                 }
581             }
582             else if (FormEncodingType.MULTIPART == getEncodingType()) {
583                 if (httpMethod == HttpMethod.POST) {
584                     allParameters.addAll(getRequestParameters());
585                 }
586                 else {
587                     // for PUT, PATCH, DELETE and OPTIONS spring moves the parameters up to the query
588                     // it doesn't replace the query
589                     allParameters.addAll(0, getRequestParameters());
590                 }
591             }
592         }
593 
594         return normalize(allParameters);
595     }
596 
597     private static List<NameValuePair> normalize(final List<NameValuePair> pairs) {
598         if (pairs == null || pairs.isEmpty()) {
599             return pairs;
600         }
601 
602         final List<NameValuePair> resultingPairs = new ArrayList<>();
603         for (final NameValuePair pair : pairs) {
604             resultingPairs.add(pair.normalized());
605         }
606 
607         return resultingPairs;
608     }
609 
610     /**
611      * Returns the request parameters to use.
612      *
613      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT
614      * YOUR OWN RISK.</span><br>
615      *
616      * Retrieves the request parameters to use. If set, these request parameters
617      * will overwrite any request parameters which may be present in the
618      * {@link #getUrl() URL}. Should not be used in combination with the
619      * {@link #setRequestBody(String) request body}.
620      *
621      * @return the request parameters to use
622      *
623      */
624     public List<NameValuePair> getRequestParameters() {
625         return requestParameters_;
626     }
627 
628     /**
629      * Creates or updates this object..
630      *
631      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT
632      * YOUR OWN RISK.</span><br>
633      *
634      * Sets the request parameters to use. If set, these request parameters will
635      * overwrite any request parameters which may be present in the {@link #getUrl()
636      * URL}. Should not be used in combination with the
637      * {@link #setRequestBody(String) request body}.
638      *
639      * @param requestParameters the request parameters to use
640      * @throws RuntimeException if the request body has already been set
641      *
642      */
643     public void setRequestParameters(final List<NameValuePair> requestParameters) throws RuntimeException {
644         if (requestBody_ != null) {
645             final String msg = "Trying to set the request parameters, but the request body has already been specified;"
646                     + "the two are mutually exclusive!";
647             throw new RuntimeException(msg);
648         }
649         requestParameters_ = requestParameters;
650     }
651 
652     /**
653      * Returns the body content to be submitted if this is a <code>POST</code>
654      * request.
655      *
656      * Returns the body content to be submitted if this is a <code>POST</code>
657      * request. Ignored for all other request types. Should not be used in
658      * combination with {@link #setRequestParameters(List) request parameters}.
659      *
660      * @return the body content to be submitted if this is a <code>POST</code>
661      *         request
662      *
663      */
664     public String getRequestBody() {
665         return requestBody_;
666     }
667 
668     /**
669      * Creates or updates this object..
670      *
671      * Sets the body content to be submitted if this is a {@code POST}, {@code PUT}
672      * or {@code PATCH} request. Other request types result in
673      * {@link RuntimeException}. Should not be used in combination with
674      * {@link #setRequestParameters(List) request parameters}.
675      *
676      * @param requestBody the body content to be submitted if this is a
677      *                    {@code POST}, {@code PUT} or {@code PATCH} request
678      * @throws RuntimeException if the request parameters have already been set or
679      *                          this is not a {@code POST}, {@code PUT} or
680      *                          {@code PATCH} request.
681      *
682      */
683     public void setRequestBody(final String requestBody) throws RuntimeException {
684         if (requestParameters_ != null && !requestParameters_.isEmpty()) {
685             final String msg = "Trying to set the request body, but the request parameters have already been specified;"
686                        + "the two are mutually exclusive!";
687             throw new RuntimeException(msg);
688         }
689         if (httpMethod_ != HttpMethod.POST
690                 && httpMethod_ != HttpMethod.PUT
691                 && httpMethod_ != HttpMethod.PATCH
692                 && httpMethod_ != HttpMethod.DELETE
693                 && httpMethod_ != HttpMethod.OPTIONS) {
694             final String msg = "The request body may only be set for POST, PUT, PATCH, DELETE or OPTIONS requests!";
695             throw new RuntimeException(msg);
696         }
697         requestBody_ = requestBody;
698     }
699 
700     /**
701      * Returns the HTTP submit method to use.
702      *
703      * Returns the HTTP submit method to use.
704      *
705      * @return the HTTP submit method to use
706      *
707      */
708     public HttpMethod getHttpMethod() {
709         return httpMethod_;
710     }
711 
712     /**
713      * Creates or updates this object..
714      *
715      * Sets the HTTP submit method to use.
716      *
717      * @param submitMethod the HTTP submit method to use
718      *
719      */
720     public void setHttpMethod(final HttpMethod submitMethod) {
721         httpMethod_ = submitMethod;
722     }
723 
724     /**
725      * Returns the additional HTTP headers to use.
726      *
727      * Returns the additional HTTP headers to use.
728      *
729      * @return the additional HTTP headers to use
730      *
731      */
732     public Map<String, String> getAdditionalHeaders() {
733         return additionalHeaders_;
734     }
735 
736     /**
737      * Creates or updates this object..
738      *
739      * Sets the additional HTTP headers to use.
740      *
741      * @param additionalHeaders the additional HTTP headers to use
742      *
743      */
744     public void setAdditionalHeaders(final Map<String, String> additionalHeaders) {
745         additionalHeaders_ = additionalHeaders;
746     }
747 
748     /**
749      * Creates or updates this object..
750      *
751      * Returns whether the specified header name is already included in the
752      * additional HTTP headers.
753      *
754      * @param name the name of the additional HTTP header
755      * @return true if the specified header name is included in the additional HTTP
756      *         headers
757      *
758      */
759     public boolean isAdditionalHeader(final String name) {
760         for (final String key : additionalHeaders_.keySet()) {
761             if (name.equalsIgnoreCase(key)) {
762                 return true;
763             }
764         }
765         return false;
766     }
767 
768     /**
769      * Creates or updates this object..
770      *
771      * Returns the header value associated with this name.
772      *
773      * @param name the name of the additional HTTP header
774      * @return the value or null
775      *
776      */
777     public String getAdditionalHeader(final String name) {
778         String newKey = name;
779         for (final String key : additionalHeaders_.keySet()) {
780             if (name.equalsIgnoreCase(key)) {
781                 newKey = key;
782                 break;
783             }
784         }
785         return additionalHeaders_.get(newKey);
786     }
787 
788     /**
789      * Creates or updates this object..
790      *
791      * Sets the referer HTTP header - only if the provided url is valid.
792      *
793      * @param url the url for the referer HTTP header
794      *
795      */
796     public void setRefererHeader(final URL url) {
797         if (url == null || !url.getProtocol().startsWith("http")) {
798             return;
799         }
800 
801         try {
802             setAdditionalHeader(HttpHeader.REFERER, UrlUtils.getUrlWithoutRef(url).toExternalForm());
803         }
804         catch (final MalformedURLException ignored) {
805             // bad luck us the whole url from the pager
806         }
807     }
808 
809     /**
810      * Returns the destination of this request, used to compute the
811      * {@code Sec-Fetch-Dest} header (and, unless overridden, the default
812      * {@code Sec-Fetch-Mode}). Defaults to {@link FetchDestination#EMPTY},
813      * which is correct for plain {@code XMLHttpRequest}/{@code fetch()} calls.
814      *
815      * @return the destination of this request
816      */
817     public FetchDestination getFetchDestination() {
818         return fetchDestination_;
819     }
820 
821     /**
822      * Sets the destination of this request.
823      *
824      * @param fetchDestination the destination of this request, or {@code null}
825      *                         to reset to {@link FetchDestination#EMPTY}
826      */
827     public void setFetchDestination(final FetchDestination fetchDestination) {
828         fetchDestination_ = fetchDestination == null ? FetchDestination.EMPTY : fetchDestination;
829     }
830 
831     /**
832      * Returns the explicit mode override for this request, if any. When
833      * {@code null} (the default), the mode is derived from the
834      * {@link #getFetchDestination() destination}.
835      *
836      * @return the mode override, or {@code null} if none was set
837      */
838     public FetchMode getFetchModeOverride() {
839         return fetchModeOverride_;
840     }
841 
842     /**
843      * Sets an explicit mode override for this request, e.g. for a {@code fetch()}
844      * call using an explicit {@code mode} option, or a subresource request using
845      * the {@code crossorigin} attribute (which forces CORS mode).
846      *
847      * @param fetchMode the mode to use, or {@code null} to derive it from the
848      *                  {@link #getFetchDestination() destination}
849      */
850     public void setFetchModeOverride(final FetchMode fetchMode) {
851         fetchModeOverride_ = fetchMode;
852     }
853 
854     /**
855      * Returns whether this request is the result of a navigation backed by
856      * genuine user activation (e.g. a click on a link, a typed URL, or a form
857      * submitted via a click on its submit button) as opposed to one triggered
858      * purely by script (e.g. {@code location.href = ...}, a {@code <meta
859      * http-equiv="refresh">}, or an automatically-loaded {@code <iframe>}).
860      * <p>
861      * Only relevant for requests whose {@code Sec-Fetch-Mode} is {@code
862      * navigate}; used to compute the presence of the {@code Sec-Fetch-User}
863      * header, which real browsers omit entirely (never send as {@code ?0})
864      * whenever this is {@code false}.
865      * </p>
866      *
867      * @return whether this request was triggered by a real user gesture
868      */
869     public boolean isUserActivation() {
870         return userActivation_;
871     }
872 
873     /**
874      * Sets whether this request is the result of a navigation backed by genuine
875      * user activation.
876      *
877      * @param userActivation whether this request was triggered by a real user
878      *                       gesture
879      */
880     public void setUserActivation(final boolean userActivation) {
881         userActivation_ = userActivation;
882     }
883 
884     /**
885      * Returns the URL of the document or script that initiated this request, used
886      * to compute the {@code Sec-Fetch-Site} header. {@code null} means there is
887      * no initiator (e.g. a browser-chrome-initiated navigation such as a typed
888      * URL or bookmark), which maps to {@code Sec-Fetch-Site: none}.
889      * <p>
890      * Note this is tracked separately from the {@code Referer} header: unlike
891      * the referrer, it must not be affected by referrer-policy stripping, since
892      * {@code Sec-Fetch-Site} always reflects the true relationship between the
893      * initiator and the target, even when no {@code Referer} header is sent.
894      * </p>
895      *
896      * @return the URL of the initiator, or {@code null} if there is none
897      */
898     public URL getRequestingUrl() {
899         return requestingUrl_ == null ? null : UrlUtils.toUrlSafe(requestingUrl_);
900     }
901 
902     /**
903      * Sets the URL of the document or script that initiated this request.
904      *
905      * @param requestingUrl the URL of the initiator, or {@code null} if there is
906      *                      none
907      */
908     public void setRequestingUrl(final URL requestingUrl) {
909         requestingUrl_ = requestingUrl == null ? null : requestingUrl.toExternalForm();
910     }
911 
912     /**
913      * Convenience method for the common case of a top-level navigation (an
914      * anchor/area click, a form submission, a script-driven location change, ...):
915      * sets {@link FetchDestination#DOCUMENT}, the initiator URL, and whether the
916      * navigation was backed by genuine user activation, all in one call.
917      * <p>
918      * Every navigation-triggering call site needs all three of these set
919      * together for correct {@code Sec-Fetch-*} headers; bundling them here
920      * makes it harder for a call site to set some of them and forget the rest.
921      * </p>
922      * <p>
923      * For navigations whose destination is not {@link FetchDestination#DOCUMENT}
924      * (e.g. an {@code <iframe>}/{@code <frame>} load), use
925      * {@link #markAsNavigation(FetchDestination, URL, boolean)} instead.
926      * </p>
927      *
928      * @param requestingUrl the URL of the page initiating this navigation, or
929      *                      {@code null} if there is none (e.g. a typed URL)
930      * @param userActivation whether this navigation was triggered by a real
931      *                       user gesture as opposed to script
932      */
933     public void markAsNavigation(final URL requestingUrl, final boolean userActivation) {
934         markAsNavigation(FetchDestination.DOCUMENT, requestingUrl, userActivation);
935     }
936 
937     /**
938      * Same as {@link #markAsNavigation(URL, boolean)}, but for navigations whose
939      * destination isn't a top-level {@link FetchDestination#DOCUMENT} - currently
940      * only {@code <iframe>}/{@code <frame>} loads ({@link FetchDestination#IFRAME}/
941      * {@link FetchDestination#FRAME}).
942      *
943      * @param destination the navigation's destination
944      * @param requestingUrl the URL of the page initiating this navigation, or
945      *                      {@code null} if there is none
946      * @param userActivation whether this navigation was triggered by a real
947      *                       user gesture as opposed to script
948      */
949     public void markAsNavigation(final FetchDestination destination, final URL requestingUrl,
950             final boolean userActivation) {
951         setFetchDestination(destination);
952         setRequestingUrl(requestingUrl);
953         setUserActivation(userActivation);
954     }
955 
956     /**
957      * Creates or updates this object..
958      *
959      * Sets the specified name/value pair in the additional HTTP headers.
960      *
961      * @param name  the name of the additional HTTP header
962      * @param value the value of the additional HTTP header
963      *
964      */
965     public void setAdditionalHeader(final String name, final String value) {
966         String newKey = name;
967         for (final String key : additionalHeaders_.keySet()) {
968             if (name.equalsIgnoreCase(key)) {
969                 newKey = key;
970                 break;
971             }
972         }
973         additionalHeaders_.put(newKey, value);
974     }
975 
976     /**
977      * Creates or updates this object..
978      *
979      * Removed the specified name/value pair from the additional HTTP headers.
980      *
981      * @param name the name of the additional HTTP header
982      *
983      */
984     public void removeAdditionalHeader(String name) {
985         for (final String key : additionalHeaders_.keySet()) {
986             if (name.equalsIgnoreCase(key)) {
987                 name = key;
988                 break;
989             }
990         }
991         additionalHeaders_.remove(name);
992     }
993 
994     /**
995      * Returns the credentials if set as part of the url.
996      *
997      * Returns the credentials to use.
998      *
999      * @return the credentials if set as part of the url
1000      *
1001      */
1002     public Credentials getUrlCredentials() {
1003         return urlCredentials_;
1004     }
1005 
1006     /**
1007      * Returns the credentials if set from the external builder.
1008      *
1009      * Returns the credentials to use.
1010      *
1011      * @return the credentials if set from the external builder
1012      *
1013      */
1014     public Credentials getCredentials() {
1015         return credentials_;
1016     }
1017 
1018     /**
1019      * Creates or updates this object..
1020      *
1021      * Sets the credentials to use.
1022      *
1023      * @param credentials the credentials to use
1024      *
1025      */
1026     public void setCredentials(final Credentials credentials) {
1027         credentials_ = credentials;
1028     }
1029 
1030     /**
1031      * Returns the character set to use to perform the request.
1032      *
1033      * Returns the character set to use to perform the request.
1034      *
1035      * @return the character set to use to perform the request
1036      *
1037      */
1038     public Charset getCharset() {
1039         return charset_;
1040     }
1041 
1042     /**
1043      * Creates or updates this object.
1044      *
1045      * Sets the character set to use to perform the request. The default value is
1046      * {@link java.nio.charset.StandardCharsets#ISO_8859_1}.
1047      *
1048      * @param charset the character set to use to perform the request
1049      *
1050      */
1051     public void setCharset(final Charset charset) {
1052         charset_ = charset;
1053     }
1054 
1055     /**
1056      * Returns the default character set to use for the response when it does not
1057      * specify one.
1058      *
1059      * @return the default character set to use for the response when it does not
1060      *         specify one.
1061      *
1062      */
1063     public Charset getDefaultResponseContentCharset() {
1064         return defaultResponseContentCharset_;
1065     }
1066 
1067     /**
1068      * Creates or updates this object.
1069      *
1070      * Sets the default character set to use for the response when it does not
1071      * specify one.
1072      * <p>
1073      * Unless set, the default is {@link java.nio.charset.StandardCharsets#UTF_8}.
1074      * </p>
1075      *
1076      * @param defaultResponseContentCharset the default character set of the
1077      *                                      response
1078      *
1079      */
1080     public void setDefaultResponseContentCharset(final Charset defaultResponseContentCharset) {
1081         WebAssert.notNull("defaultResponseContentCharset", defaultResponseContentCharset);
1082         defaultResponseContentCharset_ = defaultResponseContentCharset;
1083     }
1084 
1085     /**
1086      * Creates or updates this object..
1087      *
1088      * @param hint the hint to check for
1089      * @return true if the hint is enabled
1090      *
1091      */
1092     public boolean hasHint(final HttpHint hint) {
1093         if (httpHints_ == null) {
1094             return false;
1095         }
1096         return httpHints_.contains(hint);
1097     }
1098 
1099     /**
1100      * Creates or updates this object..
1101      *
1102      * Enables the hint.
1103      *
1104      * @param hint the hint to add
1105      *
1106      */
1107     public void addHint(final HttpHint hint) {
1108         if (httpHints_ == null) {
1109             httpHints_ = EnumSet.noneOf(HttpHint.class);
1110         }
1111         httpHints_.add(hint);
1112     }
1113 
1114     /**
1115      * Returns a string representation of this object.
1116      *
1117      * Returns a string representation of this object.
1118      *
1119      * @return a string representation of this object
1120      *
1121      */
1122     @Override
1123     public String toString() {
1124         final StringBuilder builder = new StringBuilder(100)
1125                 .append(getClass().getSimpleName())
1126                 .append("[<url=\"")
1127                 .append(url_)
1128                 .append("\", ").append(httpMethod_)
1129                 .append(", ").append(encodingType_)
1130                 .append(", ").append(requestParameters_)
1131                 .append(", ").append(additionalHeaders_)
1132                 .append(", ").append(credentials_)
1133                 .append(">]");
1134         return builder.toString();
1135     }
1136 
1137     private void writeObject(final ObjectOutputStream oos) throws IOException {
1138         oos.defaultWriteObject();
1139         oos.writeObject(charset_ == null ? null : charset_.name());
1140         oos.writeObject(defaultResponseContentCharset_ == null ? null : defaultResponseContentCharset_.name());
1141     }
1142 
1143     private void readObject(final ObjectInputStream ois) throws ClassNotFoundException, IOException {
1144         ois.defaultReadObject();
1145         final String charsetName = (String) ois.readObject();
1146         if (charsetName != null) {
1147             charset_ = Charset.forName(charsetName);
1148         }
1149         final String defaultResponseContentCharset = (String) ois.readObject();
1150         if (defaultResponseContentCharset != null) {
1151             defaultResponseContentCharset_ = Charset.forName(defaultResponseContentCharset);
1152         }
1153     }
1154 }