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