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