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 }