| /* | |
| * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. Oracle designates this | |
| * particular file as subject to the "Classpath" exception as provided | |
| * by Oracle in the LICENSE file that accompanied this code. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| package jdk.internal.net.http; | |
| import java.io.IOException; | |
| import java.net.Authenticator; | |
| import java.net.InetSocketAddress; | |
| import java.net.Proxy; | |
| import java.net.ProxySelector; | |
| import java.net.URI; | |
| import java.time.Duration; | |
| import java.util.List; | |
| import java.util.Locale; | |
| import java.util.Objects; | |
| import java.util.Optional; | |
| import java.net.http.HttpClient; | |
| import java.net.http.HttpHeaders; | |
| import java.net.http.HttpRequest; | |
| import java.util.function.BiPredicate; | |
| import jdk.internal.net.http.common.Alpns; | |
| import jdk.internal.net.http.common.HttpHeadersBuilder; | |
| import jdk.internal.net.http.common.Utils; | |
| import jdk.internal.net.http.websocket.WebSocketRequest; | |
| import static java.net.Authenticator.RequestorType.SERVER; | |
| import static jdk.internal.net.http.common.Utils.ALLOWED_HEADERS; | |
| import static jdk.internal.net.http.common.Utils.ProxyHeaders; | |
| import static jdk.internal.net.http.common.Utils.copyProxy; | |
| public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { | |
| private final HttpHeaders userHeaders; | |
| private final HttpHeadersBuilder systemHeadersBuilder; | |
| private final URI uri; | |
| private volatile Proxy proxy; // ensure safe publishing | |
| private final InetSocketAddress authority; // only used when URI not specified | |
| private final String method; | |
| final BodyPublisher requestPublisher; | |
| final boolean secure; | |
| final boolean expectContinue; | |
| private volatile boolean isWebSocket; | |
| private final Duration timeout; // may be null | |
| private final Optional<HttpClient.Version> version; | |
| private volatile boolean userSetAuthorization; | |
| private volatile boolean userSetProxyAuthorization; | |
| private static String userAgent() { | |
| String version = System.getProperty("java.version"); | |
| return "Java-http-client/" + version; | |
| } | |
| /** The value of the User-Agent header for all requests sent by the client. */ | |
| public static final String USER_AGENT = userAgent(); | |
| /** | |
| * Creates an HttpRequestImpl from the given builder. | |
| */ | |
| public HttpRequestImpl(HttpRequestBuilderImpl builder) { | |
| String method = builder.method(); | |
| this.method = method == null ? "GET" : method; | |
| this.userHeaders = HttpHeaders.of(builder.headersBuilder().map(), ALLOWED_HEADERS); | |
| this.systemHeadersBuilder = new HttpHeadersBuilder(); | |
| this.uri = builder.uri(); | |
| assert uri != null; | |
| this.proxy = null; | |
| this.expectContinue = builder.expectContinue(); | |
| this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https"); | |
| this.requestPublisher = builder.bodyPublisher(); // may be null | |
| this.timeout = builder.timeout(); | |
| this.version = builder.version(); | |
| this.authority = null; | |
| } | |
| /** | |
| * Creates an HttpRequestImpl from the given request. | |
| */ | |
| public HttpRequestImpl(HttpRequest request, ProxySelector ps) { | |
| String method = request.method(); | |
| if (method != null && !Utils.isValidName(method)) | |
| throw new IllegalArgumentException("illegal method \"" | |
| + method.replace("\n","\\n") | |
| .replace("\r", "\\r") | |
| .replace("\t", "\\t") | |
| + "\""); | |
| URI requestURI = Objects.requireNonNull(request.uri(), | |
| "uri must be non null"); | |
| Duration timeout = request.timeout().orElse(null); | |
| this.method = method == null ? "GET" : method; | |
| this.userHeaders = HttpHeaders.of(request.headers().map(), Utils.VALIDATE_USER_HEADER); | |
| if (request instanceof HttpRequestImpl) { | |
| // all cases exception WebSocket should have a new system headers | |
| this.isWebSocket = ((HttpRequestImpl) request).isWebSocket; | |
| if (isWebSocket) { | |
| this.systemHeadersBuilder = ((HttpRequestImpl)request).systemHeadersBuilder; | |
| } else { | |
| this.systemHeadersBuilder = new HttpHeadersBuilder(); | |
| } | |
| } else { | |
| HttpRequestBuilderImpl.checkURI(requestURI); | |
| checkTimeout(timeout); | |
| this.systemHeadersBuilder = new HttpHeadersBuilder(); | |
| } | |
| if (userHeaders.firstValue("User-Agent").isEmpty()) { | |
| this.systemHeadersBuilder.setHeader("User-Agent", USER_AGENT); | |
| } | |
| this.uri = requestURI; | |
| this.proxy = retrieveProxy(request, ps, uri); | |
| this.expectContinue = request.expectContinue(); | |
| this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https"); | |
| this.requestPublisher = request.bodyPublisher().orElse(null); | |
| this.timeout = timeout; | |
| this.version = request.version(); | |
| this.authority = null; | |
| } | |
| private static void checkTimeout(Duration duration) { | |
| if (duration != null) { | |
| if (duration.isNegative() || Duration.ZERO.equals(duration)) | |
| throw new IllegalArgumentException("Invalid duration: " + duration); | |
| } | |
| } | |
| /** Returns a new instance suitable for redirection. */ | |
| public static HttpRequestImpl newInstanceForRedirection(URI uri, | |
| String method, | |
| HttpRequestImpl other, | |
| boolean mayHaveBody) { | |
| if (uri.getScheme().equalsIgnoreCase(other.uri.getScheme()) && | |
| uri.getRawAuthority().equals(other.uri.getRawAuthority())) { | |
| return new HttpRequestImpl(uri, method, other, mayHaveBody, Optional.empty()); | |
| } | |
| return new HttpRequestImpl(uri, method, other, mayHaveBody, Optional.of(Utils.ALLOWED_REDIRECT_HEADERS)); | |
| } | |
| /** Returns a new instance suitable for authentication. */ | |
| public static HttpRequestImpl newInstanceForAuthentication(HttpRequestImpl other) { | |
| HttpRequestImpl request = new HttpRequestImpl(other.uri(), other.method(), other, true); | |
| if (request.isWebSocket()) { | |
| Utils.setWebSocketUpgradeHeaders(request); | |
| } | |
| return request; | |
| } | |
| /** | |
| * Creates a HttpRequestImpl using fields of an existing request impl. | |
| * The newly created HttpRequestImpl does not copy the system headers. | |
| */ | |
| private HttpRequestImpl(URI uri, | |
| String method, | |
| HttpRequestImpl other, | |
| boolean mayHaveBody) { | |
| this(uri, method, other, mayHaveBody, Optional.empty()); | |
| } | |
| private HttpRequestImpl(URI uri, | |
| String method, | |
| HttpRequestImpl other, | |
| boolean mayHaveBody, | |
| Optional<BiPredicate<String, String>> redirectHeadersFilter) { | |
| assert method == null || Utils.isValidName(method); | |
| this.method = method == null ? "GET" : method; | |
| HttpHeaders userHeaders = redirectHeadersFilter.isPresent() ? | |
| HttpHeaders.of(other.userHeaders.map(), redirectHeadersFilter.get()) : other.userHeaders; | |
| this.userHeaders = userHeaders; | |
| this.isWebSocket = other.isWebSocket; | |
| this.systemHeadersBuilder = new HttpHeadersBuilder(); | |
| if (userHeaders.firstValue("User-Agent").isEmpty()) { | |
| this.systemHeadersBuilder.setHeader("User-Agent", USER_AGENT); | |
| } | |
| this.uri = uri; | |
| this.proxy = other.proxy; | |
| this.expectContinue = other.expectContinue; | |
| this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https"); | |
| this.requestPublisher = mayHaveBody ? publisher(other) : null; // may be null | |
| this.timeout = other.timeout; | |
| this.version = other.version(); | |
| this.authority = null; | |
| } | |
| private BodyPublisher publisher(HttpRequestImpl other) { | |
| BodyPublisher res = other.requestPublisher; | |
| if (!Objects.equals(method, other.method)) { | |
| res = null; | |
| } | |
| return res; | |
| } | |
| /* used for creating CONNECT requests */ | |
| HttpRequestImpl(String method, InetSocketAddress authority, ProxyHeaders headers) { | |
| // TODO: isWebSocket flag is not specified, but the assumption is that | |
| // such a request will never be made on a connection that will be returned | |
| // to the connection pool (we might need to revisit this constructor later) | |
| assert "CONNECT".equalsIgnoreCase(method); | |
| this.method = method; | |
| this.systemHeadersBuilder = new HttpHeadersBuilder(); | |
| this.systemHeadersBuilder.map().putAll(headers.systemHeaders().map()); | |
| this.userHeaders = headers.userHeaders(); | |
| this.uri = URI.create("socket://" + authority.getHostString() + ":" | |
| + authority.getPort() + "/"); | |
| this.proxy = null; | |
| this.requestPublisher = null; | |
| this.authority = authority; | |
| this.secure = false; | |
| this.expectContinue = false; | |
| this.timeout = null; | |
| // The CONNECT request sent for tunneling is only used in two cases: | |
| // 1. websocket, which only supports HTTP/1.1 | |
| // 2. SSL tunneling through a HTTP/1.1 proxy | |
| // In either case we do not want to upgrade the connection to the proxy. | |
| // What we want to possibly upgrade is the tunneled connection to the | |
| // target server (so not the CONNECT request itself) | |
| this.version = Optional.of(HttpClient.Version.HTTP_1_1); | |
| } | |
| final boolean isConnect() { | |
| return "CONNECT".equalsIgnoreCase(method); | |
| } | |
| /** | |
| * Creates a HttpRequestImpl from the given set of Headers and the associated | |
| * "parent" request. Fields not taken from the headers are taken from the | |
| * parent. | |
| */ | |
| static HttpRequestImpl createPushRequest(HttpRequestImpl parent, | |
| HttpHeaders headers) | |
| throws IOException | |
| { | |
| return new HttpRequestImpl(parent, headers); | |
| } | |
| // only used for push requests | |
| private HttpRequestImpl(HttpRequestImpl parent, HttpHeaders headers) | |
| throws IOException | |
| { | |
| this.method = headers.firstValue(":method") | |
| .orElseThrow(() -> new IOException("No method in Push Promise")); | |
| String path = headers.firstValue(":path") | |
| .orElseThrow(() -> new IOException("No path in Push Promise")); | |
| String scheme = headers.firstValue(":scheme") | |
| .orElseThrow(() -> new IOException("No scheme in Push Promise")); | |
| String authority = headers.firstValue(":authority") | |
| .orElseThrow(() -> new IOException("No authority in Push Promise")); | |
| StringBuilder sb = new StringBuilder(); | |
| sb.append(scheme).append("://").append(authority).append(path); | |
| this.uri = URI.create(sb.toString()); | |
| this.proxy = null; | |
| this.userHeaders = HttpHeaders.of(headers.map(), ALLOWED_HEADERS); | |
| this.systemHeadersBuilder = parent.systemHeadersBuilder; | |
| this.expectContinue = parent.expectContinue; | |
| this.secure = parent.secure; | |
| this.requestPublisher = parent.requestPublisher; | |
| this.timeout = parent.timeout; | |
| this.version = parent.version; | |
| this.authority = null; | |
| } | |
| @Override | |
| public String toString() { | |
| return (uri == null ? "" : uri.toString()) + " " + method; | |
| } | |
| @Override | |
| public HttpHeaders headers() { | |
| return userHeaders; | |
| } | |
| InetSocketAddress authority() { return authority; } | |
| void setH2Upgrade(Exchange<?> exchange) { | |
| systemHeadersBuilder.setHeader("Connection", "Upgrade, HTTP2-Settings"); | |
| systemHeadersBuilder.setHeader("Upgrade", Alpns.H2C); | |
| systemHeadersBuilder.setHeader("HTTP2-Settings", exchange.h2cSettingsStrings()); | |
| } | |
| @Override | |
| public boolean expectContinue() { return expectContinue; } | |
| /** Retrieves a copy of the proxy either from the given {@link HttpRequest} or {@link ProxySelector}, if there is one. */ | |
| private static Proxy retrieveProxy(HttpRequest request, ProxySelector ps, URI uri) { | |
| // WebSocket determines and sets the proxy itself | |
| if (request instanceof HttpRequestImpl requestImpl && requestImpl.isWebSocket) { | |
| return requestImpl.proxy; | |
| } | |
| // Try to find a matching one from the `ProxySelector` | |
| if (ps != null) { | |
| List<Proxy> pl = ps.select(uri); | |
| if (!pl.isEmpty()) { | |
| Proxy p = pl.getFirst(); | |
| if (p.type() == Proxy.Type.HTTP) { | |
| return copyProxy(p); | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| InetSocketAddress proxy() { | |
| if (proxy == null || proxy.type() != Proxy.Type.HTTP | |
| || method.equalsIgnoreCase("CONNECT")) { | |
| return null; | |
| } | |
| return (InetSocketAddress)proxy.address(); | |
| } | |
| boolean secure() { return secure; } | |
| @Override | |
| public void setProxy(Proxy proxy) { | |
| assert isWebSocket; | |
| this.proxy = copyProxy(proxy); | |
| } | |
| @Override | |
| public void isWebSocket(boolean is) { | |
| isWebSocket = is; | |
| } | |
| boolean isWebSocket() { | |
| return isWebSocket; | |
| } | |
| /** | |
| * These flags are set if the user set an Authorization or Proxy-Authorization header | |
| * overriding headers produced by an Authenticator that was also set | |
| * | |
| * The values are checked in the AuthenticationFilter which tells the library | |
| * to return whatever response received to the user instead of causing request | |
| * to be resent, in case of error. | |
| */ | |
| public void setUserSetAuthFlag(Authenticator.RequestorType type, boolean value) { | |
| if (type == SERVER) { | |
| userSetAuthorization = value; | |
| } else { | |
| userSetProxyAuthorization = value; | |
| } | |
| } | |
| public boolean getUserSetAuthFlag(Authenticator.RequestorType type) { | |
| if (type == SERVER) { | |
| return userSetAuthorization; | |
| } else { | |
| return userSetProxyAuthorization; | |
| } | |
| } | |
| @Override | |
| public Optional<BodyPublisher> bodyPublisher() { | |
| return requestPublisher == null ? Optional.empty() | |
| : Optional.of(requestPublisher); | |
| } | |
| /** | |
| * Returns the request method for this request. If not set explicitly, | |
| * the default method for any request is "GET". | |
| */ | |
| @Override | |
| public String method() { return method; } | |
| @Override | |
| public URI uri() { return uri; } | |
| @Override | |
| public Optional<Duration> timeout() { | |
| return timeout == null ? Optional.empty() : Optional.of(timeout); | |
| } | |
| HttpHeaders getUserHeaders() { return userHeaders; } | |
| HttpHeadersBuilder getSystemHeadersBuilder() { return systemHeadersBuilder; } | |
| @Override | |
| public Optional<HttpClient.Version> version() { return version; } | |
| @Override | |
| public void setSystemHeader(String name, String value) { | |
| systemHeadersBuilder.setHeader(name, value); | |
| } | |
| InetSocketAddress getAddress() { | |
| URI uri = uri(); | |
| if (uri == null) { | |
| return authority(); | |
| } | |
| int p = uri.getPort(); | |
| if (p == -1) { | |
| if (uri.getScheme().equalsIgnoreCase("https")) { | |
| p = 443; | |
| } else { | |
| p = 80; | |
| } | |
| } | |
| final String host = uri.getHost(); | |
| final int port = p; | |
| if (proxy() == null) { | |
| return new InetSocketAddress(host, port); | |
| } else { | |
| return InetSocketAddress.createUnresolved(host, port); | |
| } | |
| } | |
| } |