8197564: HTTP Client implementation

Co-authored-by: Daniel Fuchs <daniel.fuchs@oracle.com>
Co-authored-by: Michael McMahon <michael.x.mcmahon@oracle.com>
Co-authored-by: Pavel Rappo <pavel.rappo@oracle.com>
Reviewed-by: chegar, dfuchs, michaelm, prappo
This commit is contained in:
Chris Hegarty 2018-04-17 08:54:17 -07:00
parent a9234c0ab6
commit a3b61fd4c7
398 changed files with 37643 additions and 10897 deletions

View file

@ -0,0 +1,605 @@
/*
* Copyright (c) 2015, 2018, 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 java.net.http;
import java.io.IOException;
import java.net.Authenticator;
import java.net.CookieHandler;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URLPermission;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.PushPromiseHandler;
import jdk.internal.net.http.HttpClientBuilderImpl;
/**
* An HTTP Client.
*
* <p> An {@code HttpClient} can be used to send {@linkplain HttpRequest
* requests} and retrieve their {@linkplain HttpResponse responses}. An {@code
* HttpClient} is created through a {@link HttpClient#newBuilder() builder}. The
* builder can be used to configure per-client state, like: the preferred
* protocol version ( HTTP/1.1 or HTTP/2 ), whether to follow redirects, a
* proxy, an authenticator, etc. Once built, an {@code HttpClient} is immutable,
* and can be used to send multiple requests.
*
* <p> An {@code HttpClient} provides configuration information, and resource
* sharing, for all requests send through it.
*
* <p> A {@link BodyHandler BodyHandler} must be supplied for each {@link
* HttpRequest} sent. The {@code BodyHandler} determines how to handle the
* response body, if any. Once an {@link HttpResponse} is received, the
* headers, response code, and body (typically) are available. Whether the
* response body bytes have been read or not depends on the type, {@code T}, of
* the response body.
*
* <p> Requests can be sent either synchronously or asynchronously:
* <ul>
* <li>{@link HttpClient#send(HttpRequest, BodyHandler)} blocks
* until the request has been sent and the response has been received.</li>
*
* <li>{@link HttpClient#sendAsync(HttpRequest, BodyHandler)} sends the
* request and receives the response asynchronously. The {@code sendAsync}
* method returns immediately with a {@link CompletableFuture
* CompletableFuture}&lt;{@link HttpResponse}&gt;. The {@code
* CompletableFuture} completes when the response becomes available. The
* returned {@code CompletableFuture} can be combined in different ways to
* declare dependencies among several asynchronous tasks.</li>
* </ul>
*
* <p><b>Synchronous Example</b>
* <pre>{@code HttpClient client = HttpClient.newBuilder()
* .version(Version.HTTP_1_1)
* .followRedirects(Redirect.NORMAL)
* .proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 80)))
* .authenticator(Authenticator.getDefault())
* .build();
* HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
* System.out.println(response.statusCode());
* System.out.println(response.body()); }</pre>
*
* <p><b>Asynchronous Example</b>
* <pre>{@code HttpRequest request = HttpRequest.newBuilder()
* .uri(URI.create("https://foo.com/"))
* .timeout(Duration.ofMinutes(1))
* .header("Content-Type", "application/json")
* .POST(BodyPublishers.ofFile(Paths.get("file.json")))
* .build();
* client.sendAsync(request, BodyHandlers.ofString())
* .thenApply(HttpResponse::body)
* .thenAccept(System.out::println); }</pre>
*
* <p> <a id="securitychecks"></a><b>Security checks</b></a>
*
* <p> If a security manager is present then security checks are performed by
* the HTTP Client's sending methods. An appropriate {@link URLPermission} is
* required to access the destination server, and proxy server if one has
* been configured. The form of the {@code URLPermission} required to access a
* proxy has a {@code method} parameter of {@code "CONNECT"} (for all kinds of
* proxying) and a {@code URL} string of the form {@code "socket://host:port"}
* where host and port specify the proxy's address.
*
* @implNote If an explicit {@linkplain HttpClient.Builder#executor(Executor)
* executor} has not been set for an {@code HttpClient}, and a security manager
* has been installed, then the default executor will execute asynchronous and
* dependent tasks in a context that is granted no permissions. Custom
* {@linkplain HttpRequest.BodyPublisher request body publishers}, {@linkplain
* HttpResponse.BodyHandler response body handlers}, {@linkplain
* HttpResponse.BodySubscriber response body subscribers}, and {@linkplain
* WebSocket.Listener WebSocket Listeners}, if executing operations that require
* privileges, should do so within an appropriate {@linkplain
* AccessController#doPrivileged(PrivilegedAction) privileged context}.
*
* @since 11
*/
public abstract class HttpClient {
/**
* Creates an HttpClient.
*/
protected HttpClient() {}
/**
* Returns a new {@code HttpClient} with default settings.
*
* <p> Equivalent to {@code newBuilder().build()}.
*
* <p> The default settings include: the "GET" request method, a preference
* of {@linkplain HttpClient.Version#HTTP_2 HTTP/2}, a redirection policy of
* {@linkplain Redirect#NEVER NEVER}, the {@linkplain
* ProxySelector#getDefault() default proxy selector}, and the {@linkplain
* SSLContext#getDefault() default SSL context}.
*
* @implNote The system-wide default values are retrieved at the time the
* {@code HttpClient} instance is constructed. Changing the system-wide
* values after an {@code HttpClient} instance has been built, for
* instance, by calling {@link ProxySelector#setDefault(ProxySelector)}
* or {@link SSLContext#setDefault(SSLContext)}, has no effect on already
* built instances.
*
* @return a new HttpClient
*/
public static HttpClient newHttpClient() {
return newBuilder().build();
}
/**
* Creates a new {@code HttpClient} builder.
*
* @return an {@code HttpClient.Builder}
*/
public static Builder newBuilder() {
return new HttpClientBuilderImpl();
}
/**
* A builder of {@linkplain HttpClient HTTP Clients}.
*
* <p> Builders are created by invoking {@link HttpClient#newBuilder()
* newBuilder}. Each of the setter methods modifies the state of the builder
* and returns the same instance. Builders are not thread-safe and should not be
* used concurrently from multiple threads without external synchronization.
*
* @since 11
*/
public interface Builder {
/**
* A proxy selector that always return {@link Proxy#NO_PROXY} implying
* a direct connection.
*
* <p> This is a convenience object that can be passed to
* {@link #proxy(ProxySelector)} in order to build an instance of
* {@link HttpClient} that uses no proxy.
*/
public static final ProxySelector NO_PROXY = ProxySelector.of(null);
/**
* Sets a cookie handler.
*
* @param cookieHandler the cookie handler
* @return this builder
*/
public Builder cookieHandler(CookieHandler cookieHandler);
/**
* Sets an {@code SSLContext}.
*
* <p> If this method is not invoked prior to {@linkplain #build()
* building}, then newly built clients will use the {@linkplain
* SSLContext#getDefault() default context}, which is normally adequate
* for client applications that do not need to specify protocols, or
* require client authentication.
*
* @param sslContext the SSLContext
* @return this builder
*/
public Builder sslContext(SSLContext sslContext);
/**
* Sets an {@code SSLParameters}.
*
* <p> If this method is not invoked prior to {@linkplain #build()
* building}, then newly built clients will use a default,
* implementation specific, set of parameters.
*
* <p> Some parameters which are used internally by the HTTP Client
* implementation (such as the application protocol list) should not be
* set by callers, as they may be ignored. The contents of the given
* object are copied.
*
* @param sslParameters the SSLParameters
* @return this builder
*/
public Builder sslParameters(SSLParameters sslParameters);
/**
* Sets the executor to be used for asynchronous and dependent tasks.
*
* <p> If this method is not invoked prior to {@linkplain #build()
* building}, a default executor is created for each newly built {@code
* HttpClient}. The default executor uses a {@linkplain
* Executors#newCachedThreadPool(ThreadFactory) cached thread pool},
* with a custom thread factory.
*
* @implNote If a security manager has been installed, the thread
* factory creates threads that run with an access control context that
* has no permissions.
*
* @param executor the Executor
* @return this builder
*/
public Builder executor(Executor executor);
/**
* Specifies whether requests will automatically follow redirects issued
* by the server.
*
* <p> If this method is not invoked prior to {@linkplain #build()
* building}, then newly built clients will use a default redirection
* policy of {@link Redirect#NEVER NEVER}.
*
* @param policy the redirection policy
* @return this builder
*/
public Builder followRedirects(Redirect policy);
/**
* Requests a specific HTTP protocol version where possible.
*
* <p> If this method is not invoked prior to {@linkplain #build()
* building}, then newly built clients will prefer {@linkplain
* Version#HTTP_2 HTTP/2}.
*
* <p> If set to {@linkplain Version#HTTP_2 HTTP/2}, then each request
* will attempt to upgrade to HTTP/2. If the upgrade succeeds, then the
* response to this request will use HTTP/2 and all subsequent requests
* and responses to the same
* <a href="https://tools.ietf.org/html/rfc6454#section-4">origin server</a>
* will use HTTP/2. If the upgrade fails, then the response will be
* handled using HTTP/1.1
*
* @implNote Constraints may also affect the selection of protocol version.
* For example, if HTTP/2 is requested through a proxy, and if the implementation
* does not support this mode, then HTTP/1.1 may be used
*
* @param version the requested HTTP protocol version
* @return this builder
*/
public Builder version(HttpClient.Version version);
/**
* Sets the default priority for any HTTP/2 requests sent from this
* client. The value provided must be between {@code 1} and {@code 256}
* (inclusive).
*
* @param priority the priority weighting
* @return this builder
* @throws IllegalArgumentException if the given priority is out of range
*/
public Builder priority(int priority);
/**
* Sets a {@link java.net.ProxySelector}.
*
* @apiNote {@link ProxySelector#of(InetSocketAddress) ProxySelector::of}
* provides a {@code ProxySelector} which uses a single proxy for all
* requests. The system-wide proxy selector can be retrieved by
* {@link ProxySelector#getDefault()}.
*
* @implNote
* If this method is not invoked prior to {@linkplain #build() building},
* then newly built clients will use the {@linkplain
* ProxySelector#getDefault() default proxy selector}, which is usually
* adequate for client applications. The default proxy selector supports
* a set of system properties</a> related to
* <a href="{@docRoot}/java.base/java/net/doc-files/net-properties.html#Proxies">
* proxy settings</a>. This default behavior can be disabled by
* supplying an explicit proxy selector, such as {@link #NO_PROXY} or
* one returned by {@link ProxySelector#of(InetSocketAddress)
* ProxySelector::of}, before {@linkplain #build() building}.
*
* @param proxySelector the ProxySelector
* @return this builder
*/
public Builder proxy(ProxySelector proxySelector);
/**
* Sets an authenticator to use for HTTP authentication.
*
* @param authenticator the Authenticator
* @return this builder
*/
public Builder authenticator(Authenticator authenticator);
/**
* Returns a new {@link HttpClient} built from the current state of this
* builder.
*
* @return a new {@code HttpClient}
*/
public HttpClient build();
}
/**
* Returns an {@code Optional} containing this client's {@link
* CookieHandler}. If no {@code CookieHandler} was set in this client's
* builder, then the {@code Optional} is empty.
*
* @return an {@code Optional} containing this client's {@code CookieHandler}
*/
public abstract Optional<CookieHandler> cookieHandler();
/**
* Returns the follow redirects policy for this client. The default value
* for client's built by builders that do not specify a redirect policy is
* {@link HttpClient.Redirect#NEVER NEVER}.
*
* @return this client's follow redirects setting
*/
public abstract Redirect followRedirects();
/**
* Returns an {@code Optional} containing the {@code ProxySelector}
* supplied to this client. If no proxy selector was set in this client's
* builder, then the {@code Optional} is empty.
*
* <p> Even though this method may return an empty optional, the {@code
* HttpClient} may still have a non-exposed {@linkplain
* Builder#proxy(ProxySelector) default proxy selector} that is
* used for sending HTTP requests.
*
* @return an {@code Optional} containing the proxy selector supplied
* to this client.
*/
public abstract Optional<ProxySelector> proxy();
/**
* Returns this client's {@code SSLContext}.
*
* <p> If no {@code SSLContext} was set in this client's builder, then the
* {@linkplain SSLContext#getDefault() default context} is returned.
*
* @return this client's SSLContext
*/
public abstract SSLContext sslContext();
/**
* Returns a copy of this client's {@link SSLParameters}.
*
* <p> If no {@code SSLParameters} were set in the client's builder, then an
* implementation specific default set of parameters, that the client will
* use, is returned.
*
* @return this client's {@code SSLParameters}
*/
public abstract SSLParameters sslParameters();
/**
* Returns an {@code Optional} containing the {@link Authenticator} set on
* this client. If no {@code Authenticator} was set in the client's builder,
* then the {@code Optional} is empty.
*
* @return an {@code Optional} containing this client's {@code Authenticator}
*/
public abstract Optional<Authenticator> authenticator();
/**
* Returns the preferred HTTP protocol version for this client. The default
* value is {@link HttpClient.Version#HTTP_2}
*
* @implNote Constraints may also affect the selection of protocol version.
* For example, if HTTP/2 is requested through a proxy, and if the
* implementation does not support this mode, then HTTP/1.1 may be used
*
* @return the HTTP protocol version requested
*/
public abstract HttpClient.Version version();
/**
* Returns an {@code Optional} containing this client's {@link
* Executor}. If no {@code Executor} was set in the client's builder,
* then the {@code Optional} is empty.
*
* <p> Even though this method may return an empty optional, the {@code
* HttpClient} may still have an non-exposed {@linkplain
* HttpClient.Builder#executor(Executor) default executor} that is used for
* executing asynchronous and dependent tasks.
*
* @return an {@code Optional} containing this client's {@code Executor}
*/
public abstract Optional<Executor> executor();
/**
* The HTTP protocol version.
*
* @since 11
*/
public enum Version {
/**
* HTTP version 1.1
*/
HTTP_1_1,
/**
* HTTP version 2
*/
HTTP_2
}
/**
* Defines the automatic redirection policy.
*
* <p> The automatic redirection policy is checked whenever a {@code 3XX}
* response code is received. If redirection does not happen automatically,
* then the response, containing the {@code 3XX} response code, is returned,
* where it can be handled manually.
*
* <p> {@code Redirect} policy is set via the {@linkplain
* HttpClient.Builder#followRedirects(Redirect) Builder.followRedirects}
* method.
*
* @implNote When automatic redirection occurs, the request method of the
* redirected request may be modified depending on the specific {@code 30X}
* status code, as specified in <a href="https://tools.ietf.org/html/rfc7231">
* RFC 7231</a>. In addition, the {@code 301} and {@code 302} status codes
* cause a {@code POST} request to be converted to a {@code GET} in the
* redirected request.
*
* @since 11
*/
public enum Redirect {
/**
* Never redirect.
*/
NEVER,
/**
* Always redirect.
*/
ALWAYS,
/**
* Always redirect, except from HTTPS URLs to HTTP URLs.
*/
NORMAL
}
/**
* Sends the given request using this client, blocking if necessary to get
* the response. The returned {@link HttpResponse}{@code <T>} contains the
* response status, headers, and body ( as handled by given response body
* handler ).
*
* @param <T> the response body type
* @param request the request
* @param responseBodyHandler the response body handler
* @return the response
* @throws IOException if an I/O error occurs when sending or receiving
* @throws InterruptedException if the operation is interrupted
* @throws IllegalArgumentException if the {@code request} argument is not
* a request that could have been validly built as specified by {@link
* HttpRequest.Builder HttpRequest.Builder}.
* @throws SecurityException If a security manager has been installed
* and it denies {@link java.net.URLPermission access} to the
* URL in the given request, or proxy if one is configured.
* See <a href="#securitychecks">security checks</a> for further
* information.
*/
public abstract <T> HttpResponse<T>
send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
throws IOException, InterruptedException;
/**
* Sends the given request asynchronously using this client with the given
* response body handler.
*
* <p> Equivalent to: {@code sendAsync(request, responseBodyHandler, null)}.
*
* @param <T> the response body type
* @param request the request
* @param responseBodyHandler the response body handler
* @return a {@code CompletableFuture<HttpResponse<T>>}
* @throws IllegalArgumentException if the {@code request} argument is not
* a request that could have been validly built as specified by {@link
* HttpRequest.Builder HttpRequest.Builder}.
*/
public abstract <T> CompletableFuture<HttpResponse<T>>
sendAsync(HttpRequest request,
BodyHandler<T> responseBodyHandler);
/**
* Sends the given request asynchronously using this client with the given
* response body handler and push promise handler.
*
* <p> The returned completable future, if completed successfully, completes
* with an {@link HttpResponse}{@code <T>} that contains the response status,
* headers, and body ( as handled by given response body handler ).
*
* <p> {@linkplain PushPromiseHandler Push promises} received, if any, are
* handled by the given {@code pushPromiseHandler}. A {@code null} valued
* {@code pushPromiseHandler} rejects any push promises.
*
* <p> The returned completable future completes exceptionally with:
* <ul>
* <li>{@link IOException} - if an I/O error occurs when sending or receiving</li>
* <li>{@link SecurityException} - If a security manager has been installed
* and it denies {@link java.net.URLPermission access} to the
* URL in the given request, or proxy if one is configured.
* See <a href="#securitychecks">security checks</a> for further
* information.</li>
* </ul>
*
* @param <T> the response body type
* @param request the request
* @param responseBodyHandler the response body handler
* @param pushPromiseHandler push promise handler, may be null
* @return a {@code CompletableFuture<HttpResponse<T>>}
* @throws IllegalArgumentException if the {@code request} argument is not
* a request that could have been validly built as specified by {@link
* HttpRequest.Builder HttpRequest.Builder}.
*/
public abstract <T> CompletableFuture<HttpResponse<T>>
sendAsync(HttpRequest request,
BodyHandler<T> responseBodyHandler,
PushPromiseHandler<T> pushPromiseHandler);
/**
* Creates a new {@code WebSocket} builder (optional operation).
*
* <p> <b>Example</b>
* <pre>{@code HttpClient client = HttpClient.newHttpClient();
* CompletableFuture<WebSocket> ws = client.newWebSocketBuilder()
* .buildAsync(URI.create("ws://websocket.example.com"), listener); }</pre>
*
* <p> Finer control over the WebSocket Opening Handshake can be achieved
* by using a custom {@code HttpClient}.
*
* <p> <b>Example</b>
* <pre>{@code InetSocketAddress addr = new InetSocketAddress("proxy.example.com", 80);
* HttpClient client = HttpClient.newBuilder()
* .proxy(ProxySelector.of(addr))
* .build();
* CompletableFuture<WebSocket> ws = client.newWebSocketBuilder()
* .buildAsync(URI.create("ws://websocket.example.com"), listener); }</pre>
*
* @implSpec The default implementation of this method throws
* {@code UnsupportedOperationException}. Clients obtained through
* {@link HttpClient#newHttpClient()} or {@link HttpClient#newBuilder()}
* return a {@code WebSocket} builder.
*
* @implNote Both builder and {@code WebSocket}s created with it operate in
* a non-blocking fashion. That is, their methods do not block before
* returning a {@code CompletableFuture}. Asynchronous tasks are executed in
* this {@code HttpClient}'s executor.
*
* <p> When a {@code CompletionStage} returned from
* {@link WebSocket.Listener#onClose Listener.onClose} completes,
* the {@code WebSocket} will send a Close message that has the same code
* the received message has and an empty reason.
*
* @return a {@code WebSocket.Builder}
* @throws UnsupportedOperationException
* if this {@code HttpClient} does not provide WebSocket support
*/
public WebSocket.Builder newWebSocketBuilder() {
throw new UnsupportedOperationException();
}
}

View file

@ -0,0 +1,168 @@
/*
* Copyright (c) 2015, 2018, 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 java.net.http;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static java.util.Objects.requireNonNull;
/**
* A read-only view of a set of HTTP headers.
*
* <p> An {@code HttpHeaders} is not created directly, but rather returned from
* an {@link HttpResponse HttpResponse}. Specific HTTP headers can be set for
* {@linkplain HttpRequest requests} through the one of the request builder's
* {@link HttpRequest.Builder#header(String, String) headers} methods.
*
* <p> The methods of this class ( that accept a String header name ), and the
* Map returned by the {@link #map() map} method, operate without regard to
* case when retrieving the header value.
*
* <p> {@code HttpHeaders} instances are immutable.
*
* @since 11
*/
public abstract class HttpHeaders {
/**
* Creates an HttpHeaders.
*/
protected HttpHeaders() {}
/**
* Returns an {@link Optional} containing the first value of the given named
* (and possibly multi-valued) header. If the header is not present, then
* the returned {@code Optional} is empty.
*
* @implSpec
* The default implementation invokes
* {@code allValues(name).stream().findFirst()}
*
* @param name the header name
* @return an {@code Optional<String>} for the first named value
*/
public Optional<String> firstValue(String name) {
return allValues(name).stream().findFirst();
}
/**
* Returns an {@link OptionalLong} containing the first value of the
* named header field. If the header is not present, then the Optional is
* empty. If the header is present but contains a value that does not parse
* as a {@code Long} value, then an exception is thrown.
*
* @implSpec
* The default implementation invokes
* {@code allValues(name).stream().mapToLong(Long::valueOf).findFirst()}
*
* @param name the header name
* @return an {@code OptionalLong}
* @throws NumberFormatException if a value is found, but does not parse as
* a Long
*/
public OptionalLong firstValueAsLong(String name) {
return allValues(name).stream().mapToLong(Long::valueOf).findFirst();
}
/**
* Returns an unmodifiable List of all of the values of the given named
* header. Always returns a List, which may be empty if the header is not
* present.
*
* @implSpec
* The default implementation invokes, among other things, the
* {@code map().get(name)} to retrieve the list of header values.
*
* @param name the header name
* @return a List of String values
*/
public List<String> allValues(String name) {
requireNonNull(name);
List<String> values = map().get(name);
// Making unmodifiable list out of empty in order to make a list which
// throws UOE unconditionally
return values != null ? values : unmodifiableList(emptyList());
}
/**
* Returns an unmodifiable multi Map view of this HttpHeaders.
*
* @return the Map
*/
public abstract Map<String, List<String>> map();
/**
* Tests this HTTP headers instance for equality with the given object.
*
* <p> If the given object is not an {@code HttpHeaders} then this
* method returns {@code false}. Two HTTP headers are equal if each
* of their corresponding {@linkplain #map() maps} are equal.
*
* <p> This method satisfies the general contract of the {@link
* Object#equals(Object) Object.equals} method.
*
* @param obj the object to which this object is to be compared
* @return {@code true} if, and only if, the given object is an {@code
* HttpHeaders} that is equal to this HTTP headers
*/
public final boolean equals(Object obj) {
if (!(obj instanceof HttpHeaders))
return false;
HttpHeaders that = (HttpHeaders)obj;
return this.map().equals(that.map());
}
/**
* Computes a hash code for this HTTP headers instance.
*
* <p> The hash code is based upon the components of the HTTP headers
* {@link #map() map}, and satisfies the general contract of the
* {@link Object#hashCode Object.hashCode} method.
*
* @return the hash-code value for this HTTP headers
*/
public final int hashCode() {
return map().hashCode();
}
/**
* Returns this HTTP headers as a string.
*
* @return a string describing the HTTP headers
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString()).append(" { ");
sb.append(map());
sb.append(" }");
return sb.toString();
}
}

View file

@ -0,0 +1,650 @@
/*
* Copyright (c) 2015, 2018, 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 java.net.http;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Iterator;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Flow;
import java.util.function.Supplier;
import jdk.internal.net.http.HttpRequestBuilderImpl;
import jdk.internal.net.http.RequestPublishers;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* An HTTP request.
*
* <p> An {@code HttpRequest} instance is built through an {@code HttpRequest}
* {@linkplain HttpRequest.Builder builder}. An {@code HttpRequest} builder
* is obtained from one of the {@link HttpRequest#newBuilder(URI) newBuilder}
* methods. A request's {@link URI}, headers, and body can be set. Request
* bodies are provided through a {@link BodyPublisher BodyPublisher} supplied
* to one of the {@link Builder#POST(BodyPublisher) POST},
* {@link Builder#PUT(BodyPublisher) PUT} or
* {@link Builder#method(String,BodyPublisher) method} methods.
* Once all required parameters have been set in the builder, {@link
* Builder#build() build} will return the {@code HttpRequest}. Builders can be
* copied and modified many times in order to build multiple related requests
* that differ in some parameters.
*
* <p> The following is an example of a GET request that prints the response
* body as a String:
*
* <pre>{@code HttpClient client = HttpClient.newHttpClient();
* HttpRequest request = HttpRequest.newBuilder()
* .uri(URI.create("http://foo.com/"))
* .build();
* client.sendAsync(request, BodyHandlers.ofString())
* .thenApply(HttpResponse::body)
* .thenAccept(System.out::println)
* .join(); }</pre>
*
* <p>The class {@link BodyPublishers BodyPublishers} provides implementations
* of many common publishers. Alternatively, a custom {@code BodyPublisher}
* implementation can be used.
*
* @since 11
*/
public abstract class HttpRequest {
/**
* Creates an HttpRequest.
*/
protected HttpRequest() {}
/**
* A builder of {@linkplain HttpRequest HTTP requests}.
*
* <p> Instances of {@code HttpRequest.Builder} are created by calling {@link
* HttpRequest#newBuilder(URI)} or {@link HttpRequest#newBuilder()}.
*
* <p> Each of the setter methods modifies the state of the builder
* and returns the same instance. The methods are not synchronized and
* should not be called from multiple threads without external
* synchronization. The {@link #build() build} method returns a new
* {@code HttpRequest} each time it is invoked. Once built an {@code
* HttpRequest} is immutable, and can be sent multiple times.
*
* <p> Note, that not all request headers may be set by user code. Some are
* restricted for security reasons and others such as the headers relating
* to authentication, redirection and cookie management may be managed by
* specific APIs rather than through directly user set headers.
*
* @since 11
*/
public interface Builder {
/**
* Sets this {@code HttpRequest}'s request {@code URI}.
*
* @param uri the request URI
* @return this builder
* @throws IllegalArgumentException if the {@code URI} scheme is not
* supported
*/
public Builder uri(URI uri);
/**
* Requests the server to acknowledge the request before sending the
* body. This is disabled by default. If enabled, the server is
* requested to send an error response or a {@code 100 Continue}
* response before the client sends the request body. This means the
* request publisher for the request will not be invoked until this
* interim response is received.
*
* @param enable {@code true} if Expect continue to be sent
* @return this builder
*/
public Builder expectContinue(boolean enable);
/**
* Sets the preferred {@link HttpClient.Version} for this request.
*
* <p> The corresponding {@link HttpResponse} should be checked for the
* version that was actually used. If the version is not set in a
* request, then the version requested will be that of the sending
* {@link HttpClient}.
*
* @param version the HTTP protocol version requested
* @return this builder
*/
public Builder version(HttpClient.Version version);
/**
* Adds the given name value pair to the set of headers for this request.
* The given value is added to the list of values for that name.
*
* @implNote An implementation may choose to restrict some header names
* or values, as the HTTP Client may determine their value itself.
* For example, "Content-Length", which will be determined by
* the request Publisher. In such a case, an implementation of
* {@code HttpRequest.Builder} may choose to throw an
* {@code IllegalArgumentException} if such a header is passed
* to the builder.
*
* @param name the header name
* @param value the header value
* @return this builder
* @throws IllegalArgumentException if the header name or value is not
* valid, see <a href="https://tools.ietf.org/html/rfc7230#section-3.2">
* RFC 7230 section-3.2</a>, or the header name or value is restricted
* by the implementation.
*/
public Builder header(String name, String value);
/**
* Adds the given name value pairs to the set of headers for this
* request. The supplied {@code String} instances must alternate as
* header names and header values.
* To add several values to the same name then the same name must
* be supplied with each new value.
*
* @param headers the list of name value pairs
* @return this builder
* @throws IllegalArgumentException if there are an odd number of
* parameters, or if a header name or value is not valid, see
* <a href="https://tools.ietf.org/html/rfc7230#section-3.2">
* RFC 7230 section-3.2</a>, or a header name or value is
* {@linkplain #header(String, String) restricted} by the
* implementation.
*/
public Builder headers(String... headers);
/**
* Sets a timeout for this request. If the response is not received
* within the specified timeout then an {@link HttpTimeoutException} is
* thrown from {@link HttpClient#send(java.net.http.HttpRequest,
* java.net.http.HttpResponse.BodyHandler) HttpClient::send} or
* {@link HttpClient#sendAsync(java.net.http.HttpRequest,
* java.net.http.HttpResponse.BodyHandler) HttpClient::sendAsync}
* completes exceptionally with an {@code HttpTimeoutException}. The effect
* of not setting a timeout is the same as setting an infinite Duration, ie.
* block forever.
*
* @param duration the timeout duration
* @return this builder
* @throws IllegalArgumentException if the duration is non-positive
*/
public abstract Builder timeout(Duration duration);
/**
* Sets the given name value pair to the set of headers for this
* request. This overwrites any previously set values for name.
*
* @param name the header name
* @param value the header value
* @return this builder
* @throws IllegalArgumentException if the header name or value is not valid,
* see <a href="https://tools.ietf.org/html/rfc7230#section-3.2">
* RFC 7230 section-3.2</a>, or the header name or value is
* {@linkplain #header(String, String) restricted} by the
* implementation.
*/
public Builder setHeader(String name, String value);
/**
* Sets the request method of this builder to GET.
* This is the default.
*
* @return this builder
*/
public Builder GET();
/**
* Sets the request method of this builder to POST and sets its
* request body publisher to the given value.
*
* @param bodyPublisher the body publisher
*
* @return this builder
*/
public Builder POST(BodyPublisher bodyPublisher);
/**
* Sets the request method of this builder to PUT and sets its
* request body publisher to the given value.
*
* @param bodyPublisher the body publisher
*
* @return this builder
*/
public Builder PUT(BodyPublisher bodyPublisher);
/**
* Sets the request method of this builder to DELETE.
*
* @return this builder
*/
public Builder DELETE();
/**
* Sets the request method and request body of this builder to the
* given values.
*
* @apiNote The {@link BodyPublishers#noBody() noBody} request
* body publisher can be used where no request body is required or
* appropriate. Whether a method is restricted, or not, is
* implementation specific. For example, some implementations may choose
* to restrict the {@code CONNECT} method.
*
* @param method the method to use
* @param bodyPublisher the body publisher
* @return this builder
* @throws IllegalArgumentException if the method name is not
* valid, see <a href="https://tools.ietf.org/html/rfc7230#section-3.1.1">
* RFC 7230 section-3.1.1</a>, or the method is restricted by the
* implementation.
*/
public Builder method(String method, BodyPublisher bodyPublisher);
/**
* Builds and returns an {@link HttpRequest}.
*
* @return a new {@code HttpRequest}
* @throws IllegalStateException if a URI has not been set
*/
public HttpRequest build();
/**
* Returns an exact duplicate copy of this {@code Builder} based on
* current state. The new builder can then be modified independently of
* this builder.
*
* @return an exact copy of this builder
*/
public Builder copy();
}
/**
* Creates an {@code HttpRequest} builder with the given URI.
*
* @param uri the request URI
* @return a new request builder
* @throws IllegalArgumentException if the URI scheme is not supported.
*/
public static HttpRequest.Builder newBuilder(URI uri) {
return new HttpRequestBuilderImpl(uri);
}
/**
* Creates an {@code HttpRequest} builder.
*
* @return a new request builder
*/
public static HttpRequest.Builder newBuilder() {
return new HttpRequestBuilderImpl();
}
/**
* Returns an {@code Optional} containing the {@link BodyPublisher} set on
* this request. If no {@code BodyPublisher} was set in the requests's
* builder, then the {@code Optional} is empty.
*
* @return an {@code Optional} containing this request's {@code BodyPublisher}
*/
public abstract Optional<BodyPublisher> bodyPublisher();
/**
* Returns the request method for this request. If not set explicitly,
* the default method for any request is "GET".
*
* @return this request's method
*/
public abstract String method();
/**
* Returns an {@code Optional} containing this request's timeout duration.
* If the timeout duration was not set in the request's builder, then the
* {@code Optional} is empty.
*
* @return an {@code Optional} containing this request's timeout duration
*/
public abstract Optional<Duration> timeout();
/**
* Returns this request's {@linkplain HttpRequest.Builder#expectContinue(boolean)
* expect continue} setting.
*
* @return this request's expect continue setting
*/
public abstract boolean expectContinue();
/**
* Returns this request's {@code URI}.
*
* @return this request's URI
*/
public abstract URI uri();
/**
* Returns an {@code Optional} containing the HTTP protocol version that
* will be requested for this {@code HttpRequest}. If the version was not
* set in the request's builder, then the {@code Optional} is empty.
* In that case, the version requested will be that of the sending
* {@link HttpClient}. The corresponding {@link HttpResponse} should be
* queried to determine the version that was actually used.
*
* @return HTTP protocol version
*/
public abstract Optional<HttpClient.Version> version();
/**
* The (user-accessible) request headers that this request was (or will be)
* sent with.
*
* @return this request's HttpHeaders
*/
public abstract HttpHeaders headers();
/**
* Tests this HTTP request instance for equality with the given object.
*
* <p> If the given object is not an {@code HttpRequest} then this
* method returns {@code false}. Two HTTP requests are equal if their URI,
* method, and headers fields are all equal.
*
* <p> This method satisfies the general contract of the {@link
* Object#equals(Object) Object.equals} method.
*
* @param obj the object to which this object is to be compared
* @return {@code true} if, and only if, the given object is an {@code
* HttpRequest} that is equal to this HTTP request
*/
@Override
public final boolean equals(Object obj) {
if (! (obj instanceof HttpRequest))
return false;
HttpRequest that = (HttpRequest)obj;
if (!that.method().equals(this.method()))
return false;
if (!that.uri().equals(this.uri()))
return false;
if (!that.headers().equals(this.headers()))
return false;
return true;
}
/**
* Computes a hash code for this HTTP request instance.
*
* <p> The hash code is based upon the HTTP request's URI, method, and
* header components, and satisfies the general contract of the
* {@link Object#hashCode Object.hashCode} method.
*
* @return the hash-code value for this HTTP request
*/
public final int hashCode() {
return method().hashCode()
+ uri().hashCode()
+ headers().hashCode();
}
/**
* A {@code BodyPublisher} converts high-level Java objects into a flow of
* byte buffers suitable for sending as a request body. The class
* {@link BodyPublishers BodyPublishers} provides implementations of many
* common publishers.
*
* <p> The {@code BodyPublisher} interface extends {@link Flow.Publisher
* Flow.Publisher&lt;ByteBuffer&gt;}, which means that a {@code BodyPublisher}
* acts as a publisher of {@linkplain ByteBuffer byte buffers}.
*
* <p> When sending a request that contains a body, the HTTP Client
* subscribes to the request's {@code BodyPublisher} in order to receive the
* flow of outgoing request body data. The normal semantics of {@link
* Flow.Subscriber} and {@link Flow.Publisher} are implemented by the HTTP
* Client and are expected from {@code BodyPublisher} implementations. Each
* outgoing request results in one HTTP Client {@code Subscriber}
* subscribing to the {@code BodyPublisher} in order to provide the sequence
* of byte buffers containing the request body. Instances of {@code
* ByteBuffer} published by the publisher must be allocated by the
* publisher, and must not be accessed after being published to the HTTP
* Client. These subscriptions complete normally when the request body is
* fully sent, and can be canceled or terminated early through error. If a
* request needs to be resent for any reason, then a new subscription is
* created which is expected to generate the same data as before.
*
* <p> A {@code BodyPublisher} that reports a {@linkplain #contentLength()
* content length} of {@code 0} may not be subscribed to by the HTTP Client,
* as it has effectively no data to publish.
*
* @see BodyPublishers
* @since 11
*/
public interface BodyPublisher extends Flow.Publisher<ByteBuffer> {
/**
* Returns the content length for this request body. May be zero
* if no request body being sent, greater than zero for a fixed
* length content, or less than zero for an unknown content length.
*
* <p> This method may be invoked before the publisher is subscribed to.
* This method may be invoked more than once by the HTTP client
* implementation, and MUST return the same constant value each time.
*
* @return the content length for this request body, if known
*/
long contentLength();
}
/**
* Implementations of {@link BodyPublisher BodyPublisher} that implement
* various useful publishers, such as publishing the request body from a
* String, or from a file.
*
* <p> The following are examples of using the predefined body publishers to
* convert common high-level Java objects into a flow of data suitable for
* sending as a request body:
*
* <pre>{@code // Request body from a String
* HttpRequest request = HttpRequest.newBuilder()
* .uri(URI.create("https://foo.com/"))
* .header("Content-Type", "text/plain; charset=UTF-8")
* .POST(BodyPublishers.ofString("some body text"))
* .build();
*
* // Request body from a File
* HttpRequest request = HttpRequest.newBuilder()
* .uri(URI.create("https://foo.com/"))
* .header("Content-Type", "application/json")
* .POST(BodyPublishers.ofFile(Paths.get("file.json")))
* .build();
*
* // Request body from a byte array
* HttpRequest request = HttpRequest.newBuilder()
* .uri(URI.create("https://foo.com/"))
* .POST(BodyPublishers.ofByteArray(new byte[] { ... }))
* .build(); }</pre>
*
* @since 11
*/
public static class BodyPublishers {
private BodyPublishers() { }
/**
* Returns a request body publisher whose body is retrieved from the
* given {@code Flow.Publisher}. The returned request body publisher
* has an unknown content length.
*
* @apiNote This method can be used as an adapter between {@code
* BodyPublisher} and {@code Flow.Publisher}, where the amount of
* request body that the publisher will publish is unknown.
*
* @param publisher the publisher responsible for publishing the body
* @return a BodyPublisher
*/
public static BodyPublisher
fromPublisher(Flow.Publisher<? extends ByteBuffer> publisher) {
return new RequestPublishers.PublisherAdapter(publisher, -1L);
}
/**
* Returns a request body publisher whose body is retrieved from the
* given {@code Flow.Publisher}. The returned request body publisher
* has the given content length.
*
* <p> The given {@code contentLength} is a positive number, that
* represents the exact amount of bytes the {@code publisher} must
* publish.
*
* @apiNote This method can be used as an adapter between {@code
* BodyPublisher} and {@code Flow.Publisher}, where the amount of
* request body that the publisher will publish is known.
*
* @param publisher the publisher responsible for publishing the body
* @param contentLength a positive number representing the exact
* amount of bytes the publisher will publish
* @throws IllegalArgumentException if the content length is
* non-positive
* @return a BodyPublisher
*/
public static BodyPublisher
fromPublisher(Flow.Publisher<? extends ByteBuffer> publisher,
long contentLength) {
if (contentLength < 1)
throw new IllegalArgumentException("non-positive contentLength: "
+ contentLength);
return new RequestPublishers.PublisherAdapter(publisher, contentLength);
}
/**
* Returns a request body publisher whose body is the given {@code
* String}, converted using the {@link StandardCharsets#UTF_8 UTF_8}
* character set.
*
* @param body the String containing the body
* @return a BodyPublisher
*/
public static BodyPublisher ofString(String body) {
return ofString(body, UTF_8);
}
/**
* Returns a request body publisher whose body is the given {@code
* String}, converted using the given character set.
*
* @param s the String containing the body
* @param charset the character set to convert the string to bytes
* @return a BodyPublisher
*/
public static BodyPublisher ofString(String s, Charset charset) {
return new RequestPublishers.StringPublisher(s, charset);
}
/**
* A request body publisher that reads its data from an {@link
* InputStream}. A {@link Supplier} of {@code InputStream} is used in
* case the request needs to be repeated, as the content is not buffered.
* The {@code Supplier} may return {@code null} on subsequent attempts,
* in which case the request fails.
*
* @param streamSupplier a Supplier of open InputStreams
* @return a BodyPublisher
*/
// TODO (spec): specify that the stream will be closed
public static BodyPublisher ofInputStream(Supplier<? extends InputStream> streamSupplier) {
return new RequestPublishers.InputStreamPublisher(streamSupplier);
}
/**
* Returns a request body publisher whose body is the given byte array.
*
* @param buf the byte array containing the body
* @return a BodyPublisher
*/
public static BodyPublisher ofByteArray(byte[] buf) {
return new RequestPublishers.ByteArrayPublisher(buf);
}
/**
* Returns a request body publisher whose body is the content of the
* given byte array of {@code length} bytes starting from the specified
* {@code offset}.
*
* @param buf the byte array containing the body
* @param offset the offset of the first byte
* @param length the number of bytes to use
* @return a BodyPublisher
* @throws IndexOutOfBoundsException if the sub-range is defined to be
* out-of-bounds
*/
public static BodyPublisher ofByteArray(byte[] buf, int offset, int length) {
Objects.checkFromIndexSize(offset, length, buf.length);
return new RequestPublishers.ByteArrayPublisher(buf, offset, length);
}
/**
* A request body publisher that takes data from the contents of a File.
*
* <p> Security manager permission checks are performed in this factory
* method, when the {@code BodyPublisher} is created. Care must be taken
* that the {@code BodyPublisher} is not shared with untrusted code.
*
* @param path the path to the file containing the body
* @return a BodyPublisher
* @throws java.io.FileNotFoundException if the path is not found
* @throws SecurityException if a security manager has been installed
* and it denies {@link SecurityManager#checkRead(String)
* read access} to the given file
*/
public static BodyPublisher ofFile(Path path) throws FileNotFoundException {
Objects.requireNonNull(path);
return RequestPublishers.FilePublisher.create(path);
}
/**
* A request body publisher that takes data from an {@code Iterable}
* of byte arrays. An {@link Iterable} is provided which supplies
* {@link Iterator} instances. Each attempt to send the request results
* in one invocation of the {@code Iterable}.
*
* @param iter an Iterable of byte arrays
* @return a BodyPublisher
*/
public static BodyPublisher ofByteArrays(Iterable<byte[]> iter) {
return new RequestPublishers.IterablePublisher(iter);
}
/**
* A request body publisher which sends no request body.
*
* @return a BodyPublisher which completes immediately and sends
* no request body.
*/
public static BodyPublisher noBody() {
return new RequestPublishers.EmptyPublisher();
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2015, 2018, 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 java.net.http;
import java.io.IOException;
/**
* Thrown when a response is not received within a specified time period.
*
* @since 11
*/
public class HttpTimeoutException extends IOException {
private static final long serialVersionUID = 981344271622632951L;
/**
* Constructs an {@code HttpTimeoutException} with the given detail message.
*
* @param message
* The detail message; can be {@code null}
*/
public HttpTimeoutException(String message) {
super(message);
}
}

View file

@ -0,0 +1,771 @@
/*
* Copyright (c) 2015, 2018, 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 java.net.http;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
/**
* A WebSocket Client.
*
* <p> {@code WebSocket} instances can be created via {@link WebSocket.Builder}.
*
* <p> WebSocket has an input and an output sides. These sides are independent
* from each other. A side can either be open or closed. Once closed, the side
* remains closed. WebSocket messages are sent through a {@code WebSocket} and
* received through a {@code WebSocket.Listener} associated with it. Messages
* can be sent until the WebSocket's output is closed, and received until the
* WebSocket's input is closed.
*
* <p> A send method is any of the {@code sendText}, {@code sendBinary},
* {@code sendPing}, {@code sendPong} and {@code sendClose} methods of
* {@code WebSocket}. A send method initiates a send operation and returns a
* {@code CompletableFuture} which completes once the operation has completed.
* If the {@code CompletableFuture} completes normally the operation is
* considered succeeded. If the {@code CompletableFuture} completes
* exceptionally, the operation is considered failed. An operation that has been
* initiated but not yet completed is considered pending.
*
* <p> A receive method is any of the {@code onText}, {@code onBinary},
* {@code onPing}, {@code onPong} and {@code onClose} methods of
* {@code Listener}. A receive method initiates a receive operation and returns
* a {@code CompletionStage} which completes once the operation has completed.
*
* <p> A WebSocket maintains an <a id="counter">internal counter</a>.
* This counter's value is a number of times the WebSocket has yet to invoke a
* receive method. While this counter is zero the WebSocket does not invoke
* receive methods. The counter is incremented by {@code n} when {@code
* request(n)} is called. The counter is decremented by one when the WebSocket
* invokes a receive method. {@code onOpen} and {@code onError} are not receive
* methods. WebSocket invokes {@code onOpen} prior to any other methods on the
* listener. WebSocket invokes {@code onOpen} at most once. WebSocket may invoke
* {@code onError} at any given time. If the WebSocket invokes {@code onError}
* or {@code onClose}, then no further listener's methods will be invoked, no
* matter the value of the counter. For a newly built WebSocket the counter is
* zero. A WebSocket invokes methods on the listener in a thread-safe manner.
*
* <p> Unless otherwise stated, {@code null} arguments will cause methods
* of {@code WebSocket} to throw {@code NullPointerException}, similarly,
* {@code WebSocket} will not pass {@code null} arguments to methods of
* {@code Listener}. The state of a WebSocket is not changed by the invocations
* that throw or return a {@code CompletableFuture} that completes with one of
* the {@code NullPointerException}, {@code IllegalArgumentException},
* {@code IllegalStateException} exceptions.
*
* <p> {@code WebSocket} handles received Ping and Close messages automatically
* (as per the WebSocket Protocol) by replying with Pong and Close messages. If
* the listener receives Ping or Close messages, no mandatory actions from the
* listener are required.
*
* @apiNote The relationship between a WebSocket and the associated Listener is
* analogous to that of a Subscription and the associated Subscriber of type
* {@link java.util.concurrent.Flow}.
*
* @since 11
*/
public interface WebSocket {
/**
* The WebSocket Close message status code (<code>{@value}</code>),
* indicating normal closure, meaning that the purpose for which the
* connection was established has been fulfilled.
*
* @see #sendClose(int, String)
* @see Listener#onClose(WebSocket, int, String)
*/
int NORMAL_CLOSURE = 1000;
/**
* A builder of {@linkplain WebSocket WebSocket Clients}.
*
* <p> A builder can be created by invoking the
* {@link HttpClient#newWebSocketBuilder HttpClient.newWebSocketBuilder}
* method. The intermediate (setter-like) methods change the state of the
* builder and return the same builder they have been invoked on. If an
* intermediate method is not invoked, an appropriate default value (or
* behavior) will be assumed. A {@code Builder} is not safe for use by
* multiple threads without external synchronization.
*
* @since 11
*/
interface Builder {
/**
* Adds the given name-value pair to the list of additional HTTP headers
* sent during the opening handshake.
*
* <p> Headers defined in the
* <a href="https://tools.ietf.org/html/rfc6455#section-11.3">WebSocket
* Protocol</a> are illegal. If this method is not invoked, no
* additional HTTP headers will be sent.
*
* @param name
* the header name
* @param value
* the header value
*
* @return this builder
*/
Builder header(String name, String value);
/**
* Sets a timeout for establishing a WebSocket connection.
*
* <p> If the connection is not established within the specified
* duration then building of the {@code WebSocket} will fail with
* {@link HttpTimeoutException}. If this method is not invoked then the
* infinite timeout is assumed.
*
* @param timeout
* the timeout, non-{@linkplain Duration#isNegative() negative},
* non-{@linkplain Duration#ZERO ZERO}
*
* @return this builder
*/
Builder connectTimeout(Duration timeout);
/**
* Sets a request for the given subprotocols.
*
* <p> After the {@code WebSocket} has been built, the actual
* subprotocol can be queried via
* {@link WebSocket#getSubprotocol WebSocket.getSubprotocol()}.
*
* <p> Subprotocols are specified in the order of preference. The most
* preferred subprotocol is specified first. If there are any additional
* subprotocols they are enumerated from the most preferred to the least
* preferred.
*
* <p> Subprotocols not conforming to the syntax of subprotocol
* identifiers are illegal. If this method is not invoked then no
* subprotocols will be requested.
*
* @param mostPreferred
* the most preferred subprotocol
* @param lesserPreferred
* the lesser preferred subprotocols
*
* @return this builder
*/
Builder subprotocols(String mostPreferred, String... lesserPreferred);
/**
* Builds a {@link WebSocket} connected to the given {@code URI} and
* associated with the given {@code Listener}.
*
* <p> Returns a {@code CompletableFuture} which will either complete
* normally with the resulting {@code WebSocket} or complete
* exceptionally with one of the following errors:
* <ul>
* <li> {@link IOException} -
* if an I/O error occurs
* <li> {@link WebSocketHandshakeException} -
* if the opening handshake fails
* <li> {@link HttpTimeoutException} -
* if the opening handshake does not complete within
* the timeout
* <li> {@link InterruptedException} -
* if the operation is interrupted
* <li> {@link SecurityException} -
* if a security manager has been installed and it denies
* {@link java.net.URLPermission access} to {@code uri}.
* <a href="HttpRequest.html#securitychecks">Security checks</a>
* contains more information relating to the security context
* in which the the listener is invoked.
* <li> {@link IllegalArgumentException} -
* if any of the arguments of this builder's methods are
* illegal
* </ul>
*
* @param uri
* the WebSocket URI
* @param listener
* the listener
*
* @return a {@code CompletableFuture} with the {@code WebSocket}
*/
CompletableFuture<WebSocket> buildAsync(URI uri, Listener listener);
}
/**
* The receiving interface of {@code WebSocket}.
*
* <p> A {@code WebSocket} invokes methods of the associated listener
* passing itself as an argument. When data has been received, the
* {@code WebSocket} invokes a receive method. Methods {@code onText},
* {@code onBinary}, {@code onPing} and {@code onPong} must return a
* {@code CompletionStage} that completes once the message has been received
* by the listener.
*
* <p> An {@code IOException} raised in {@code WebSocket} will result in an
* invocation of {@code onError} with that exception (if the input is not
* closed). Unless otherwise stated if the listener's method throws an
* exception or a {@code CompletionStage} returned from a method completes
* exceptionally, the WebSocket will invoke {@code onError} with this
* exception.
*
* <p> If a listener's method returns {@code null} rather than a
* {@code CompletionStage}, {@code WebSocket} will behave as if the listener
* returned a {@code CompletionStage} that is already completed normally.
*
* @apiNote Careful attention may be required if a listener is associated
* with more than a single {@code WebSocket}. In this case invocations
* related to different instances of {@code WebSocket} may not be ordered
* and may even happen concurrently.
*
* <p> {@code CompletionStage}s returned from the receive methods have
* nothing to do with the
* <a href="WebSocket.html#counter">counter of invocations</a>.
* Namely, a {@code CompletionStage} does not have to be completed in order
* to receive more invocations of the listener's methods.
* Here is an example of a listener that requests invocations, one at a
* time, until a complete message has been accumulated, then processes
* the result, and completes the {@code CompletionStage}:
* <pre>{@code WebSocket.Listener listener = new WebSocket.Listener() {
*
* List<CharSequence> parts = new ArrayList<>();
* CompletableFuture<?> accumulatedMessage = new CompletableFuture<>();
*
* public CompletionStage<?> onText(WebSocket webSocket,
* CharSequence message,
* boolean last) {
* parts.add(message);
* webSocket.request(1);
* if (last) {
* processWholeText(parts);
* parts = new ArrayList<>();
* accumulatedMessage.complete(null);
* CompletionStage<?> cf = accumulatedMessage;
* accumulatedMessage = new CompletableFuture<>();
* return cf;
* }
* return accumulatedMessage;
* }
* ...
* } } </pre>
*
* @since 11
*/
interface Listener {
/**
* A {@code WebSocket} has been connected.
*
* <p> This is the initial invocation and it is made once. It is
* typically used to make a request for more invocations.
*
* @implSpec The default implementation is equivalent to:
* <pre>{@code webSocket.request(1); }</pre>
*
* @param webSocket
* the WebSocket that has been connected
*/
default void onOpen(WebSocket webSocket) { webSocket.request(1); }
/**
* A textual data has been received.
*
* <p> Return a {@code CompletionStage} which will be used by the
* {@code WebSocket} as an indication it may reclaim the
* {@code CharSequence}. Do not access the {@code CharSequence} after
* this {@code CompletionStage} has completed.
*
* @implSpec The default implementation is equivalent to:
* <pre>{@code webSocket.request(1);
* return null; }</pre>
*
* @implNote The {@code data} is always a legal UTF-16 sequence.
*
* @param webSocket
* the WebSocket on which the data has been received
* @param data
* the data
* @param last
* whether this invocation completes the message
*
* @return a {@code CompletionStage} which completes when the
* {@code CharSequence} may be reclaimed; or {@code null} if it may be
* reclaimed immediately
*/
default CompletionStage<?> onText(WebSocket webSocket,
CharSequence data,
boolean last) {
webSocket.request(1);
return null;
}
/**
* A binary data has been received.
*
* <p> This data is located in bytes from the buffer's position to its
* limit.
*
* <p> Return a {@code CompletionStage} which will be used by the
* {@code WebSocket} as an indication it may reclaim the
* {@code ByteBuffer}. Do not access the {@code ByteBuffer} after
* this {@code CompletionStage} has completed.
*
* @implSpec The default implementation is equivalent to:
* <pre>{@code webSocket.request(1);
* return null; }</pre>
*
* @param webSocket
* the WebSocket on which the data has been received
* @param data
* the data
* @param last
* whether this invocation completes the message
*
* @return a {@code CompletionStage} which completes when the
* {@code ByteBuffer} may be reclaimed; or {@code null} if it may be
* reclaimed immediately
*/
default CompletionStage<?> onBinary(WebSocket webSocket,
ByteBuffer data,
boolean last) {
webSocket.request(1);
return null;
}
/**
* A Ping message has been received.
*
* <p> As guaranteed by the WebSocket Protocol, the message consists of
* not more than {@code 125} bytes. These bytes are located from the
* buffer's position to its limit.
*
* <p> Given that the WebSocket implementation will automatically send a
* reciprocal pong when a ping is received, it is rarely required to
* send a pong message explicitly when a ping is received.
*
* <p> Return a {@code CompletionStage} which will be used by the
* {@code WebSocket} as a signal it may reclaim the
* {@code ByteBuffer}. Do not access the {@code ByteBuffer} after
* this {@code CompletionStage} has completed.
*
* @implSpec The default implementation is equivalent to:
* <pre>{@code webSocket.request(1);
* return null; }</pre>
*
* @param webSocket
* the WebSocket on which the message has been received
* @param message
* the message
*
* @return a {@code CompletionStage} which completes when the
* {@code ByteBuffer} may be reclaimed; or {@code null} if it may be
* reclaimed immediately
*/
default CompletionStage<?> onPing(WebSocket webSocket,
ByteBuffer message) {
webSocket.request(1);
return null;
}
/**
* A Pong message has been received.
*
* <p> As guaranteed by the WebSocket Protocol, the message consists of
* not more than {@code 125} bytes. These bytes are located from the
* buffer's position to its limit.
*
* <p> Return a {@code CompletionStage} which will be used by the
* {@code WebSocket} as a signal it may reclaim the
* {@code ByteBuffer}. Do not access the {@code ByteBuffer} after
* this {@code CompletionStage} has completed.
*
* @implSpec The default implementation is equivalent to:
* <pre>{@code webSocket.request(1);
* return null; }</pre>
*
* @param webSocket
* the WebSocket on which the message has been received
* @param message
* the message
*
* @return a {@code CompletionStage} which completes when the
* {@code ByteBuffer} may be reclaimed; or {@code null} if it may be
* reclaimed immediately
*/
default CompletionStage<?> onPong(WebSocket webSocket,
ByteBuffer message) {
webSocket.request(1);
return null;
}
/**
* Receives a Close message indicating the WebSocket's input has been
* closed.
*
* <p> This is the last invocation from the specified {@code WebSocket}.
* By the time this invocation begins the WebSocket's input will have
* been closed.
*
* <p> A Close message consists of a status code and a reason for
* closing. The status code is an integer from the range
* {@code 1000 <= code <= 65535}. The {@code reason} is a string which
* has a UTF-8 representation not longer than {@code 123} bytes.
*
* <p> If the WebSocket's output is not already closed, the
* {@code CompletionStage} returned by this method will be used as an
* indication that the WebSocket's output may be closed. The WebSocket
* will close its output at the earliest of completion of the returned
* {@code CompletionStage} or invoking either of the {@code sendClose}
* or {@code abort} methods.
*
* @apiNote Returning a {@code CompletionStage} that never completes,
* effectively disables the reciprocating closure of the output.
*
* <p> To specify a custom closure code or reason code the
* {@code sendClose} method may be invoked from inside the
* {@code onClose} invocation:
* <pre>{@code public CompletionStage<?> onClose(WebSocket webSocket,
* int statusCode,
* String reason) {
* webSocket.sendClose(CUSTOM_STATUS_CODE, CUSTOM_REASON);
* return new CompletableFuture<Void>();
* } } </pre>
*
* @implSpec The default implementation of this method returns
* {@code null}, indicating that the output should be closed
* immediately.
*
* @param webSocket
* the WebSocket on which the message has been received
* @param statusCode
* the status code
* @param reason
* the reason
*
* @return a {@code CompletionStage} which completes when the
* {@code WebSocket} may be closed; or {@code null} if it may be
* closed immediately
*/
default CompletionStage<?> onClose(WebSocket webSocket,
int statusCode,
String reason) {
return null;
}
/**
* An error has occurred.
*
* <p> This is the last invocation from the specified WebSocket. By the
* time this invocation begins both the WebSocket's input and output
* will have been closed. A WebSocket may invoke this method on the
* associated listener at any time after it has invoked {@code onOpen},
* regardless of whether or not any invocations have been requested from
* the WebSocket.
*
* <p> If an exception is thrown from this method, resulting behavior is
* undefined.
*
* @param webSocket
* the WebSocket on which the error has occurred
* @param error
* the error
*/
default void onError(WebSocket webSocket, Throwable error) { }
}
/**
* Sends textual data with characters from the given character sequence.
*
* <p> The character sequence must not be modified until the
* {@code CompletableFuture} returned from this method has completed.
*
* <p> A {@code CompletableFuture} returned from this method can
* complete exceptionally with:
* <ul>
* <li> {@link IllegalStateException} -
* if there is a pending text or binary send operation
* or if the previous binary data does not complete the message
* <li> {@link IOException} -
* if an I/O error occurs, or if the output is closed
* </ul>
*
* @implNote If {@code data} is a malformed UTF-16 sequence, the operation
* will fail with {@code IOException}.
*
* @param data
* the data
* @param last
* {@code true} if this invocation completes the message,
* {@code false} otherwise
*
* @return a {@code CompletableFuture} that completes, with this WebSocket,
* when the data has been sent
*/
CompletableFuture<WebSocket> sendText(CharSequence data, boolean last);
/**
* Sends binary data with bytes from the given buffer.
*
* <p> The data is located in bytes from the buffer's position to its limit.
* Upon normal completion of a {@code CompletableFuture} returned from this
* method the buffer will have no remaining bytes. The buffer must not be
* accessed until after that.
*
* <p> The {@code CompletableFuture} returned from this method can
* complete exceptionally with:
* <ul>
* <li> {@link IllegalStateException} -
* if there is a pending text or binary send operation
* or if the previous textual data does not complete the message
* <li> {@link IOException} -
* if an I/O error occurs, or if the output is closed
* </ul>
*
* @param data
* the data
* @param last
* {@code true} if this invocation completes the message,
* {@code false} otherwise
*
* @return a {@code CompletableFuture} that completes, with this WebSocket,
* when the data has been sent
*/
CompletableFuture<WebSocket> sendBinary(ByteBuffer data, boolean last);
/**
* Sends a Ping message with bytes from the given buffer.
*
* <p> The message consists of not more than {@code 125} bytes from the
* buffer's position to its limit. Upon normal completion of a
* {@code CompletableFuture} returned from this method the buffer will
* have no remaining bytes. The buffer must not be accessed until after that.
*
* <p> The {@code CompletableFuture} returned from this method can
* complete exceptionally with:
* <ul>
* <li> {@link IllegalStateException} -
* if there is a pending ping or pong send operation
* <li> {@link IllegalArgumentException} -
* if the message is too long
* <li> {@link IOException} -
* if an I/O error occurs, or if the output is closed
* </ul>
*
* @param message
* the message
*
* @return a {@code CompletableFuture} that completes, with this WebSocket,
* when the Ping message has been sent
*/
CompletableFuture<WebSocket> sendPing(ByteBuffer message);
/**
* Sends a Pong message with bytes from the given buffer.
*
* <p> The message consists of not more than {@code 125} bytes from the
* buffer's position to its limit. Upon normal completion of a
* {@code CompletableFuture} returned from this method the buffer will have
* no remaining bytes. The buffer must not be accessed until after that.
*
* <p> Given that the WebSocket implementation will automatically send a
* reciprocal pong when a ping is received, it is rarely required to send a
* pong message explicitly.
*
* <p> The {@code CompletableFuture} returned from this method can
* complete exceptionally with:
* <ul>
* <li> {@link IllegalStateException} -
* if there is a pending ping or pong send operation
* <li> {@link IllegalArgumentException} -
* if the message is too long
* <li> {@link IOException} -
* if an I/O error occurs, or if the output is closed
* </ul>
*
* @param message
* the message
*
* @return a {@code CompletableFuture} that completes, with this WebSocket,
* when the Pong message has been sent
*/
CompletableFuture<WebSocket> sendPong(ByteBuffer message);
/**
* Initiates an orderly closure of this WebSocket's output by
* sending a Close message with the given status code and the reason.
*
* <p> The {@code statusCode} is an integer from the range
* {@code 1000 <= code <= 4999}. Status codes {@code 1002}, {@code 1003},
* {@code 1006}, {@code 1007}, {@code 1009}, {@code 1010}, {@code 1012},
* {@code 1013} and {@code 1015} are illegal. Behaviour in respect to other
* status codes is implementation-specific. A legal {@code reason} is a
* string that has a UTF-8 representation not longer than {@code 123} bytes.
*
* <p> A {@code CompletableFuture} returned from this method can
* complete exceptionally with:
* <ul>
* <li> {@link IllegalArgumentException} -
* if {@code statusCode} is illegal, or
* if {@code reason} is illegal
* <li> {@link IOException} -
* if an I/O error occurs, or if the output is closed
* </ul>
*
* <p> Unless the {@code CompletableFuture} returned from this method
* completes with {@code IllegalArgumentException}, or the method throws
* {@code NullPointerException}, the output will be closed.
*
* <p> If not already closed, the input remains open until a Close message
* {@linkplain Listener#onClose(WebSocket, int, String) received}, or
* {@code abort} is invoked, or an
* {@linkplain Listener#onError(WebSocket, Throwable) error} occurs.
*
* @apiNote Use the provided integer constant {@link #NORMAL_CLOSURE} as a
* status code and an empty string as a reason in a typical case:
* <pre>{@code CompletableFuture<WebSocket> webSocket = ...
* webSocket.thenCompose(ws -> ws.sendText("Hello, ", false))
* .thenCompose(ws -> ws.sendText("world!", true))
* .thenCompose(ws -> ws.sendClose(WebSocket.NORMAL_CLOSURE, ""))
* .join(); }</pre>
*
* The {@code sendClose} method does not close this WebSocket's input. It
* merely closes this WebSocket's output by sending a Close message. To
* enforce closing the input, invoke the {@code abort} method. Here is an
* example of an application that sends a Close message, and then starts a
* timer. Once no data has been received within the specified timeout, the
* timer goes off and the alarm aborts {@code WebSocket}:
* <pre>{@code MyAlarm alarm = new MyAlarm(webSocket::abort);
* WebSocket.Listener listener = new WebSocket.Listener() {
*
* public CompletionStage<?> onText(WebSocket webSocket,
* CharSequence data,
* boolean last) {
* alarm.snooze();
* ...
* }
* ...
* };
* ...
* Runnable startTimer = () -> {
* MyTimer idleTimer = new MyTimer();
* idleTimer.add(alarm, 30, TimeUnit.SECONDS);
* };
* webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok").thenRun(startTimer);
* } </pre>
*
* @param statusCode
* the status code
* @param reason
* the reason
*
* @return a {@code CompletableFuture} that completes, with this WebSocket,
* when the Close message has been sent
*/
CompletableFuture<WebSocket> sendClose(int statusCode, String reason);
/**
* Increments the counter of invocations of receive methods.
*
* <p> This WebSocket will invoke {@code onText}, {@code onBinary},
* {@code onPing}, {@code onPong} or {@code onClose} methods on the
* associated listener (i.e. receive methods) up to {@code n} more times.
*
* @apiNote The parameter of this method is the number of invocations being
* requested from this WebSocket to the associated listener, not the number
* of messages. Sometimes a message may be delivered to the listener in a
* single invocation, but not always. For example, Ping, Pong and Close
* messages are delivered in a single invocation of {@code onPing},
* {@code onPong} and {@code onClose} methods respectively. However, whether
* or not Text and Binary messages are delivered in a single invocation of
* {@code onText} and {@code onBinary} methods depends on the boolean
* argument ({@code last}) of these methods. If {@code last} is
* {@code false}, then there is more to a message than has been delivered to
* the invocation.
*
* <p> Here is an example of a listener that requests invocations, one at a
* time, until a complete message has been accumulated, and then processes
* the result:
* <pre>{@code WebSocket.Listener listener = new WebSocket.Listener() {
*
* StringBuilder text = new StringBuilder();
*
* public CompletionStage<?> onText(WebSocket webSocket,
* CharSequence message,
* boolean last) {
* text.append(message);
* if (last) {
* processCompleteTextMessage(text);
* text = new StringBuilder();
* }
* webSocket.request(1);
* return null;
* }
* ...
* } } </pre>
*
* @param n
* the number of invocations
*
* @throws IllegalArgumentException
* if {@code n <= 0}
*/
void request(long n);
/**
* Returns the subprotocol used by this WebSocket.
*
* @return the subprotocol, or an empty string if there's no subprotocol
*/
String getSubprotocol();
/**
* Tells whether this WebSocket's output is closed.
*
* <p> If this method returns {@code true}, subsequent invocations will also
* return {@code true}.
*
* @return {@code true} if closed, {@code false} otherwise
*/
boolean isOutputClosed();
/**
* Tells whether this WebSocket's input is closed.
*
* <p> If this method returns {@code true}, subsequent invocations will also
* return {@code true}.
*
* @return {@code true} if closed, {@code false} otherwise
*/
boolean isInputClosed();
/**
* Closes this WebSocket's input and output abruptly.
*
* <p> When this method returns both the input and the output will have been
* closed. Any pending send operations will fail with {@code IOException}.
* Subsequent invocations of {@code abort} will have no effect.
*/
void abort();
}

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2015, 2018, 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 java.net.http;
import java.io.IOException;
/**
* An exception used to signal the opening handshake failed.
*
* @since 11
*/
public final class WebSocketHandshakeException extends IOException {
private static final long serialVersionUID = 1L;
private final transient HttpResponse<?> response;
/**
* Constructs a {@code WebSocketHandshakeException} with the given
* {@code HttpResponse}.
*
* @param response
* the {@code HttpResponse} that resulted in the handshake failure
*/
public WebSocketHandshakeException(HttpResponse<?> response) {
this.response = response;
}
/**
* Returns the server's counterpart of the opening handshake.
*
* <p> The value may be unavailable ({@code null}) if this exception has
* been serialized and then deserialized.
*
* @return server response
*/
public HttpResponse<?> getResponse() {
return response;
}
@Override
public WebSocketHandshakeException initCause(Throwable cause) {
return (WebSocketHandshakeException) super.initCause(cause);
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright (c) 2015, 2018, 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.
*/
/**
* <h2>HTTP Client and WebSocket APIs</h2>
*
* <p> Provides high-level client interfaces to HTTP (versions 1.1 and 2) and
* low-level client interfaces to WebSocket. The main types defined are:
*
* <ul>
* <li>{@link java.net.http.HttpClient}</li>
* <li>{@link java.net.http.HttpRequest}</li>
* <li>{@link java.net.http.HttpResponse}</li>
* <li>{@link java.net.http.WebSocket}</li>
* </ul>
*
* <p> The protocol-specific requirements are defined in the
* <a href="https://tools.ietf.org/html/rfc7540">Hypertext Transfer Protocol
* Version 2 (HTTP/2)</a>, the <a href="https://tools.ietf.org/html/rfc2616">
* Hypertext Transfer Protocol (HTTP/1.1)</a>, and
* <a href="https://tools.ietf.org/html/rfc6455">The WebSocket Protocol</a>.
*
* <p> Asynchronous tasks and dependent actions of returned {@link
* java.util.concurrent.CompletableFuture} instances are executed on the threads
* supplied by the client's {@link java.util.concurrent.Executor}, where
* practical.
*
* <p> {@code CompletableFuture}s returned by this API will throw {@link
* java.lang.UnsupportedOperationException} for their {@link
* java.util.concurrent.CompletableFuture#obtrudeValue(Object) obtrudeValue}
* and {@link java.util.concurrent.CompletableFuture#obtrudeException(Throwable)
* obtrudeException} methods. Invoking the {@link
* java.util.concurrent.CompletableFuture#cancel cancel} method on a {@code
* CompletableFuture} returned by this API will not interrupt the underlying
* operation, but may be useful to complete, exceptionally, dependent stages
* that have not already completed.
*
* <p> Unless otherwise stated, {@code null} parameter values will cause methods
* of all classes in this package to throw {@code NullPointerException}.
*
* @since 11
*/
package java.net.http;

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2015, 2018, 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.net.InetSocketAddress;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import jdk.internal.net.http.common.SSLTube;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Utils;
import static jdk.internal.net.http.common.Utils.ServerName;
/**
* Asynchronous version of SSLConnection.
*
* There are two concrete implementations of this class: AsyncSSLConnection
* and AsyncSSLTunnelConnection.
* This abstraction is useful when downgrading from HTTP/2 to HTTP/1.1 over
* an SSL connection. See ExchangeImpl::get in the case where an ALPNException
* is thrown.
*
* Note: An AsyncSSLConnection wraps a PlainHttpConnection, while an
* AsyncSSLTunnelConnection wraps a PlainTunnelingConnection.
* If both these wrapped classes where made to inherit from a
* common abstraction then it might be possible to merge
* AsyncSSLConnection and AsyncSSLTunnelConnection back into
* a single class - and simply use different factory methods to
* create different wrappees, but this is left up for further cleanup.
*
*/
abstract class AbstractAsyncSSLConnection extends HttpConnection
{
protected final SSLEngine engine;
protected final String serverName;
protected final SSLParameters sslParameters;
// Setting this property disables HTTPS hostname verification. Use with care.
private static final boolean disableHostnameVerification
= Utils.isHostnameVerificationDisabled();
AbstractAsyncSSLConnection(InetSocketAddress addr,
HttpClientImpl client,
ServerName serverName, int port,
String[] alpn) {
super(addr, client);
this.serverName = serverName.getName();
SSLContext context = client.theSSLContext();
sslParameters = createSSLParameters(client, serverName, alpn);
Log.logParams(sslParameters);
engine = createEngine(context, serverName.getName(), port, sslParameters);
}
abstract HttpConnection plainConnection();
abstract SSLTube getConnectionFlow();
final CompletableFuture<String> getALPN() {
assert connected();
return getConnectionFlow().getALPN();
}
final SSLEngine getEngine() { return engine; }
private static SSLParameters createSSLParameters(HttpClientImpl client,
ServerName serverName,
String[] alpn) {
SSLParameters sslp = client.sslParameters();
SSLParameters sslParameters = Utils.copySSLParameters(sslp);
if (!disableHostnameVerification)
sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
if (alpn != null) {
Log.logSSL("AbstractAsyncSSLConnection: Setting application protocols: {0}",
Arrays.toString(alpn));
sslParameters.setApplicationProtocols(alpn);
} else {
Log.logSSL("AbstractAsyncSSLConnection: no applications set!");
}
if (!serverName.isLiteral()) {
String name = serverName.getName();
if (name != null && name.length() > 0) {
sslParameters.setServerNames(List.of(new SNIHostName(name)));
}
}
return sslParameters;
}
private static SSLEngine createEngine(SSLContext context, String serverName, int port,
SSLParameters sslParameters) {
SSLEngine engine = context.createSSLEngine(serverName, port);
engine.setUseClientMode(true);
engine.setSSLParameters(sslParameters);
return engine;
}
@Override
final boolean isSecure() {
return true;
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2017, 2018, 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.util.concurrent.Flow;
import jdk.internal.net.http.common.Demand;
/**
* A {@link Flow.Subscription} wrapping a {@link Demand} instance.
*/
abstract class AbstractSubscription implements Flow.Subscription {
private final Demand demand = new Demand();
/**
* Returns the subscription's demand.
* @return the subscription's demand.
*/
protected Demand demand() { return demand; }
}

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2015, 2018, 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.nio.channels.SelectableChannel;
/**
* Event handling interface from HttpClientImpl's selector.
*
* If REPEATING is set then the event is not cancelled after being posted.
*/
abstract class AsyncEvent {
public static final int REPEATING = 0x2; // one off event if not set
protected final int flags;
AsyncEvent() {
this(0);
}
AsyncEvent(int flags) {
this.flags = flags;
}
/** Returns the channel */
public abstract SelectableChannel channel();
/** Returns the selector interest op flags OR'd */
public abstract int interestOps();
/** Called when event occurs */
public abstract void handle();
/**
* Called when an error occurs during registration, or when the selector has
* been shut down. Aborts all exchanges.
*
* @param ioe the IOException, or null
*/
public abstract void abort(IOException ioe);
public boolean repeating() {
return (flags & REPEATING) != 0;
}
}

View file

@ -0,0 +1,100 @@
/*
* Copyright (c) 2015, 2018, 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.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.concurrent.CompletableFuture;
import jdk.internal.net.http.common.SSLTube;
import jdk.internal.net.http.common.Utils;
/**
* Asynchronous version of SSLConnection.
*/
class AsyncSSLConnection extends AbstractAsyncSSLConnection {
final PlainHttpConnection plainConnection;
final PlainHttpPublisher writePublisher;
private volatile SSLTube flow;
AsyncSSLConnection(InetSocketAddress addr,
HttpClientImpl client,
String[] alpn) {
super(addr, client, Utils.getServerName(addr), addr.getPort(), alpn);
plainConnection = new PlainHttpConnection(addr, client);
writePublisher = new PlainHttpPublisher();
}
@Override
PlainHttpConnection plainConnection() {
return plainConnection;
}
@Override
public CompletableFuture<Void> connectAsync() {
return plainConnection
.connectAsync()
.thenApply( unused -> {
// create the SSLTube wrapping the SocketTube, with the given engine
flow = new SSLTube(engine,
client().theExecutor(),
plainConnection.getConnectionFlow());
return null; } );
}
@Override
boolean connected() {
return plainConnection.connected();
}
@Override
HttpPublisher publisher() { return writePublisher; }
@Override
boolean isProxied() {
return false;
}
@Override
SocketChannel channel() {
return plainConnection.channel();
}
@Override
ConnectionPool.CacheKey cacheKey() {
return ConnectionPool.cacheKey(address, null);
}
@Override
public void close() {
plainConnection.close();
}
@Override
SSLTube getConnectionFlow() {
return flow;
}
}

View file

@ -0,0 +1,117 @@
/*
* Copyright (c) 2015, 2018, 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.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.concurrent.CompletableFuture;
import java.net.http.HttpHeaders;
import jdk.internal.net.http.common.SSLTube;
import jdk.internal.net.http.common.Utils;
/**
* An SSL tunnel built on a Plain (CONNECT) TCP tunnel.
*/
class AsyncSSLTunnelConnection extends AbstractAsyncSSLConnection {
final PlainTunnelingConnection plainConnection;
final PlainHttpPublisher writePublisher;
volatile SSLTube flow;
AsyncSSLTunnelConnection(InetSocketAddress addr,
HttpClientImpl client,
String[] alpn,
InetSocketAddress proxy,
HttpHeaders proxyHeaders)
{
super(addr, client, Utils.getServerName(addr), addr.getPort(), alpn);
this.plainConnection = new PlainTunnelingConnection(addr, proxy, client, proxyHeaders);
this.writePublisher = new PlainHttpPublisher();
}
@Override
public CompletableFuture<Void> connectAsync() {
if (debug.on()) debug.log("Connecting plain tunnel connection");
// This will connect the PlainHttpConnection flow, so that
// its HttpSubscriber and HttpPublisher are subscribed to the
// SocketTube
return plainConnection
.connectAsync()
.thenApply( unused -> {
if (debug.on()) debug.log("creating SSLTube");
// create the SSLTube wrapping the SocketTube, with the given engine
flow = new SSLTube(engine,
client().theExecutor(),
plainConnection.getConnectionFlow());
return null;} );
}
@Override
boolean isTunnel() { return true; }
@Override
boolean connected() {
return plainConnection.connected(); // && sslDelegate.connected();
}
@Override
HttpPublisher publisher() { return writePublisher; }
@Override
public String toString() {
return "AsyncSSLTunnelConnection: " + super.toString();
}
@Override
PlainTunnelingConnection plainConnection() {
return plainConnection;
}
@Override
ConnectionPool.CacheKey cacheKey() {
return ConnectionPool.cacheKey(address, plainConnection.proxyAddr);
}
@Override
public void close() {
plainConnection.close();
}
@Override
SocketChannel channel() {
return plainConnection.channel();
}
@Override
boolean isProxied() {
return true;
}
@Override
SSLTube getConnectionFlow() {
return flow;
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2017, 2018, 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.nio.channels.SelectableChannel;
import java.util.Objects;
import java.util.function.Consumer;
/**
* An asynchronous event which is triggered only once from the selector manager
* thread as soon as event registration are handled.
*/
final class AsyncTriggerEvent extends AsyncEvent{
private final Runnable trigger;
private final Consumer<? super IOException> errorHandler;
AsyncTriggerEvent(Consumer<? super IOException> errorHandler,
Runnable trigger) {
super(0);
this.trigger = Objects.requireNonNull(trigger);
this.errorHandler = Objects.requireNonNull(errorHandler);
}
/** Returns null */
@Override
public SelectableChannel channel() { return null; }
/** Returns 0 */
@Override
public int interestOps() { return 0; }
@Override
public void handle() { trigger.run(); }
@Override
public void abort(IOException ioe) { errorHandler.accept(ioe); }
@Override
public boolean repeating() { return false; }
}

View file

@ -0,0 +1,440 @@
/*
* Copyright (c) 2015, 2018, 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.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.net.InetSocketAddress;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Base64;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.WeakHashMap;
import java.net.http.HttpHeaders;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Utils;
import static java.net.Authenticator.RequestorType.PROXY;
import static java.net.Authenticator.RequestorType.SERVER;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
/**
* Implementation of Http Basic authentication.
*/
class AuthenticationFilter implements HeaderFilter {
volatile MultiExchange<?> exchange;
private static final Base64.Encoder encoder = Base64.getEncoder();
static final int DEFAULT_RETRY_LIMIT = 3;
static final int retry_limit = Utils.getIntegerNetProperty(
"jdk.httpclient.auth.retrylimit", DEFAULT_RETRY_LIMIT);
static final int UNAUTHORIZED = 401;
static final int PROXY_UNAUTHORIZED = 407;
private static final List<String> BASIC_DUMMY =
List.of("Basic " + Base64.getEncoder()
.encodeToString("o:o".getBytes(ISO_8859_1)));
// A public no-arg constructor is required by FilterFactory
public AuthenticationFilter() {}
private PasswordAuthentication getCredentials(String header,
boolean proxy,
HttpRequestImpl req)
throws IOException
{
HttpClientImpl client = exchange.client();
java.net.Authenticator auth =
client.authenticator()
.orElseThrow(() -> new IOException("No authenticator set"));
URI uri = req.uri();
HeaderParser parser = new HeaderParser(header);
String authscheme = parser.findKey(0);
String realm = parser.findValue("realm");
java.net.Authenticator.RequestorType rtype = proxy ? PROXY : SERVER;
URL url = toURL(uri, req.method(), proxy);
String host;
int port;
String protocol;
InetSocketAddress proxyAddress;
if (proxy && (proxyAddress = req.proxy()) != null) {
// request sent to server through proxy
proxyAddress = req.proxy();
host = proxyAddress.getHostString();
port = proxyAddress.getPort();
protocol = "http"; // we don't support https connection to proxy
} else {
// direct connection to server or proxy
host = uri.getHost();
port = uri.getPort();
protocol = uri.getScheme();
}
// needs to be instance method in Authenticator
return auth.requestPasswordAuthenticationInstance(host,
null,
port,
protocol,
realm,
authscheme,
url,
rtype
);
}
private URL toURL(URI uri, String method, boolean proxy)
throws MalformedURLException
{
if (proxy && "CONNECT".equalsIgnoreCase(method)
&& "socket".equalsIgnoreCase(uri.getScheme())) {
return null; // proxy tunneling
}
return uri.toURL();
}
private URI getProxyURI(HttpRequestImpl r) {
InetSocketAddress proxy = r.proxy();
if (proxy == null) {
return null;
}
// our own private scheme for proxy URLs
// eg. proxy.http://host:port/
String scheme = "proxy." + r.uri().getScheme();
try {
return new URI(scheme,
null,
proxy.getHostString(),
proxy.getPort(),
"/",
null,
null);
} catch (URISyntaxException e) {
throw new InternalError(e);
}
}
@Override
public void request(HttpRequestImpl r, MultiExchange<?> e) throws IOException {
// use preemptive authentication if an entry exists.
Cache cache = getCache(e);
this.exchange = e;
// Proxy
if (exchange.proxyauth == null) {
URI proxyURI = getProxyURI(r);
if (proxyURI != null) {
CacheEntry ca = cache.get(proxyURI, true);
if (ca != null) {
exchange.proxyauth = new AuthInfo(true, ca.scheme, null, ca);
addBasicCredentials(r, true, ca.value);
}
}
}
// Server
if (exchange.serverauth == null) {
CacheEntry ca = cache.get(r.uri(), false);
if (ca != null) {
exchange.serverauth = new AuthInfo(true, ca.scheme, null, ca);
addBasicCredentials(r, false, ca.value);
}
}
}
// TODO: refactor into per auth scheme class
private static void addBasicCredentials(HttpRequestImpl r,
boolean proxy,
PasswordAuthentication pw) {
String hdrname = proxy ? "Proxy-Authorization" : "Authorization";
StringBuilder sb = new StringBuilder(128);
sb.append(pw.getUserName()).append(':').append(pw.getPassword());
String s = encoder.encodeToString(sb.toString().getBytes(ISO_8859_1));
String value = "Basic " + s;
if (proxy) {
if (r.isConnect()) {
if (!Utils.PROXY_TUNNEL_FILTER
.test(hdrname, List.of(value))) {
Log.logError("{0} disabled", hdrname);
return;
}
} else if (r.proxy() != null) {
if (!Utils.PROXY_FILTER
.test(hdrname, List.of(value))) {
Log.logError("{0} disabled", hdrname);
return;
}
}
}
r.setSystemHeader(hdrname, value);
}
// Information attached to a HttpRequestImpl relating to authentication
static class AuthInfo {
final boolean fromcache;
final String scheme;
int retries;
PasswordAuthentication credentials; // used in request
CacheEntry cacheEntry; // if used
AuthInfo(boolean fromcache,
String scheme,
PasswordAuthentication credentials) {
this.fromcache = fromcache;
this.scheme = scheme;
this.credentials = credentials;
this.retries = 1;
}
AuthInfo(boolean fromcache,
String scheme,
PasswordAuthentication credentials,
CacheEntry ca) {
this(fromcache, scheme, credentials);
assert credentials == null || (ca != null && ca.value == null);
cacheEntry = ca;
}
AuthInfo retryWithCredentials(PasswordAuthentication pw) {
// If the info was already in the cache we need to create a new
// instance with fromCache==false so that it's put back in the
// cache if authentication succeeds
AuthInfo res = fromcache ? new AuthInfo(false, scheme, pw) : this;
res.credentials = Objects.requireNonNull(pw);
res.retries = retries;
return res;
}
}
@Override
public HttpRequestImpl response(Response r) throws IOException {
Cache cache = getCache(exchange);
int status = r.statusCode();
HttpHeaders hdrs = r.headers();
HttpRequestImpl req = r.request();
if (status != UNAUTHORIZED && status != PROXY_UNAUTHORIZED) {
// check if any authentication succeeded for first time
if (exchange.serverauth != null && !exchange.serverauth.fromcache) {
AuthInfo au = exchange.serverauth;
cache.store(au.scheme, req.uri(), false, au.credentials);
}
if (exchange.proxyauth != null && !exchange.proxyauth.fromcache) {
AuthInfo au = exchange.proxyauth;
URI proxyURI = getProxyURI(req);
if (proxyURI != null) {
cache.store(au.scheme, proxyURI, true, au.credentials);
}
}
return null;
}
boolean proxy = status == PROXY_UNAUTHORIZED;
String authname = proxy ? "Proxy-Authenticate" : "WWW-Authenticate";
String authval = hdrs.firstValue(authname).orElseThrow(() -> {
return new IOException("Invalid auth header");
});
HeaderParser parser = new HeaderParser(authval);
String scheme = parser.findKey(0);
// TODO: Need to generalise from Basic only. Delegate to a provider class etc.
if (!scheme.equalsIgnoreCase("Basic")) {
return null; // error gets returned to app
}
if (proxy) {
if (r.isConnectResponse) {
if (!Utils.PROXY_TUNNEL_FILTER
.test("Proxy-Authorization", BASIC_DUMMY)) {
Log.logError("{0} disabled", "Proxy-Authorization");
return null;
}
} else if (req.proxy() != null) {
if (!Utils.PROXY_FILTER
.test("Proxy-Authorization", BASIC_DUMMY)) {
Log.logError("{0} disabled", "Proxy-Authorization");
return null;
}
}
}
AuthInfo au = proxy ? exchange.proxyauth : exchange.serverauth;
if (au == null) {
// if no authenticator, let the user deal with 407/401
if (!exchange.client().authenticator().isPresent()) return null;
PasswordAuthentication pw = getCredentials(authval, proxy, req);
if (pw == null) {
throw new IOException("No credentials provided");
}
// No authentication in request. Get credentials from user
au = new AuthInfo(false, "Basic", pw);
if (proxy) {
exchange.proxyauth = au;
} else {
exchange.serverauth = au;
}
req = HttpRequestImpl.newInstanceForAuthentication(req);
addBasicCredentials(req, proxy, pw);
return req;
} else if (au.retries > retry_limit) {
throw new IOException("too many authentication attempts. Limit: " +
Integer.toString(retry_limit));
} else {
// we sent credentials, but they were rejected
if (au.fromcache) {
cache.remove(au.cacheEntry);
}
// if no authenticator, let the user deal with 407/401
if (!exchange.client().authenticator().isPresent()) return null;
// try again
PasswordAuthentication pw = getCredentials(authval, proxy, req);
if (pw == null) {
throw new IOException("No credentials provided");
}
au = au.retryWithCredentials(pw);
if (proxy) {
exchange.proxyauth = au;
} else {
exchange.serverauth = au;
}
req = HttpRequestImpl.newInstanceForAuthentication(req);
addBasicCredentials(req, proxy, au.credentials);
au.retries++;
return req;
}
}
// Use a WeakHashMap to make it possible for the HttpClient to
// be garbage collected when no longer referenced.
static final WeakHashMap<HttpClientImpl,Cache> caches = new WeakHashMap<>();
static synchronized Cache getCache(MultiExchange<?> exchange) {
HttpClientImpl client = exchange.client();
Cache c = caches.get(client);
if (c == null) {
c = new Cache();
caches.put(client, c);
}
return c;
}
// Note: Make sure that Cache and CacheEntry do not keep any strong
// reference to the HttpClient: it would prevent the client being
// GC'ed when no longer referenced.
static final class Cache {
final LinkedList<CacheEntry> entries = new LinkedList<>();
Cache() {}
synchronized CacheEntry get(URI uri, boolean proxy) {
for (CacheEntry entry : entries) {
if (entry.equalsKey(uri, proxy)) {
return entry;
}
}
return null;
}
synchronized void remove(String authscheme, URI domain, boolean proxy) {
for (CacheEntry entry : entries) {
if (entry.equalsKey(domain, proxy)) {
entries.remove(entry);
}
}
}
synchronized void remove(CacheEntry entry) {
entries.remove(entry);
}
synchronized void store(String authscheme,
URI domain,
boolean proxy,
PasswordAuthentication value) {
remove(authscheme, domain, proxy);
entries.add(new CacheEntry(authscheme, domain, proxy, value));
}
}
static URI normalize(URI uri, boolean isPrimaryKey) {
String path = uri.getPath();
if (path == null || path.isEmpty()) {
// make sure the URI has a path, ignore query and fragment
try {
return new URI(uri.getScheme(), uri.getAuthority(), "/", null, null);
} catch (URISyntaxException e) {
throw new InternalError(e);
}
} else if (isPrimaryKey || !"/".equals(path)) {
// remove extraneous components and normalize path
return uri.resolve(".");
} else {
// path == "/" and the URI is not used to store
// the primary key in the cache: nothing to do.
return uri;
}
}
static final class CacheEntry {
final String root;
final String scheme;
final boolean proxy;
final PasswordAuthentication value;
CacheEntry(String authscheme,
URI uri,
boolean proxy,
PasswordAuthentication value) {
this.scheme = authscheme;
this.root = normalize(uri, true).toString(); // remove extraneous components
this.proxy = proxy;
this.value = value;
}
public PasswordAuthentication value() {
return value;
}
public boolean equalsKey(URI uri, boolean proxy) {
if (this.proxy != proxy) {
return false;
}
String other = String.valueOf(normalize(uri, false));
return other.startsWith(root);
}
}
}

View file

@ -0,0 +1,315 @@
/*
* Copyright (c) 2017, 2018, 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.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicBoolean;
import java.net.http.HttpResponse.BodySubscriber;
import jdk.internal.net.http.common.Demand;
import jdk.internal.net.http.common.SequentialScheduler;
import jdk.internal.net.http.common.Utils;
/**
* A buffering BodySubscriber. When subscribed, accumulates ( buffers ) a given
* amount ( in bytes ) of a publisher's data before pushing it to a downstream
* subscriber.
*/
public class BufferingSubscriber<T> implements BodySubscriber<T>
{
/** The downstream consumer of the data. */
private final BodySubscriber<T> downstreamSubscriber;
/** The amount of data to be accumulate before pushing downstream. */
private final int bufferSize;
/** The subscription, created lazily. */
private volatile Flow.Subscription subscription;
/** The downstream subscription, created lazily. */
private volatile DownstreamSubscription downstreamSubscription;
/** Must be held when accessing the internal buffers. */
private final Object buffersLock = new Object();
/** The internal buffers holding the buffered data. */
private ArrayList<ByteBuffer> internalBuffers;
/** The actual accumulated remaining bytes in internalBuffers. */
private int accumulatedBytes;
/** Holds the Throwable from upstream's onError. */
private volatile Throwable throwable;
/** State of the buffering subscriber:
* 1) [UNSUBSCRIBED] when initially created
* 2) [ACTIVE] when subscribed and can receive data
* 3) [ERROR | CANCELLED | COMPLETE] (terminal state)
*/
static final int UNSUBSCRIBED = 0x01;
static final int ACTIVE = 0x02;
static final int ERROR = 0x04;
static final int CANCELLED = 0x08;
static final int COMPLETE = 0x10;
private volatile int state;
public BufferingSubscriber(BodySubscriber<T> downstreamSubscriber,
int bufferSize) {
this.downstreamSubscriber = Objects.requireNonNull(downstreamSubscriber);
this.bufferSize = bufferSize;
synchronized (buffersLock) {
internalBuffers = new ArrayList<>();
}
state = UNSUBSCRIBED;
}
/** Returns the number of bytes remaining in the given buffers. */
private static final long remaining(List<ByteBuffer> buffers) {
return buffers.stream().mapToLong(ByteBuffer::remaining).sum();
}
/**
* Tells whether, or not, there is at least a sufficient number of bytes
* accumulated in the internal buffers. If the subscriber is COMPLETE, and
* has some buffered data, then there is always enough ( to pass downstream ).
*/
private final boolean hasEnoughAccumulatedBytes() {
assert Thread.holdsLock(buffersLock);
return accumulatedBytes >= bufferSize
|| (state == COMPLETE && accumulatedBytes > 0);
}
/**
* Returns a new, unmodifiable, List<ByteBuffer> containing exactly the
* amount of data as required before pushing downstream. The amount of data
* may be less than required ( bufferSize ), in the case where the subscriber
* is COMPLETE.
*/
private List<ByteBuffer> fromInternalBuffers() {
assert Thread.holdsLock(buffersLock);
int leftToFill = bufferSize;
int state = this.state;
assert (state == ACTIVE || state == CANCELLED)
? accumulatedBytes >= leftToFill : true;
List<ByteBuffer> dsts = new ArrayList<>();
ListIterator<ByteBuffer> itr = internalBuffers.listIterator();
while (itr.hasNext()) {
ByteBuffer b = itr.next();
if (b.remaining() <= leftToFill) {
itr.remove();
if (b.position() != 0)
b = b.slice(); // ensure position = 0 when propagated
dsts.add(b);
leftToFill -= b.remaining();
accumulatedBytes -= b.remaining();
if (leftToFill == 0)
break;
} else {
int prevLimit = b.limit();
b.limit(b.position() + leftToFill);
ByteBuffer slice = b.slice();
dsts.add(slice);
b.limit(prevLimit);
b.position(b.position() + leftToFill);
accumulatedBytes -= leftToFill;
leftToFill = 0;
break;
}
}
assert (state == ACTIVE || state == CANCELLED)
? leftToFill == 0 : state == COMPLETE;
assert (state == ACTIVE || state == CANCELLED)
? remaining(dsts) == bufferSize : state == COMPLETE;
assert accumulatedBytes >= 0;
assert dsts.stream().noneMatch(b -> b.position() != 0);
return Collections.unmodifiableList(dsts);
}
/** Subscription that is passed to the downstream subscriber. */
private class DownstreamSubscription implements Flow.Subscription {
private final AtomicBoolean cancelled = new AtomicBoolean(); // false
private final Demand demand = new Demand();
private volatile boolean illegalArg;
@Override
public void request(long n) {
if (cancelled.get() || illegalArg) {
return;
}
if (n <= 0L) {
// pass the "bad" value upstream so the Publisher can deal with
// it appropriately, i.e. invoke onError
illegalArg = true;
subscription.request(n);
return;
}
demand.increase(n);
pushDemanded();
}
private final SequentialScheduler pushDemandedScheduler =
new SequentialScheduler(new PushDemandedTask());
void pushDemanded() {
if (cancelled.get())
return;
pushDemandedScheduler.runOrSchedule();
}
class PushDemandedTask extends SequentialScheduler.CompleteRestartableTask {
@Override
public void run() {
try {
Throwable t = throwable;
if (t != null) {
pushDemandedScheduler.stop(); // stop the demand scheduler
downstreamSubscriber.onError(t);
return;
}
while (true) {
List<ByteBuffer> item;
synchronized (buffersLock) {
if (cancelled.get())
return;
if (!hasEnoughAccumulatedBytes())
break;
if (!demand.tryDecrement())
break;
item = fromInternalBuffers();
}
assert item != null;
downstreamSubscriber.onNext(item);
}
if (cancelled.get())
return;
// complete only if all data consumed
boolean complete;
synchronized (buffersLock) {
complete = state == COMPLETE && internalBuffers.isEmpty();
}
if (complete) {
assert internalBuffers.isEmpty();
pushDemandedScheduler.stop(); // stop the demand scheduler
downstreamSubscriber.onComplete();
return;
}
} catch (Throwable t) {
cancel(); // cancel if there is any error
throw t;
}
boolean requestMore = false;
synchronized (buffersLock) {
if (!hasEnoughAccumulatedBytes() && !demand.isFulfilled()) {
// request more upstream data
requestMore = true;
}
}
if (requestMore)
subscription.request(1);
}
}
@Override
public void cancel() {
if (cancelled.compareAndExchange(false, true))
return; // already cancelled
state = CANCELLED; // set CANCELLED state of upstream subscriber
subscription.cancel(); // cancel upstream subscription
pushDemandedScheduler.stop(); // stop the demand scheduler
}
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
Objects.requireNonNull(subscription);
if (this.subscription != null) {
subscription.cancel();
return;
}
int s = this.state;
assert s == UNSUBSCRIBED;
state = ACTIVE;
this.subscription = subscription;
downstreamSubscription = new DownstreamSubscription();
downstreamSubscriber.onSubscribe(downstreamSubscription);
}
@Override
public void onNext(List<ByteBuffer> item) {
Objects.requireNonNull(item);
int s = state;
if (s == CANCELLED)
return;
if (s != ACTIVE)
throw new InternalError("onNext on inactive subscriber");
synchronized (buffersLock) {
internalBuffers.addAll(item);
accumulatedBytes += remaining(item);
}
downstreamSubscription.pushDemanded();
}
@Override
public void onError(Throwable incomingThrowable) {
Objects.requireNonNull(incomingThrowable);
int s = state;
assert s == ACTIVE : "Expected ACTIVE, got:" + s;
state = ERROR;
Throwable t = this.throwable;
assert t == null : "Expected null, got:" + t;
this.throwable = incomingThrowable;
downstreamSubscription.pushDemanded();
}
@Override
public void onComplete() {
int s = state;
assert s == ACTIVE : "Expected ACTIVE, got:" + s;
state = COMPLETE;
downstreamSubscription.pushDemanded();
}
@Override
public CompletionStage<T> getBody() {
return downstreamSubscriber.getBody();
}
}

View file

@ -0,0 +1,487 @@
/*
* Copyright (c) 2015, 2018, 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.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Flow;
import java.util.stream.Collectors;
import jdk.internal.net.http.common.FlowTube;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.Utils;
/**
* Http 1.1 connection pool.
*/
final class ConnectionPool {
static final long KEEP_ALIVE = Utils.getIntegerNetProperty(
"jdk.httpclient.keepalive.timeout", 1200); // seconds
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
// Pools of idle connections
private final HashMap<CacheKey,LinkedList<HttpConnection>> plainPool;
private final HashMap<CacheKey,LinkedList<HttpConnection>> sslPool;
private final ExpiryList expiryList;
private final String dbgTag; // used for debug
boolean stopped;
/**
* Entries in connection pool are keyed by destination address and/or
* proxy address:
* case 1: plain TCP not via proxy (destination only)
* case 2: plain TCP via proxy (proxy only)
* case 3: SSL not via proxy (destination only)
* case 4: SSL over tunnel (destination and proxy)
*/
static class CacheKey {
final InetSocketAddress proxy;
final InetSocketAddress destination;
CacheKey(InetSocketAddress destination, InetSocketAddress proxy) {
this.proxy = proxy;
this.destination = destination;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final CacheKey other = (CacheKey) obj;
if (!Objects.equals(this.proxy, other.proxy)) {
return false;
}
if (!Objects.equals(this.destination, other.destination)) {
return false;
}
return true;
}
@Override
public int hashCode() {
return Objects.hash(proxy, destination);
}
}
ConnectionPool(long clientId) {
this("ConnectionPool("+clientId+")");
}
/**
* There should be one of these per HttpClient.
*/
private ConnectionPool(String tag) {
dbgTag = tag;
plainPool = new HashMap<>();
sslPool = new HashMap<>();
expiryList = new ExpiryList();
}
final String dbgString() {
return dbgTag;
}
synchronized void start() {
assert !stopped : "Already stopped";
}
static CacheKey cacheKey(InetSocketAddress destination,
InetSocketAddress proxy)
{
return new CacheKey(destination, proxy);
}
synchronized HttpConnection getConnection(boolean secure,
InetSocketAddress addr,
InetSocketAddress proxy) {
if (stopped) return null;
CacheKey key = new CacheKey(addr, proxy);
HttpConnection c = secure ? findConnection(key, sslPool)
: findConnection(key, plainPool);
//System.out.println ("getConnection returning: " + c);
return c;
}
/**
* Returns the connection to the pool.
*/
void returnToPool(HttpConnection conn) {
returnToPool(conn, Instant.now(), KEEP_ALIVE);
}
// Called also by whitebox tests
void returnToPool(HttpConnection conn, Instant now, long keepAlive) {
// Don't call registerCleanupTrigger while holding a lock,
// but register it before the connection is added to the pool,
// since we don't want to trigger the cleanup if the connection
// is not in the pool.
CleanupTrigger cleanup = registerCleanupTrigger(conn);
// it's possible that cleanup may have been called.
synchronized(this) {
if (cleanup.isDone()) {
return;
} else if (stopped) {
conn.close();
return;
}
if (conn instanceof PlainHttpConnection) {
putConnection(conn, plainPool);
} else {
assert conn.isSecure();
putConnection(conn, sslPool);
}
expiryList.add(conn, now, keepAlive);
}
//System.out.println("Return to pool: " + conn);
}
private CleanupTrigger registerCleanupTrigger(HttpConnection conn) {
// Connect the connection flow to a pub/sub pair that will take the
// connection out of the pool and close it if anything happens
// while the connection is sitting in the pool.
CleanupTrigger cleanup = new CleanupTrigger(conn);
FlowTube flow = conn.getConnectionFlow();
if (debug.on()) debug.log("registering %s", cleanup);
flow.connectFlows(cleanup, cleanup);
return cleanup;
}
private HttpConnection
findConnection(CacheKey key,
HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
LinkedList<HttpConnection> l = pool.get(key);
if (l == null || l.isEmpty()) {
return null;
} else {
HttpConnection c = l.removeFirst();
expiryList.remove(c);
return c;
}
}
/* called from cache cleaner only */
private boolean
removeFromPool(HttpConnection c,
HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
//System.out.println("cacheCleaner removing: " + c);
assert Thread.holdsLock(this);
CacheKey k = c.cacheKey();
List<HttpConnection> l = pool.get(k);
if (l == null || l.isEmpty()) {
pool.remove(k);
return false;
}
return l.remove(c);
}
private void
putConnection(HttpConnection c,
HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
CacheKey key = c.cacheKey();
LinkedList<HttpConnection> l = pool.get(key);
if (l == null) {
l = new LinkedList<>();
pool.put(key, l);
}
l.add(c);
}
/**
* Purge expired connection and return the number of milliseconds
* in which the next connection is scheduled to expire.
* If no connections are scheduled to be purged return 0.
* @return the delay in milliseconds in which the next connection will
* expire.
*/
long purgeExpiredConnectionsAndReturnNextDeadline() {
if (!expiryList.purgeMaybeRequired()) return 0;
return purgeExpiredConnectionsAndReturnNextDeadline(Instant.now());
}
// Used for whitebox testing
long purgeExpiredConnectionsAndReturnNextDeadline(Instant now) {
long nextPurge = 0;
// We may be in the process of adding new elements
// to the expiry list - but those elements will not
// have outlast their keep alive timer yet since we're
// just adding them.
if (!expiryList.purgeMaybeRequired()) return nextPurge;
List<HttpConnection> closelist;
synchronized (this) {
closelist = expiryList.purgeUntil(now);
for (HttpConnection c : closelist) {
if (c instanceof PlainHttpConnection) {
boolean wasPresent = removeFromPool(c, plainPool);
assert wasPresent;
} else {
boolean wasPresent = removeFromPool(c, sslPool);
assert wasPresent;
}
}
nextPurge = now.until(
expiryList.nextExpiryDeadline().orElse(now),
ChronoUnit.MILLIS);
}
closelist.forEach(this::close);
return nextPurge;
}
private void close(HttpConnection c) {
try {
c.close();
} catch (Throwable e) {} // ignore
}
void stop() {
List<HttpConnection> closelist = Collections.emptyList();
try {
synchronized (this) {
stopped = true;
closelist = expiryList.stream()
.map(e -> e.connection)
.collect(Collectors.toList());
expiryList.clear();
plainPool.clear();
sslPool.clear();
}
} finally {
closelist.forEach(this::close);
}
}
static final class ExpiryEntry {
final HttpConnection connection;
final Instant expiry; // absolute time in seconds of expiry time
ExpiryEntry(HttpConnection connection, Instant expiry) {
this.connection = connection;
this.expiry = expiry;
}
}
/**
* Manages a LinkedList of sorted ExpiryEntry. The entry with the closer
* deadline is at the tail of the list, and the entry with the farther
* deadline is at the head. In the most common situation, new elements
* will need to be added at the head (or close to it), and expired elements
* will need to be purged from the tail.
*/
private static final class ExpiryList {
private final LinkedList<ExpiryEntry> list = new LinkedList<>();
private volatile boolean mayContainEntries;
// A loosely accurate boolean whose value is computed
// at the end of each operation performed on ExpiryList;
// Does not require synchronizing on the ConnectionPool.
boolean purgeMaybeRequired() {
return mayContainEntries;
}
// Returns the next expiry deadline
// should only be called while holding a synchronization
// lock on the ConnectionPool
Optional<Instant> nextExpiryDeadline() {
if (list.isEmpty()) return Optional.empty();
else return Optional.of(list.getLast().expiry);
}
// should only be called while holding a synchronization
// lock on the ConnectionPool
void add(HttpConnection conn) {
add(conn, Instant.now(), KEEP_ALIVE);
}
// Used by whitebox test.
void add(HttpConnection conn, Instant now, long keepAlive) {
Instant then = now.truncatedTo(ChronoUnit.SECONDS)
.plus(keepAlive, ChronoUnit.SECONDS);
// Elements with the farther deadline are at the head of
// the list. It's more likely that the new element will
// have the farthest deadline, and will need to be inserted
// at the head of the list, so we're using an ascending
// list iterator to find the right insertion point.
ListIterator<ExpiryEntry> li = list.listIterator();
while (li.hasNext()) {
ExpiryEntry entry = li.next();
if (then.isAfter(entry.expiry)) {
li.previous();
// insert here
li.add(new ExpiryEntry(conn, then));
mayContainEntries = true;
return;
}
}
// last (or first) element of list (the last element is
// the first when the list is empty)
list.add(new ExpiryEntry(conn, then));
mayContainEntries = true;
}
// should only be called while holding a synchronization
// lock on the ConnectionPool
void remove(HttpConnection c) {
if (c == null || list.isEmpty()) return;
ListIterator<ExpiryEntry> li = list.listIterator();
while (li.hasNext()) {
ExpiryEntry e = li.next();
if (e.connection.equals(c)) {
li.remove();
mayContainEntries = !list.isEmpty();
return;
}
}
}
// should only be called while holding a synchronization
// lock on the ConnectionPool.
// Purge all elements whose deadline is before now (now included).
List<HttpConnection> purgeUntil(Instant now) {
if (list.isEmpty()) return Collections.emptyList();
List<HttpConnection> closelist = new ArrayList<>();
// elements with the closest deadlines are at the tail
// of the queue, so we're going to use a descending iterator
// to remove them, and stop when we find the first element
// that has not expired yet.
Iterator<ExpiryEntry> li = list.descendingIterator();
while (li.hasNext()) {
ExpiryEntry entry = li.next();
// use !isAfter instead of isBefore in order to
// remove the entry if its expiry == now
if (!entry.expiry.isAfter(now)) {
li.remove();
HttpConnection c = entry.connection;
closelist.add(c);
} else break; // the list is sorted
}
mayContainEntries = !list.isEmpty();
return closelist;
}
// should only be called while holding a synchronization
// lock on the ConnectionPool
java.util.stream.Stream<ExpiryEntry> stream() {
return list.stream();
}
// should only be called while holding a synchronization
// lock on the ConnectionPool
void clear() {
list.clear();
mayContainEntries = false;
}
}
void cleanup(HttpConnection c, Throwable error) {
if (debug.on())
debug.log("%s : ConnectionPool.cleanup(%s)",
String.valueOf(c.getConnectionFlow()), error);
synchronized(this) {
if (c instanceof PlainHttpConnection) {
removeFromPool(c, plainPool);
} else {
assert c.isSecure();
removeFromPool(c, sslPool);
}
expiryList.remove(c);
}
c.close();
}
/**
* An object that subscribes to the flow while the connection is in
* the pool. Anything that comes in will cause the connection to be closed
* and removed from the pool.
*/
private final class CleanupTrigger implements
FlowTube.TubeSubscriber, FlowTube.TubePublisher,
Flow.Subscription {
private final HttpConnection connection;
private volatile boolean done;
public CleanupTrigger(HttpConnection connection) {
this.connection = connection;
}
public boolean isDone() { return done;}
private void triggerCleanup(Throwable error) {
done = true;
cleanup(connection, error);
}
@Override public void request(long n) {}
@Override public void cancel() {}
@Override
public void onSubscribe(Flow.Subscription subscription) {
subscription.request(1);
}
@Override
public void onError(Throwable error) { triggerCleanup(error); }
@Override
public void onComplete() { triggerCleanup(null); }
@Override
public void onNext(List<ByteBuffer> item) {
triggerCleanup(new IOException("Data received while in pool"));
}
@Override
public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
subscriber.onSubscribe(this);
}
@Override
public String toString() {
return "CleanupTrigger(" + connection.getConnectionFlow() + ")";
}
}
}

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2015, 2018, 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.CookieHandler;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.net.http.HttpHeaders;
import jdk.internal.net.http.common.HttpHeadersImpl;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Utils;
class CookieFilter implements HeaderFilter {
public CookieFilter() {
}
@Override
public void request(HttpRequestImpl r, MultiExchange<?> e) throws IOException {
HttpClientImpl client = e.client();
Optional<CookieHandler> cookieHandlerOpt = client.cookieHandler();
if (cookieHandlerOpt.isPresent()) {
CookieHandler cookieHandler = cookieHandlerOpt.get();
Map<String,List<String>> userheaders = r.getUserHeaders().map();
Map<String,List<String>> cookies = cookieHandler.get(r.uri(), userheaders);
// add the returned cookies
HttpHeadersImpl systemHeaders = r.getSystemHeaders();
if (cookies.isEmpty()) {
Log.logTrace("Request: no cookie to add for {0}", r.uri());
} else {
Log.logTrace("Request: adding cookies for {0}", r.uri());
}
for (Map.Entry<String,List<String>> entry : cookies.entrySet()) {
final String hdrname = entry.getKey();
if (!hdrname.equalsIgnoreCase("Cookie")
&& !hdrname.equalsIgnoreCase("Cookie2"))
continue;
List<String> values = entry.getValue();
if (values == null || values.isEmpty()) continue;
for (String val : values) {
if (Utils.isValidValue(val)) {
systemHeaders.addHeader(hdrname, val);
}
}
}
} else {
Log.logTrace("Request: No cookie manager found for {0}", r.uri());
}
}
@Override
public HttpRequestImpl response(Response r) throws IOException {
HttpHeaders hdrs = r.headers();
HttpRequestImpl request = r.request();
Exchange<?> e = r.exchange;
Log.logTrace("Response: processing cookies for {0}", request.uri());
Optional<CookieHandler> cookieHandlerOpt = e.client().cookieHandler();
if (cookieHandlerOpt.isPresent()) {
CookieHandler cookieHandler = cookieHandlerOpt.get();
Log.logTrace("Response: parsing cookies from {0}", hdrs.map());
cookieHandler.put(request.uri(), hdrs.map());
} else {
Log.logTrace("Response: No cookie manager found for {0}",
request.uri());
}
return null;
}
}

View file

@ -0,0 +1,575 @@
/*
* Copyright (c) 2015, 2018, 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.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLPermission;
import java.security.AccessControlContext;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Function;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.Utils;
import jdk.internal.net.http.common.Log;
import static jdk.internal.net.http.common.Utils.permissionForProxy;
/**
* One request/response exchange (handles 100/101 intermediate response also).
* depth field used to track number of times a new request is being sent
* for a given API request. If limit exceeded exception is thrown.
*
* Security check is performed here:
* - uses AccessControlContext captured at API level
* - checks for appropriate URLPermission for request
* - if permission allowed, grants equivalent SocketPermission to call
* - in case of direct HTTP proxy, checks additionally for access to proxy
* (CONNECT proxying uses its own Exchange, so check done there)
*
*/
final class Exchange<T> {
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
final HttpRequestImpl request;
final HttpClientImpl client;
volatile ExchangeImpl<T> exchImpl;
volatile CompletableFuture<? extends ExchangeImpl<T>> exchangeCF;
volatile CompletableFuture<Void> bodyIgnored;
// used to record possible cancellation raised before the exchImpl
// has been established.
private volatile IOException failed;
final AccessControlContext acc;
final MultiExchange<T> multi;
final Executor parentExecutor;
boolean upgrading; // to HTTP/2
final PushGroup<T> pushGroup;
final String dbgTag;
Exchange(HttpRequestImpl request, MultiExchange<T> multi) {
this.request = request;
this.upgrading = false;
this.client = multi.client();
this.multi = multi;
this.acc = multi.acc;
this.parentExecutor = multi.executor;
this.pushGroup = multi.pushGroup;
this.dbgTag = "Exchange";
}
/* If different AccessControlContext to be used */
Exchange(HttpRequestImpl request,
MultiExchange<T> multi,
AccessControlContext acc)
{
this.request = request;
this.acc = acc;
this.upgrading = false;
this.client = multi.client();
this.multi = multi;
this.parentExecutor = multi.executor;
this.pushGroup = multi.pushGroup;
this.dbgTag = "Exchange";
}
PushGroup<T> getPushGroup() {
return pushGroup;
}
Executor executor() {
return parentExecutor;
}
public HttpRequestImpl request() {
return request;
}
HttpClientImpl client() {
return client;
}
public CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler) {
// If we received a 407 while establishing the exchange
// there will be no body to read: bodyIgnored will be true,
// and exchImpl will be null (if we were trying to establish
// an HTTP/2 tunnel through an HTTP/1.1 proxy)
if (bodyIgnored != null) return MinimalFuture.completedFuture(null);
// The connection will not be returned to the pool in the case of WebSocket
return exchImpl.readBodyAsync(handler, !request.isWebSocket(), parentExecutor)
.whenComplete((r,t) -> exchImpl.completed());
}
/**
* Called after a redirect or similar kind of retry where a body might
* be sent but we don't want it. Should send a RESET in h2. For http/1.1
* we can consume small quantity of data, or close the connection in
* other cases.
*/
public CompletableFuture<Void> ignoreBody() {
if (bodyIgnored != null) return bodyIgnored;
return exchImpl.ignoreBody();
}
/**
* Called when a new exchange is created to replace this exchange.
* At this point it is guaranteed that readBody/readBodyAsync will
* not be called.
*/
public void released() {
ExchangeImpl<?> impl = exchImpl;
if (impl != null) impl.released();
// Don't set exchImpl to null here. We need to keep
// it alive until it's replaced by a Stream in wrapForUpgrade.
// Setting it to null here might get it GC'ed too early, because
// the Http1Response is now only weakly referenced by the Selector.
}
public void cancel() {
// cancel can be called concurrently before or at the same time
// that the exchange impl is being established.
// In that case we won't be able to propagate the cancellation
// right away
if (exchImpl != null) {
exchImpl.cancel();
} else {
// no impl - can't cancel impl yet.
// call cancel(IOException) instead which takes care
// of race conditions between impl/cancel.
cancel(new IOException("Request cancelled"));
}
}
public void cancel(IOException cause) {
// If the impl is non null, propagate the exception right away.
// Otherwise record it so that it can be propagated once the
// exchange impl has been established.
ExchangeImpl<?> impl = exchImpl;
if (impl != null) {
// propagate the exception to the impl
if (debug.on()) debug.log("Cancelling exchImpl: %s", exchImpl);
impl.cancel(cause);
} else {
// no impl yet. record the exception
failed = cause;
// now call checkCancelled to recheck the impl.
// if the failed state is set and the impl is not null, reset
// the failed state and propagate the exception to the impl.
checkCancelled();
}
}
// This method will raise an exception if one was reported and if
// it is possible to do so. If the exception can be raised, then
// the failed state will be reset. Otherwise, the failed state
// will persist until the exception can be raised and the failed state
// can be cleared.
// Takes care of possible race conditions.
private void checkCancelled() {
ExchangeImpl<?> impl = null;
IOException cause = null;
CompletableFuture<? extends ExchangeImpl<T>> cf = null;
if (failed != null) {
synchronized(this) {
cause = failed;
impl = exchImpl;
cf = exchangeCF;
}
}
if (cause == null) return;
if (impl != null) {
// The exception is raised by propagating it to the impl.
if (debug.on()) debug.log("Cancelling exchImpl: %s", impl);
impl.cancel(cause);
failed = null;
} else {
Log.logTrace("Exchange: request [{0}/timeout={1}ms] no impl is set."
+ "\n\tCan''t cancel yet with {2}",
request.uri(),
request.timeout().isPresent() ?
// calling duration.toMillis() can throw an exception.
// this is just debugging, we don't care if it overflows.
(request.timeout().get().getSeconds() * 1000
+ request.timeout().get().getNano() / 1000000) : -1,
cause);
if (cf != null) cf.completeExceptionally(cause);
}
}
public void h2Upgrade() {
upgrading = true;
request.setH2Upgrade(client.client2());
}
synchronized IOException getCancelCause() {
return failed;
}
// get/set the exchange impl, solving race condition issues with
// potential concurrent calls to cancel() or cancel(IOException)
private CompletableFuture<? extends ExchangeImpl<T>>
establishExchange(HttpConnection connection) {
if (debug.on()) {
debug.log("establishing exchange for %s,%n\t proxy=%s",
request, request.proxy());
}
// check if we have been cancelled first.
Throwable t = getCancelCause();
checkCancelled();
if (t != null) {
return MinimalFuture.failedFuture(t);
}
CompletableFuture<? extends ExchangeImpl<T>> cf, res;
cf = ExchangeImpl.get(this, connection);
// We should probably use a VarHandle to get/set exchangeCF
// instead - as we need CAS semantics.
synchronized (this) { exchangeCF = cf; };
res = cf.whenComplete((r,x) -> {
synchronized(Exchange.this) {
if (exchangeCF == cf) exchangeCF = null;
}
});
checkCancelled();
return res.thenCompose((eimpl) -> {
// recheck for cancelled, in case of race conditions
exchImpl = eimpl;
IOException tt = getCancelCause();
checkCancelled();
if (tt != null) {
return MinimalFuture.failedFuture(tt);
} else {
// Now we're good to go. Because exchImpl is no longer
// null cancel() will be able to propagate directly to
// the impl after this point ( if needed ).
return MinimalFuture.completedFuture(eimpl);
} });
}
// Completed HttpResponse will be null if response succeeded
// will be a non null responseAsync if expect continue returns an error
public CompletableFuture<Response> responseAsync() {
return responseAsyncImpl(null);
}
CompletableFuture<Response> responseAsyncImpl(HttpConnection connection) {
SecurityException e = checkPermissions();
if (e != null) {
return MinimalFuture.failedFuture(e);
} else {
return responseAsyncImpl0(connection);
}
}
// check whether the headersSentCF was completed exceptionally with
// ProxyAuthorizationRequired. If so the Response embedded in the
// exception is returned. Otherwise we proceed.
private CompletableFuture<Response> checkFor407(ExchangeImpl<T> ex, Throwable t,
Function<ExchangeImpl<T>,CompletableFuture<Response>> andThen) {
t = Utils.getCompletionCause(t);
if (t instanceof ProxyAuthenticationRequired) {
bodyIgnored = MinimalFuture.completedFuture(null);
Response proxyResponse = ((ProxyAuthenticationRequired)t).proxyResponse;
HttpConnection c = ex == null ? null : ex.connection();
Response syntheticResponse = new Response(request, this,
proxyResponse.headers, c, proxyResponse.statusCode,
proxyResponse.version, true);
return MinimalFuture.completedFuture(syntheticResponse);
} else if (t != null) {
return MinimalFuture.failedFuture(t);
} else {
return andThen.apply(ex);
}
}
// After sending the request headers, if no ProxyAuthorizationRequired
// was raised and the expectContinue flag is on, we need to wait
// for the 100-Continue response
private CompletableFuture<Response> expectContinue(ExchangeImpl<T> ex) {
assert request.expectContinue();
return ex.getResponseAsync(parentExecutor)
.thenCompose((Response r1) -> {
Log.logResponse(r1::toString);
int rcode = r1.statusCode();
if (rcode == 100) {
Log.logTrace("Received 100-Continue: sending body");
CompletableFuture<Response> cf =
exchImpl.sendBodyAsync()
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
cf = wrapForUpgrade(cf);
cf = wrapForLog(cf);
return cf;
} else {
Log.logTrace("Expectation failed: Received {0}",
rcode);
if (upgrading && rcode == 101) {
IOException failed = new IOException(
"Unable to handle 101 while waiting for 100");
return MinimalFuture.failedFuture(failed);
}
return exchImpl.readBodyAsync(this::ignoreBody, false, parentExecutor)
.thenApply(v -> r1);
}
});
}
// After sending the request headers, if no ProxyAuthorizationRequired
// was raised and the expectContinue flag is off, we can immediately
// send the request body and proceed.
private CompletableFuture<Response> sendRequestBody(ExchangeImpl<T> ex) {
assert !request.expectContinue();
CompletableFuture<Response> cf = ex.sendBodyAsync()
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
cf = wrapForUpgrade(cf);
cf = wrapForLog(cf);
return cf;
}
CompletableFuture<Response> responseAsyncImpl0(HttpConnection connection) {
Function<ExchangeImpl<T>, CompletableFuture<Response>> after407Check;
bodyIgnored = null;
if (request.expectContinue()) {
request.addSystemHeader("Expect", "100-Continue");
Log.logTrace("Sending Expect: 100-Continue");
// wait for 100-Continue before sending body
after407Check = this::expectContinue;
} else {
// send request body and proceed.
after407Check = this::sendRequestBody;
}
// The ProxyAuthorizationRequired can be triggered either by
// establishExchange (case of HTTP/2 SSL tunneling through HTTP/1.1 proxy
// or by sendHeaderAsync (case of HTTP/1.1 SSL tunneling through HTTP/1.1 proxy
// Therefore we handle it with a call to this checkFor407(...) after these
// two places.
Function<ExchangeImpl<T>, CompletableFuture<Response>> afterExch407Check =
(ex) -> ex.sendHeadersAsync()
.handle((r,t) -> this.checkFor407(r, t, after407Check))
.thenCompose(Function.identity());
return establishExchange(connection)
.handle((r,t) -> this.checkFor407(r,t, afterExch407Check))
.thenCompose(Function.identity());
}
private CompletableFuture<Response> wrapForUpgrade(CompletableFuture<Response> cf) {
if (upgrading) {
return cf.thenCompose(r -> checkForUpgradeAsync(r, exchImpl));
}
return cf;
}
private CompletableFuture<Response> wrapForLog(CompletableFuture<Response> cf) {
if (Log.requests()) {
return cf.thenApply(response -> {
Log.logResponse(response::toString);
return response;
});
}
return cf;
}
HttpResponse.BodySubscriber<T> ignoreBody(HttpResponse.ResponseInfo hdrs) {
return HttpResponse.BodySubscribers.replacing(null);
}
// if this response was received in reply to an upgrade
// then create the Http2Connection from the HttpConnection
// initialize it and wait for the real response on a newly created Stream
private CompletableFuture<Response>
checkForUpgradeAsync(Response resp,
ExchangeImpl<T> ex) {
int rcode = resp.statusCode();
if (upgrading && (rcode == 101)) {
Http1Exchange<T> e = (Http1Exchange<T>)ex;
// check for 101 switching protocols
// 101 responses are not supposed to contain a body.
// => should we fail if there is one?
if (debug.on()) debug.log("Upgrading async %s", e.connection());
return e.readBodyAsync(this::ignoreBody, false, parentExecutor)
.thenCompose((T v) -> {// v is null
debug.log("Ignored body");
// we pass e::getBuffer to allow the ByteBuffers to accumulate
// while we build the Http2Connection
return Http2Connection.createAsync(e.connection(),
client.client2(),
this, e::drainLeftOverBytes)
.thenCompose((Http2Connection c) -> {
boolean cached = c.offerConnection();
Stream<T> s = c.getStream(1);
if (s == null) {
// s can be null if an exception occurred
// asynchronously while sending the preface.
Throwable t = c.getRecordedCause();
IOException ioe;
if (t != null) {
if (!cached)
c.close();
ioe = new IOException("Can't get stream 1: " + t, t);
} else {
ioe = new IOException("Can't get stream 1");
}
return MinimalFuture.failedFuture(ioe);
}
exchImpl.released();
Throwable t;
// There's a race condition window where an external
// thread (SelectorManager) might complete the
// exchange in timeout at the same time where we're
// trying to switch the exchange impl.
// 'failed' will be reset to null after
// exchImpl.cancel() has completed, so either we
// will observe failed != null here, or we will
// observe e.getCancelCause() != null, or the
// timeout exception will be routed to 's'.
// Either way, we need to relay it to s.
synchronized (this) {
exchImpl = s;
t = failed;
}
// Check whether the HTTP/1.1 was cancelled.
if (t == null) t = e.getCancelCause();
// if HTTP/1.1 exchange was timed out, don't
// try to go further.
if (t instanceof HttpTimeoutException) {
s.cancelImpl(t);
return MinimalFuture.failedFuture(t);
}
if (debug.on())
debug.log("Getting response async %s", s);
return s.getResponseAsync(null);
});}
);
}
return MinimalFuture.completedFuture(resp);
}
private URI getURIForSecurityCheck() {
URI u;
String method = request.method();
InetSocketAddress authority = request.authority();
URI uri = request.uri();
// CONNECT should be restricted at API level
if (method.equalsIgnoreCase("CONNECT")) {
try {
u = new URI("socket",
null,
authority.getHostString(),
authority.getPort(),
null,
null,
null);
} catch (URISyntaxException e) {
throw new InternalError(e); // shouldn't happen
}
} else {
u = uri;
}
return u;
}
/**
* Returns the security permission required for the given details.
* If method is CONNECT, then uri must be of form "scheme://host:port"
*/
private static URLPermission permissionForServer(URI uri,
String method,
Map<String, List<String>> headers) {
if (method.equals("CONNECT")) {
return new URLPermission(uri.toString(), "CONNECT");
} else {
return Utils.permissionForServer(uri, method, headers.keySet().stream());
}
}
/**
* Performs the necessary security permission checks required to retrieve
* the response. Returns a security exception representing the denied
* permission, or null if all checks pass or there is no security manager.
*/
private SecurityException checkPermissions() {
String method = request.method();
SecurityManager sm = System.getSecurityManager();
if (sm == null || method.equals("CONNECT")) {
// tunneling will have a null acc, which is fine. The proxy
// permission check will have already been preformed.
return null;
}
HttpHeaders userHeaders = request.getUserHeaders();
URI u = getURIForSecurityCheck();
URLPermission p = permissionForServer(u, method, userHeaders.map());
try {
assert acc != null;
sm.checkPermission(p, acc);
} catch (SecurityException e) {
return e;
}
ProxySelector ps = client.proxySelector();
if (ps != null) {
if (!method.equals("CONNECT")) {
// a non-tunneling HTTP proxy. Need to check access
URLPermission proxyPerm = permissionForProxy(request.proxy());
if (proxyPerm != null) {
try {
sm.checkPermission(proxyPerm, acc);
} catch (SecurityException e) {
return e;
}
}
}
}
return null;
}
HttpClient.Version version() {
return multi.version();
}
String dbgString() {
return dbgTag;
}
}

View file

@ -0,0 +1,217 @@
/*
* Copyright (c) 2015, 2018, 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.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Function;
import java.net.http.HttpResponse;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.MinimalFuture;
import static java.net.http.HttpClient.Version.HTTP_1_1;
import jdk.internal.net.http.common.Utils;
/**
* Splits request so that headers and body can be sent separately with optional
* (multiple) responses in between (e.g. 100 Continue). Also request and
* response always sent/received in different calls.
*
* Synchronous and asynchronous versions of each method are provided.
*
* Separate implementations of this class exist for HTTP/1.1 and HTTP/2
* Http1Exchange (HTTP/1.1)
* Stream (HTTP/2)
*
* These implementation classes are where work is allocated to threads.
*/
abstract class ExchangeImpl<T> {
private static final Logger debug =
Utils.getDebugLogger("ExchangeImpl"::toString, Utils.DEBUG);
final Exchange<T> exchange;
ExchangeImpl(Exchange<T> e) {
// e == null means a http/2 pushed stream
this.exchange = e;
}
final Exchange<T> getExchange() {
return exchange;
}
/**
* Returns the {@link HttpConnection} instance to which this exchange is
* assigned.
*/
abstract HttpConnection connection();
/**
* Initiates a new exchange and assigns it to a connection if one exists
* already. connection usually null.
*/
static <U> CompletableFuture<? extends ExchangeImpl<U>>
get(Exchange<U> exchange, HttpConnection connection)
{
if (exchange.version() == HTTP_1_1) {
if (debug.on())
debug.log("get: HTTP/1.1: new Http1Exchange");
return createHttp1Exchange(exchange, connection);
} else {
Http2ClientImpl c2 = exchange.client().client2(); // #### improve
HttpRequestImpl request = exchange.request();
CompletableFuture<Http2Connection> c2f = c2.getConnectionFor(request);
if (debug.on())
debug.log("get: Trying to get HTTP/2 connection");
return c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection))
.thenCompose(Function.identity());
}
}
private static <U> CompletableFuture<? extends ExchangeImpl<U>>
createExchangeImpl(Http2Connection c,
Throwable t,
Exchange<U> exchange,
HttpConnection connection)
{
if (debug.on())
debug.log("handling HTTP/2 connection creation result");
boolean secure = exchange.request().secure();
if (t != null) {
if (debug.on())
debug.log("handling HTTP/2 connection creation failed: %s",
(Object)t);
t = Utils.getCompletionCause(t);
if (t instanceof Http2Connection.ALPNException) {
Http2Connection.ALPNException ee = (Http2Connection.ALPNException)t;
AbstractAsyncSSLConnection as = ee.getConnection();
if (debug.on())
debug.log("downgrading to HTTP/1.1 with: %s", as);
CompletableFuture<? extends ExchangeImpl<U>> ex =
createHttp1Exchange(exchange, as);
return ex;
} else {
if (debug.on())
debug.log("HTTP/2 connection creation failed "
+ "with unexpected exception: %s", (Object)t);
return MinimalFuture.failedFuture(t);
}
}
if (secure && c== null) {
if (debug.on())
debug.log("downgrading to HTTP/1.1 ");
CompletableFuture<? extends ExchangeImpl<U>> ex =
createHttp1Exchange(exchange, null);
return ex;
}
if (c == null) {
// no existing connection. Send request with HTTP 1 and then
// upgrade if successful
if (debug.on())
debug.log("new Http1Exchange, try to upgrade");
return createHttp1Exchange(exchange, connection)
.thenApply((e) -> {
exchange.h2Upgrade();
return e;
});
} else {
if (debug.on()) debug.log("creating HTTP/2 streams");
Stream<U> s = c.createStream(exchange);
CompletableFuture<? extends ExchangeImpl<U>> ex = MinimalFuture.completedFuture(s);
return ex;
}
}
private static <T> CompletableFuture<Http1Exchange<T>>
createHttp1Exchange(Exchange<T> ex, HttpConnection as)
{
try {
return MinimalFuture.completedFuture(new Http1Exchange<>(ex, as));
} catch (Throwable e) {
return MinimalFuture.failedFuture(e);
}
}
/* The following methods have separate HTTP/1.1 and HTTP/2 implementations */
abstract CompletableFuture<ExchangeImpl<T>> sendHeadersAsync();
/** Sends a request body, after request headers have been sent. */
abstract CompletableFuture<ExchangeImpl<T>> sendBodyAsync();
abstract CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
boolean returnConnectionToPool,
Executor executor);
/**
* Ignore/consume the body.
*/
abstract CompletableFuture<Void> ignoreBody();
/** Gets the response headers. Completes before body is read. */
abstract CompletableFuture<Response> getResponseAsync(Executor executor);
/** Cancels a request. Not currently exposed through API. */
abstract void cancel();
/**
* Cancels a request with a cause. Not currently exposed through API.
*/
abstract void cancel(IOException cause);
/**
* Called when the exchange is released, so that cleanup actions may be
* performed - such as deregistering callbacks.
* Typically released is called during upgrade, when an HTTP/2 stream
* takes over from an Http1Exchange, or when a new exchange is created
* during a multi exchange before the final response body was received.
*/
abstract void released();
/**
* Called when the exchange is completed, so that cleanup actions may be
* performed - such as deregistering callbacks.
* Typically, completed is called at the end of the exchange, when the
* final response body has been received (or an error has caused the
* completion of the exchange).
*/
abstract void completed();
/**
* Returns true if this exchange was canceled.
* @return true if this exchange was canceled.
*/
abstract boolean isCanceled();
/**
* Returns the cause for which this exchange was canceled, if available.
* @return the cause for which this exchange was canceled, if available.
*/
abstract Throwable getCancelCause();
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2015, 2018, 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.util.LinkedList;
import java.util.List;
class FilterFactory {
// Strictly-ordered list of filters.
final LinkedList<Class<? extends HeaderFilter>> filterClasses = new LinkedList<>();
public void addFilter(Class<? extends HeaderFilter> type) {
filterClasses.add(type);
}
LinkedList<HeaderFilter> getFilterChain() {
LinkedList<HeaderFilter> l = new LinkedList<>();
for (Class<? extends HeaderFilter> clazz : filterClasses) {
try {
// Requires a public no arg constructor.
HeaderFilter headerFilter = clazz.getConstructor().newInstance();
l.add(headerFilter);
} catch (ReflectiveOperationException e) {
throw new InternalError(e);
}
}
return l;
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2015, 2018, 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;
/**
* A header filter that can examine or modify, typically system headers for
* requests before they are sent, and responses before they are returned to the
* user. Some ability to resend requests is provided.
*/
interface HeaderFilter {
void request(HttpRequestImpl r, MultiExchange<?> e) throws IOException;
/**
* Returns null if response ok to be given to user. Non null is a request
* that must be resent and its response given to user. If impl throws an
* exception that is returned to user instead.
*/
HttpRequestImpl response(Response r) throws IOException;
}

View file

@ -0,0 +1,252 @@
/*
* Copyright (c) 2015, 2018, 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.util.Iterator;
import java.util.Locale;
import java.util.NoSuchElementException;
/* This is useful for the nightmare of parsing multi-part HTTP/RFC822 headers
* sensibly:
* From a String like: 'timeout=15, max=5'
* create an array of Strings:
* { {"timeout", "15"},
* {"max", "5"}
* }
* From one like: 'Basic Realm="FuzzFace" Foo="Biz Bar Baz"'
* create one like (no quotes in literal):
* { {"basic", null},
* {"realm", "FuzzFace"}
* {"foo", "Biz Bar Baz"}
* }
* keys are converted to lower case, vals are left as is....
*/
class HeaderParser {
/* table of key/val pairs */
String raw;
String[][] tab;
int nkeys;
int asize = 10; // initial size of array is 10
public HeaderParser(String raw) {
this.raw = raw;
tab = new String[asize][2];
parse();
}
// private HeaderParser () { }
// /**
// * Creates a new HeaderParser from this, whose keys (and corresponding
// * values) range from "start" to "end-1"
// */
// public HeaderParser subsequence(int start, int end) {
// if (start == 0 && end == nkeys) {
// return this;
// }
// if (start < 0 || start >= end || end > nkeys) {
// throw new IllegalArgumentException("invalid start or end");
// }
// HeaderParser n = new HeaderParser();
// n.tab = new String [asize][2];
// n.asize = asize;
// System.arraycopy (tab, start, n.tab, 0, (end-start));
// n.nkeys= (end-start);
// return n;
// }
private void parse() {
if (raw != null) {
raw = raw.trim();
char[] ca = raw.toCharArray();
int beg = 0, end = 0, i = 0;
boolean inKey = true;
boolean inQuote = false;
int len = ca.length;
while (end < len) {
char c = ca[end];
if ((c == '=') && !inQuote) { // end of a key
tab[i][0] = new String(ca, beg, end-beg).toLowerCase(Locale.US);
inKey = false;
end++;
beg = end;
} else if (c == '\"') {
if (inQuote) {
tab[i++][1]= new String(ca, beg, end-beg);
inQuote=false;
do {
end++;
} while (end < len && (ca[end] == ' ' || ca[end] == ','));
inKey=true;
beg=end;
} else {
inQuote=true;
end++;
beg=end;
}
} else if (c == ' ' || c == ',') { // end key/val, of whatever we're in
if (inQuote) {
end++;
continue;
} else if (inKey) {
tab[i++][0] = (new String(ca, beg, end-beg)).toLowerCase(Locale.US);
} else {
tab[i++][1] = (new String(ca, beg, end-beg));
}
while (end < len && (ca[end] == ' ' || ca[end] == ',')) {
end++;
}
inKey = true;
beg = end;
} else {
end++;
}
if (i == asize) {
asize = asize * 2;
String[][] ntab = new String[asize][2];
System.arraycopy (tab, 0, ntab, 0, tab.length);
tab = ntab;
}
}
// get last key/val, if any
if (--end > beg) {
if (!inKey) {
if (ca[end] == '\"') {
tab[i++][1] = (new String(ca, beg, end-beg));
} else {
tab[i++][1] = (new String(ca, beg, end-beg+1));
}
} else {
tab[i++][0] = (new String(ca, beg, end-beg+1)).toLowerCase(Locale.US);
}
} else if (end == beg) {
if (!inKey) {
if (ca[end] == '\"') {
tab[i++][1] = String.valueOf(ca[end-1]);
} else {
tab[i++][1] = String.valueOf(ca[end]);
}
} else {
tab[i++][0] = String.valueOf(ca[end]).toLowerCase(Locale.US);
}
}
nkeys=i;
}
}
public String findKey(int i) {
if (i < 0 || i > asize) {
return null;
}
return tab[i][0];
}
public String findValue(int i) {
if (i < 0 || i > asize) {
return null;
}
return tab[i][1];
}
public String findValue(String key) {
return findValue(key, null);
}
public String findValue(String k, String Default) {
if (k == null) {
return Default;
}
k = k.toLowerCase(Locale.US);
for (int i = 0; i < asize; ++i) {
if (tab[i][0] == null) {
return Default;
} else if (k.equals(tab[i][0])) {
return tab[i][1];
}
}
return Default;
}
class ParserIterator implements Iterator<String> {
int index;
boolean returnsValue; // or key
ParserIterator (boolean returnValue) {
returnsValue = returnValue;
}
@Override
public boolean hasNext () {
return index<nkeys;
}
@Override
public String next () {
if (index >= nkeys) {
throw new NoSuchElementException();
}
return tab[index++][returnsValue?1:0];
}
}
public Iterator<String> keys () {
return new ParserIterator (false);
}
// public Iterator<String> values () {
// return new ParserIterator (true);
// }
@Override
public String toString () {
Iterator<String> k = keys();
StringBuilder sb = new StringBuilder();
sb.append("{size=").append(asize).append(" nkeys=").append(nkeys)
.append(' ');
for (int i=0; k.hasNext(); i++) {
String key = k.next();
String val = findValue (i);
if (val != null && "".equals (val)) {
val = null;
}
sb.append(" {").append(key).append(val == null ? "" : "," + val)
.append('}');
if (k.hasNext()) {
sb.append (',');
}
}
sb.append (" }");
return sb.toString();
}
// public int findInt(String k, int Default) {
// try {
// return Integer.parseInt(findValue(k, String.valueOf(Default)));
// } catch (Throwable t) {
// return Default;
// }
// }
}

View file

@ -0,0 +1,704 @@
/*
* Copyright (c) 2017, 2018, 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.EOFException;
import java.io.IOException;
import java.lang.System.Logger.Level;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import jdk.internal.net.http.common.Demand;
import jdk.internal.net.http.common.FlowTube.TubeSubscriber;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.SequentialScheduler;
import jdk.internal.net.http.common.ConnectionExpiredException;
import jdk.internal.net.http.common.Utils;
/**
* A helper class that will queue up incoming data until the receiving
* side is ready to handle it.
*/
class Http1AsyncReceiver {
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
/**
* A delegate that can asynchronously receive data from an upstream flow,
* parse, it, then possibly transform it and either store it (response
* headers) or possibly pass it to a downstream subscriber (response body).
* Usually, there will be one Http1AsyncDelegate in charge of receiving
* and parsing headers, and another one in charge of receiving, parsing,
* and forwarding body. Each will sequentially subscribe with the
* Http1AsyncReceiver in turn. There may be additional delegates which
* subscribe to the Http1AsyncReceiver, mainly for the purpose of handling
* errors while the connection is busy transmitting the request body and the
* Http1Exchange::readBody method hasn't been called yet, and response
* delegates haven't subscribed yet.
*/
static interface Http1AsyncDelegate {
/**
* Receives and handles a byte buffer reference.
* @param ref A byte buffer reference coming from upstream.
* @return false, if the byte buffer reference should be kept in the queue.
* Usually, this means that either the byte buffer reference
* was handled and parsing is finished, or that the receiver
* didn't handle the byte reference at all.
* There may or may not be any remaining data in the
* byte buffer, and the byte buffer reference must not have
* been cleared.
* true, if the byte buffer reference was fully read and
* more data can be received.
*/
public boolean tryAsyncReceive(ByteBuffer ref);
/**
* Called when an exception is raised.
* @param ex The raised Throwable.
*/
public void onReadError(Throwable ex);
/**
* Must be called before any other method on the delegate.
* The subscription can be either used directly by the delegate
* to request more data (e.g. if the delegate is a header parser),
* or can be forwarded to a downstream subscriber (if the delegate
* is a body parser that wraps a response BodySubscriber).
* In all cases, it is the responsibility of the delegate to ensure
* that request(n) and demand.tryDecrement() are called appropriately.
* No data will be sent to {@code tryAsyncReceive} unless
* the subscription has some demand.
*
* @param s A subscription that allows the delegate to control the
* data flow.
*/
public void onSubscribe(AbstractSubscription s);
/**
* Returns the subscription that was passed to {@code onSubscribe}
* @return the subscription that was passed to {@code onSubscribe}..
*/
public AbstractSubscription subscription();
/**
* Called to make sure resources are released when the
* when the Http1AsyncReceiver is stopped.
* @param error The Http1AsyncReceiver pending error ref,
* if any.
*/
public void close(Throwable error);
}
/**
* A simple subclass of AbstractSubscription that ensures the
* SequentialScheduler will be run when request() is called and demand
* becomes positive again.
*/
private static final class Http1AsyncDelegateSubscription
extends AbstractSubscription
{
private final Runnable onCancel;
private final Consumer<Throwable> onError;
private final SequentialScheduler scheduler;
private volatile boolean cancelled;
Http1AsyncDelegateSubscription(SequentialScheduler scheduler,
Runnable onCancel,
Consumer<Throwable> onError) {
this.scheduler = scheduler;
this.onCancel = onCancel;
this.onError = onError;
}
@Override
public void request(long n) {
if (cancelled) return;
try {
final Demand demand = demand();
if (demand.increase(n)) {
scheduler.runOrSchedule();
}
} catch (IllegalArgumentException x) {
cancelled = true;
onError.accept(x);
}
}
@Override
public void cancel() {
cancelled = true;
onCancel.run();
}
}
private final ConcurrentLinkedDeque<ByteBuffer> queue
= new ConcurrentLinkedDeque<>();
private final SequentialScheduler scheduler =
SequentialScheduler.synchronizedScheduler(this::flush);
private final Executor executor;
private final Http1TubeSubscriber subscriber = new Http1TubeSubscriber();
private final AtomicReference<Http1AsyncDelegate> pendingDelegateRef;
private final AtomicLong received = new AtomicLong();
final AtomicBoolean canRequestMore = new AtomicBoolean();
private volatile Throwable error;
private volatile Http1AsyncDelegate delegate;
// This reference is only used to prevent early GC of the exchange.
private volatile Http1Exchange<?> owner;
// Only used for checking whether we run on the selector manager thread.
private final HttpClientImpl client;
private boolean retry;
private volatile boolean stopRequested;
public Http1AsyncReceiver(Executor executor, Http1Exchange<?> owner) {
this.pendingDelegateRef = new AtomicReference<>();
this.executor = executor;
this.owner = owner;
this.client = owner.client;
}
// This is the main loop called by the SequentialScheduler.
// It attempts to empty the queue until the scheduler is stopped,
// or the delegate is unregistered, or the delegate is unable to
// process the data (because it's not ready or already done), which
// it signals by returning 'true';
private void flush() {
ByteBuffer buf;
try {
assert !client.isSelectorThread() :
"Http1AsyncReceiver::flush should not run in the selector: "
+ Thread.currentThread().getName();
// First check whether we have a pending delegate that has
// just subscribed, and if so, create a Subscription for it
// and call onSubscribe.
handlePendingDelegate();
// Then start emptying the queue, if possible.
while ((buf = queue.peek()) != null && !stopRequested) {
Http1AsyncDelegate delegate = this.delegate;
if (debug.on())
debug.log("Got %s bytes for delegate %s",
buf.remaining(), delegate);
if (!hasDemand(delegate)) {
// The scheduler will be invoked again later when the demand
// becomes positive.
return;
}
assert delegate != null;
if (debug.on())
debug.log("Forwarding %s bytes to delegate %s",
buf.remaining(), delegate);
// The delegate has demand: feed it the next buffer.
if (!delegate.tryAsyncReceive(buf)) {
final long remaining = buf.remaining();
if (debug.on()) debug.log(() -> {
// If the scheduler is stopped, the queue may already
// be empty and the reference may already be released.
String remstr = scheduler.isStopped() ? "" :
" remaining in ref: "
+ remaining;
remstr += remstr
+ " total remaining: " + remaining();
return "Delegate done: " + remaining;
});
canRequestMore.set(false);
// The last buffer parsed may have remaining unparsed bytes.
// Don't take it out of the queue.
return; // done.
}
// removed parsed buffer from queue, and continue with next
// if available
ByteBuffer parsed = queue.remove();
canRequestMore.set(queue.isEmpty() && !stopRequested);
assert parsed == buf;
}
// queue is empty: let's see if we should request more
checkRequestMore();
} catch (Throwable t) {
Throwable x = error;
if (x == null) error = t; // will be handled in the finally block
if (debug.on()) debug.log("Unexpected error caught in flush()", t);
} finally {
// Handles any pending error.
// The most recently subscribed delegate will get the error.
checkForErrors();
}
}
/**
* Must be called from within the scheduler main loop.
* Handles any pending errors by calling delegate.onReadError().
* If the error can be forwarded to the delegate, stops the scheduler.
*/
private void checkForErrors() {
// Handles any pending error.
// The most recently subscribed delegate will get the error.
// If the delegate is null, the error will be handled by the next
// delegate that subscribes.
// If the queue is not empty, wait until it it is empty before
// handling the error.
Http1AsyncDelegate delegate = pendingDelegateRef.get();
if (delegate == null) delegate = this.delegate;
Throwable x = error;
if (delegate != null && x != null && (stopRequested || queue.isEmpty())) {
// forward error only after emptying the queue.
final Object captured = delegate;
if (debug.on())
debug.log(() -> "flushing " + x + "\n\t delegate: " + captured
+ "\t\t queue.isEmpty: " + queue.isEmpty());
scheduler.stop();
delegate.onReadError(x);
if (stopRequested) {
// This is the special case where the subscriber
// has requested an illegal number of items.
// In this case, the error doesn't come from
// upstream, but from downstream, and we need to
// close the upstream connection.
Http1Exchange<?> exchg = owner;
stop();
if (exchg != null) exchg.connection().close();
}
}
}
/**
* Must be called from within the scheduler main loop.
* Figure out whether more data should be requested from the
* Http1TubeSubscriber.
*/
private void checkRequestMore() {
Http1AsyncDelegate delegate = this.delegate;
boolean more = this.canRequestMore.get();
boolean hasDemand = hasDemand(delegate);
if (debug.on())
debug.log("checkRequestMore: " + "canRequestMore=" + more
+ ", hasDemand=" + hasDemand
+ (delegate == null ? ", delegate=null" : ""));
if (hasDemand) {
subscriber.requestMore();
}
}
/**
* Must be called from within the scheduler main loop.
* Return true if the delegate is not null and has some demand.
* @param delegate The Http1AsyncDelegate delegate
* @return true if the delegate is not null and has some demand
*/
private boolean hasDemand(Http1AsyncDelegate delegate) {
if (delegate == null) return false;
AbstractSubscription subscription = delegate.subscription();
long demand = subscription.demand().get();
if (debug.on())
debug.log("downstream subscription demand is %s", demand);
return demand > 0;
}
/**
* Must be called from within the scheduler main loop.
* Handles pending delegate subscription.
* Return true if there was some pending delegate subscription and a new
* delegate was subscribed, false otherwise.
*
* @return true if there was some pending delegate subscription and a new
* delegate was subscribed, false otherwise.
*/
private boolean handlePendingDelegate() {
Http1AsyncDelegate pending = pendingDelegateRef.get();
if (pending != null && pendingDelegateRef.compareAndSet(pending, null)) {
Http1AsyncDelegate delegate = this.delegate;
if (delegate != null) unsubscribe(delegate);
Consumer<Throwable> onSubscriptionError = (x) -> {
setRetryOnError(false);
stopRequested = true;
onReadError(x);
};
Runnable cancel = () -> {
if (debug.on())
debug.log("Downstream subscription cancelled by %s", pending);
// The connection should be closed, as some data may
// be left over in the stream.
try {
setRetryOnError(false);
onReadError(new IOException("subscription cancelled"));
unsubscribe(pending);
} finally {
Http1Exchange<?> exchg = owner;
stop();
if (exchg != null) exchg.connection().close();
}
};
// The subscription created by a delegate is only loosely
// coupled with the upstream subscription. This is partly because
// the header/body parser work with a flow of ByteBuffer, whereas
// we have a flow List<ByteBuffer> upstream.
Http1AsyncDelegateSubscription subscription =
new Http1AsyncDelegateSubscription(scheduler, cancel, onSubscriptionError);
pending.onSubscribe(subscription);
this.delegate = delegate = pending;
final Object captured = delegate;
if (debug.on())
debug.log("delegate is now " + captured
+ ", demand=" + subscription.demand().get()
+ ", canRequestMore=" + canRequestMore.get()
+ ", queue.isEmpty=" + queue.isEmpty());
return true;
}
return false;
}
synchronized void setRetryOnError(boolean retry) {
this.retry = retry;
}
void clear() {
if (debug.on()) debug.log("cleared");
this.pendingDelegateRef.set(null);
this.delegate = null;
this.owner = null;
}
void subscribe(Http1AsyncDelegate delegate) {
synchronized(this) {
pendingDelegateRef.set(delegate);
}
if (queue.isEmpty()) {
canRequestMore.set(true);
}
if (debug.on())
debug.log("Subscribed pending " + delegate + " queue.isEmpty: "
+ queue.isEmpty());
// Everything may have been received already. Make sure
// we parse it.
if (client.isSelectorThread()) {
scheduler.runOrSchedule(executor);
} else {
scheduler.runOrSchedule();
}
}
// Used for debugging only!
long remaining() {
return Utils.remaining(queue.toArray(Utils.EMPTY_BB_ARRAY));
}
void unsubscribe(Http1AsyncDelegate delegate) {
synchronized(this) {
if (this.delegate == delegate) {
if (debug.on()) debug.log("Unsubscribed %s", delegate);
this.delegate = null;
}
}
}
// Callback: Consumer of ByteBuffer
private void asyncReceive(ByteBuffer buf) {
if (debug.on())
debug.log("Putting %s bytes into the queue", buf.remaining());
received.addAndGet(buf.remaining());
queue.offer(buf);
// This callback is called from within the selector thread.
// Use an executor here to avoid doing the heavy lifting in the
// selector.
scheduler.runOrSchedule(executor);
}
// Callback: Consumer of Throwable
void onReadError(Throwable ex) {
Http1AsyncDelegate delegate;
Throwable recorded;
if (debug.on()) debug.log("onError: %s", (Object) ex);
synchronized (this) {
delegate = this.delegate;
recorded = error;
if (recorded == null) {
// retry is set to true by HttpExchange when the connection is
// already connected, which means it's been retrieved from
// the pool.
if (retry && (ex instanceof IOException)) {
// could be either EOFException, or
// IOException("connection reset by peer), or
// SSLHandshakeException resulting from the server having
// closed the SSL session.
if (received.get() == 0) {
// If we receive such an exception before having
// received any byte, then in this case, we will
// throw ConnectionExpiredException
// to try & force a retry of the request.
retry = false;
ex = new ConnectionExpiredException(
"subscription is finished", ex);
}
}
error = ex;
}
final Throwable t = (recorded == null ? ex : recorded);
if (debug.on())
debug.log("recorded " + t + "\n\t delegate: " + delegate
+ "\t\t queue.isEmpty: " + queue.isEmpty(), ex);
}
if (queue.isEmpty() || pendingDelegateRef.get() != null || stopRequested) {
// This callback is called from within the selector thread.
// Use an executor here to avoid doing the heavy lifting in the
// selector.
scheduler.runOrSchedule(executor);
}
}
void stop() {
if (debug.on()) debug.log("stopping");
scheduler.stop();
// make sure ref count is handled properly by
// closing the delegate.
Http1AsyncDelegate previous = delegate;
if (previous != null) previous.close(error);
delegate = null;
owner = null;
}
/**
* Returns the TubeSubscriber for reading from the connection flow.
* @return the TubeSubscriber for reading from the connection flow.
*/
TubeSubscriber subscriber() {
return subscriber;
}
/**
* A simple tube subscriber for reading from the connection flow.
*/
final class Http1TubeSubscriber implements TubeSubscriber {
volatile Flow.Subscription subscription;
volatile boolean completed;
volatile boolean dropped;
public void onSubscribe(Flow.Subscription subscription) {
// supports being called multiple time.
// doesn't cancel the previous subscription, since that is
// most probably the same as the new subscription.
assert this.subscription == null || dropped == false;
this.subscription = subscription;
dropped = false;
canRequestMore.set(true);
if (delegate != null) {
scheduler.runOrSchedule(executor);
}
}
void requestMore() {
Flow.Subscription s = subscription;
if (s == null) return;
if (canRequestMore.compareAndSet(true, false)) {
if (!completed && !dropped) {
if (debug.on())
debug.log("Http1TubeSubscriber: requesting one more from upstream");
s.request(1);
return;
}
}
if (debug.on())
debug.log("Http1TubeSubscriber: no need to request more");
}
@Override
public void onNext(List<ByteBuffer> item) {
canRequestMore.set(item.isEmpty());
for (ByteBuffer buffer : item) {
asyncReceive(buffer);
}
}
@Override
public void onError(Throwable throwable) {
onReadError(throwable);
completed = true;
}
@Override
public void onComplete() {
onReadError(new EOFException("EOF reached while reading"));
completed = true;
}
public void dropSubscription() {
if (debug.on()) debug.log("Http1TubeSubscriber: dropSubscription");
// we could probably set subscription to null here...
// then we might not need the 'dropped' boolean?
dropped = true;
}
}
// Drains the content of the queue into a single ByteBuffer.
// The scheduler must be permanently stopped before calling drain().
ByteBuffer drain(ByteBuffer initial) {
// Revisit: need to clean that up.
//
ByteBuffer b = initial = (initial == null ? Utils.EMPTY_BYTEBUFFER : initial);
assert scheduler.isStopped();
if (queue.isEmpty()) return b;
// sanity check: we shouldn't have queued the same
// buffer twice.
ByteBuffer[] qbb = queue.toArray(new ByteBuffer[queue.size()]);
// the assertion looks suspicious, more investigation needed
//
// assert java.util.stream.Stream.of(qbb)
// .collect(Collectors.toSet())
// .size() == qbb.length : debugQBB(qbb);
// compute the number of bytes in the queue, the number of bytes
// in the initial buffer
// TODO: will need revisiting - as it is not guaranteed that all
// data will fit in single BB!
int size = Utils.remaining(qbb, Integer.MAX_VALUE);
int remaining = b.remaining();
int free = b.capacity() - b.position() - remaining;
if (debug.on())
debug.log("Flushing %s bytes from queue into initial buffer "
+ "(remaining=%s, free=%s)", size, remaining, free);
// check whether the initial buffer has enough space
if (size > free) {
if (debug.on())
debug.log("Allocating new buffer for initial: %s", (size + remaining));
// allocates a new buffer and copy initial to it
b = ByteBuffer.allocate(size + remaining);
Utils.copy(initial, b);
assert b.position() == remaining;
b.flip();
assert b.position() == 0;
assert b.limit() == remaining;
assert b.remaining() == remaining;
}
// store position and limit
int pos = b.position();
int limit = b.limit();
assert limit - pos == remaining;
assert b.capacity() >= remaining + size
: "capacity: " + b.capacity()
+ ", remaining: " + b.remaining()
+ ", size: " + size;
// prepare to copy the content of the queue
b.position(limit);
b.limit(pos + remaining + size);
assert b.remaining() >= size :
"remaining: " + b.remaining() + ", size: " + size;
// copy the content of the queue
int count = 0;
for (int i=0; i<qbb.length; i++) {
ByteBuffer b2 = qbb[i];
int r = b2.remaining();
assert b.remaining() >= r : "need at least " + r + " only "
+ b.remaining() + " available";
int copied = Utils.copy(b2, b);
assert copied == r : "copied="+copied+" available="+r;
assert b2.remaining() == 0;
count += copied;
}
assert count == size;
assert b.position() == pos + remaining + size :
"b.position="+b.position()+" != "+pos+"+"+remaining+"+"+size;
// reset limit and position
b.limit(limit+size);
b.position(pos);
// we can clear the refs
queue.clear();
final ByteBuffer bb = b;
if (debug.on())
debug.log("Initial buffer now has " + bb.remaining()
+ " pos=" + bb.position() + " limit=" + bb.limit());
return b;
}
private String debugQBB(ByteBuffer[] qbb) {
StringBuilder msg = new StringBuilder();
List<ByteBuffer> lbb = Arrays.asList(qbb);
Set<ByteBuffer> sbb = new HashSet<>(Arrays.asList(qbb));
int uniquebb = sbb.size();
msg.append("qbb: ").append(lbb.size())
.append(" (unique: ").append(uniquebb).append("), ")
.append("duplicates: ");
String sep = "";
for (ByteBuffer b : lbb) {
if (!sbb.remove(b)) {
msg.append(sep)
.append(String.valueOf(b))
.append("[remaining=")
.append(b.remaining())
.append(", position=")
.append(b.position())
.append(", capacity=")
.append(b.capacity())
.append("]");
sep = ", ";
}
}
return msg.toString();
}
volatile String dbgTag;
String dbgString() {
String tag = dbgTag;
if (tag == null) {
String flowTag = null;
Http1Exchange<?> exchg = owner;
Object flow = (exchg != null)
? exchg.connection().getConnectionFlow()
: null;
flowTag = tag = flow == null ? null: (String.valueOf(flow));
if (flowTag != null) {
dbgTag = tag = "Http1AsyncReceiver("+ flowTag + ")";
} else {
tag = "Http1AsyncReceiver(?)";
}
}
return tag;
}
}

View file

@ -0,0 +1,658 @@
/*
* Copyright (c) 2015, 2018, 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.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.BodySubscriber;
import java.nio.ByteBuffer;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import jdk.internal.net.http.common.Demand;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.FlowTube;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.SequentialScheduler;
import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.Utils;
import static java.net.http.HttpClient.Version.HTTP_1_1;
/**
* Encapsulates one HTTP/1.1 request/response exchange.
*/
class Http1Exchange<T> extends ExchangeImpl<T> {
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
final HttpRequestImpl request; // main request
final Http1Request requestAction;
private volatile Http1Response<T> response;
final HttpConnection connection;
final HttpClientImpl client;
final Executor executor;
private final Http1AsyncReceiver asyncReceiver;
/** Records a possible cancellation raised before any operation
* has been initiated, or an error received while sending the request. */
private Throwable failed;
private final List<CompletableFuture<?>> operations; // used for cancel
/** Must be held when operating on any internal state or data. */
private final Object lock = new Object();
/** Holds the outgoing data, either the headers or a request body part. Or
* an error from the request body publisher. At most there can be ~2 pieces
* of outgoing data ( onComplete|onError can be invoked without demand ).*/
final ConcurrentLinkedDeque<DataPair> outgoing = new ConcurrentLinkedDeque<>();
/** The write publisher, responsible for writing the complete request ( both
* headers and body ( if any ). */
private final Http1Publisher writePublisher = new Http1Publisher();
/** Completed when the header have been published, or there is an error */
private final CompletableFuture<ExchangeImpl<T>> headersSentCF = new MinimalFuture<>();
/** Completed when the body has been published, or there is an error */
private final CompletableFuture<ExchangeImpl<T>> bodySentCF = new MinimalFuture<>();
/** The subscriber to the request's body published. Maybe null. */
private volatile Http1BodySubscriber bodySubscriber;
enum State { INITIAL,
HEADERS,
BODY,
ERROR, // terminal state
COMPLETING,
COMPLETED } // terminal state
private State state = State.INITIAL;
/** A carrier for either data or an error. Used to carry data, and communicate
* errors from the request ( both headers and body ) to the exchange. */
static class DataPair {
Throwable throwable;
List<ByteBuffer> data;
DataPair(List<ByteBuffer> data, Throwable throwable){
this.data = data;
this.throwable = throwable;
}
@Override
public String toString() {
return "DataPair [data=" + data + ", throwable=" + throwable + "]";
}
}
/** An abstract supertype for HTTP/1.1 body subscribers. There are two
* concrete implementations: {@link Http1Request.StreamSubscriber}, and
* {@link Http1Request.FixedContentSubscriber}, for receiving chunked and
* fixed length bodies, respectively. */
static abstract class Http1BodySubscriber implements Flow.Subscriber<ByteBuffer> {
final MinimalFuture<Flow.Subscription> whenSubscribed = new MinimalFuture<>();
private volatile Flow.Subscription subscription;
volatile boolean complete;
private final Logger debug;
Http1BodySubscriber(Logger debug) {
assert debug != null;
this.debug = debug;
}
/** Final sentinel in the stream of request body. */
static final List<ByteBuffer> COMPLETED = List.of(ByteBuffer.allocate(0));
final void request(long n) {
if (debug.on())
debug.log("Http1BodySubscriber requesting %d, from %s",
n, subscription);
subscription.request(n);
}
final boolean isSubscribed() {
return subscription != null;
}
final void setSubscription(Flow.Subscription subscription) {
this.subscription = subscription;
whenSubscribed.complete(subscription);
}
final void cancelSubscription() {
try {
subscription.cancel();
} catch(Throwable t) {
String msg = "Ignoring exception raised when canceling BodyPublisher subscription";
if (debug.on()) debug.log("%s: %s", msg, t);
Log.logError("{0}: {1}", msg, (Object)t);
}
}
static Http1BodySubscriber completeSubscriber(Logger debug) {
return new Http1BodySubscriber(debug) {
@Override public void onSubscribe(Flow.Subscription subscription) { error(); }
@Override public void onNext(ByteBuffer item) { error(); }
@Override public void onError(Throwable throwable) { error(); }
@Override public void onComplete() { error(); }
private void error() {
throw new InternalError("should not reach here");
}
};
}
}
@Override
public String toString() {
return "HTTP/1.1 " + request.toString();
}
HttpRequestImpl request() {
return request;
}
Http1Exchange(Exchange<T> exchange, HttpConnection connection)
throws IOException
{
super(exchange);
this.request = exchange.request();
this.client = exchange.client();
this.executor = exchange.executor();
this.operations = new LinkedList<>();
operations.add(headersSentCF);
operations.add(bodySentCF);
if (connection != null) {
this.connection = connection;
} else {
InetSocketAddress addr = request.getAddress();
this.connection = HttpConnection.getConnection(addr, client, request, HTTP_1_1);
}
this.requestAction = new Http1Request(request, this);
this.asyncReceiver = new Http1AsyncReceiver(executor, this);
asyncReceiver.subscribe(new InitialErrorReceiver());
}
/** An initial receiver that handles no data, but cancels the request if
* it receives an error. Will be replaced when reading response body. */
final class InitialErrorReceiver implements Http1AsyncReceiver.Http1AsyncDelegate {
volatile AbstractSubscription s;
@Override
public boolean tryAsyncReceive(ByteBuffer ref) {
return false; // no data has been processed, leave it in the queue
}
@Override
public void onReadError(Throwable ex) {
cancelImpl(ex);
}
@Override
public void onSubscribe(AbstractSubscription s) {
this.s = s;
}
@Override
public AbstractSubscription subscription() {
return s;
}
@Override
public void close(Throwable error) {}
}
@Override
HttpConnection connection() {
return connection;
}
private void connectFlows(HttpConnection connection) {
FlowTube tube = connection.getConnectionFlow();
if (debug.on()) debug.log("%s connecting flows", tube);
// Connect the flow to our Http1TubeSubscriber:
// asyncReceiver.subscriber().
tube.connectFlows(writePublisher,
asyncReceiver.subscriber());
}
@Override
CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
// create the response before sending the request headers, so that
// the response can set the appropriate receivers.
if (debug.on()) debug.log("Sending headers only");
if (response == null) {
response = new Http1Response<>(connection, this, asyncReceiver);
}
if (debug.on()) debug.log("response created in advance");
// If the first attempt to read something triggers EOF, or
// IOException("channel reset by peer"), we're going to retry.
// Instruct the asyncReceiver to throw ConnectionExpiredException
// to force a retry.
asyncReceiver.setRetryOnError(true);
CompletableFuture<Void> connectCF;
if (!connection.connected()) {
if (debug.on()) debug.log("initiating connect async");
connectCF = connection.connectAsync();
synchronized (lock) {
operations.add(connectCF);
}
} else {
connectCF = new MinimalFuture<>();
connectCF.complete(null);
}
return connectCF
.thenCompose(unused -> {
CompletableFuture<Void> cf = new MinimalFuture<>();
try {
connectFlows(connection);
if (debug.on()) debug.log("requestAction.headers");
List<ByteBuffer> data = requestAction.headers();
synchronized (lock) {
state = State.HEADERS;
}
if (debug.on()) debug.log("setting outgoing with headers");
assert outgoing.isEmpty() : "Unexpected outgoing:" + outgoing;
appendToOutgoing(data);
cf.complete(null);
return cf;
} catch (Throwable t) {
if (debug.on()) debug.log("Failed to send headers: %s", t);
connection.close();
cf.completeExceptionally(t);
return cf;
} })
.thenCompose(unused -> headersSentCF);
}
@Override
CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
assert headersSentCF.isDone();
try {
bodySubscriber = requestAction.continueRequest();
if (bodySubscriber == null) {
bodySubscriber = Http1BodySubscriber.completeSubscriber(debug);
appendToOutgoing(Http1BodySubscriber.COMPLETED);
} else {
// start
bodySubscriber.whenSubscribed
.thenAccept((s) -> requestMoreBody());
}
} catch (Throwable t) {
cancelImpl(t);
bodySentCF.completeExceptionally(t);
}
return bodySentCF;
}
@Override
CompletableFuture<Response> getResponseAsync(Executor executor) {
CompletableFuture<Response> cf = response.readHeadersAsync(executor);
Throwable cause;
synchronized (lock) {
operations.add(cf);
cause = failed;
failed = null;
}
if (cause != null) {
Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms]"
+ "\n\tCompleting exceptionally with {2}\n",
request.uri(),
request.timeout().isPresent() ?
// calling duration.toMillis() can throw an exception.
// this is just debugging, we don't care if it overflows.
(request.timeout().get().getSeconds() * 1000
+ request.timeout().get().getNano() / 1000000) : -1,
cause);
boolean acknowledged = cf.completeExceptionally(cause);
if (debug.on())
debug.log(acknowledged ? ("completed response with " + cause)
: ("response already completed, ignoring " + cause));
}
return cf;
}
@Override
CompletableFuture<T> readBodyAsync(BodyHandler<T> handler,
boolean returnConnectionToPool,
Executor executor)
{
BodySubscriber<T> bs = handler.apply(new ResponseInfoImpl(response.responseCode(),
response.responseHeaders(),
HTTP_1_1));
CompletableFuture<T> bodyCF = response.readBody(bs,
returnConnectionToPool,
executor);
return bodyCF;
}
@Override
CompletableFuture<Void> ignoreBody() {
return response.ignoreBody(executor);
}
ByteBuffer drainLeftOverBytes() {
synchronized (lock) {
asyncReceiver.stop();
return asyncReceiver.drain(Utils.EMPTY_BYTEBUFFER);
}
}
void released() {
Http1Response<T> resp = this.response;
if (resp != null) resp.completed();
asyncReceiver.clear();
}
void completed() {
Http1Response<T> resp = this.response;
if (resp != null) resp.completed();
}
/**
* Cancel checks to see if request and responseAsync finished already.
* If not it closes the connection and completes all pending operations
*/
@Override
void cancel() {
cancelImpl(new IOException("Request cancelled"));
}
/**
* Cancel checks to see if request and responseAsync finished already.
* If not it closes the connection and completes all pending operations
*/
@Override
void cancel(IOException cause) {
cancelImpl(cause);
}
private void cancelImpl(Throwable cause) {
LinkedList<CompletableFuture<?>> toComplete = null;
int count = 0;
Throwable error;
synchronized (lock) {
if ((error = failed) == null) {
failed = error = cause;
}
if (requestAction != null && requestAction.finished()
&& response != null && response.finished()) {
return;
}
connection.close(); // TODO: ensure non-blocking if holding the lock
writePublisher.writeScheduler.stop();
if (operations.isEmpty()) {
Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms] no pending operation."
+ "\n\tCan''t cancel yet with {2}",
request.uri(),
request.timeout().isPresent() ?
// calling duration.toMillis() can throw an exception.
// this is just debugging, we don't care if it overflows.
(request.timeout().get().getSeconds() * 1000
+ request.timeout().get().getNano() / 1000000) : -1,
cause);
} else {
for (CompletableFuture<?> cf : operations) {
if (!cf.isDone()) {
if (toComplete == null) toComplete = new LinkedList<>();
toComplete.add(cf);
count++;
}
}
operations.clear();
}
}
Log.logError("Http1Exchange.cancel: count=" + count);
if (toComplete != null) {
// We might be in the selector thread in case of timeout, when
// the SelectorManager calls purgeTimeoutsAndReturnNextDeadline()
// There may or may not be other places that reach here
// from the SelectorManager thread, so just make sure we
// don't complete any CF from within the selector manager
// thread.
Executor exec = client.isSelectorThread()
? executor
: this::runInline;
Throwable x = error;
while (!toComplete.isEmpty()) {
CompletableFuture<?> cf = toComplete.poll();
exec.execute(() -> {
if (cf.completeExceptionally(x)) {
if (debug.on())
debug.log("completed cf with %s", (Object) x);
}
});
}
}
}
private void runInline(Runnable run) {
assert !client.isSelectorThread();
run.run();
}
/** Returns true if this exchange was canceled. */
boolean isCanceled() {
synchronized (lock) {
return failed != null;
}
}
/** Returns the cause for which this exchange was canceled, if available. */
Throwable getCancelCause() {
synchronized (lock) {
return failed;
}
}
/** Convenience for {@link #appendToOutgoing(DataPair)}, with just a Throwable. */
void appendToOutgoing(Throwable throwable) {
appendToOutgoing(new DataPair(null, throwable));
}
/** Convenience for {@link #appendToOutgoing(DataPair)}, with just data. */
void appendToOutgoing(List<ByteBuffer> item) {
appendToOutgoing(new DataPair(item, null));
}
private void appendToOutgoing(DataPair dp) {
if (debug.on()) debug.log("appending to outgoing " + dp);
outgoing.add(dp);
writePublisher.writeScheduler.runOrSchedule();
}
/** Tells whether, or not, there is any outgoing data that can be published,
* or if there is an error. */
private boolean hasOutgoing() {
return !outgoing.isEmpty();
}
private void requestMoreBody() {
try {
if (debug.on()) debug.log("requesting more body from the subscriber");
bodySubscriber.request(1);
} catch (Throwable t) {
if (debug.on()) debug.log("Subscription::request failed", t);
cancelImpl(t);
bodySentCF.completeExceptionally(t);
}
}
// Invoked only by the publisher
// ALL tasks should execute off the Selector-Manager thread
/** Returns the next portion of the HTTP request, or the error. */
private DataPair getOutgoing() {
final Executor exec = client.theExecutor();
final DataPair dp = outgoing.pollFirst();
if (dp == null) // publisher has not published anything yet
return null;
synchronized (lock) {
if (dp.throwable != null) {
state = State.ERROR;
exec.execute(() -> {
headersSentCF.completeExceptionally(dp.throwable);
bodySentCF.completeExceptionally(dp.throwable);
connection.close();
});
return dp;
}
switch (state) {
case HEADERS:
state = State.BODY;
// completeAsync, since dependent tasks should run in another thread
if (debug.on()) debug.log("initiating completion of headersSentCF");
headersSentCF.completeAsync(() -> this, exec);
break;
case BODY:
if (dp.data == Http1BodySubscriber.COMPLETED) {
state = State.COMPLETING;
if (debug.on()) debug.log("initiating completion of bodySentCF");
bodySentCF.completeAsync(() -> this, exec);
} else {
exec.execute(this::requestMoreBody);
}
break;
case INITIAL:
case ERROR:
case COMPLETING:
case COMPLETED:
default:
assert false : "Unexpected state:" + state;
}
return dp;
}
}
/** A Publisher of HTTP/1.1 headers and request body. */
final class Http1Publisher implements FlowTube.TubePublisher {
final Logger debug = Utils.getDebugLogger(this::dbgString);
volatile Flow.Subscriber<? super List<ByteBuffer>> subscriber;
volatile boolean cancelled;
final Http1WriteSubscription subscription = new Http1WriteSubscription();
final Demand demand = new Demand();
final SequentialScheduler writeScheduler =
SequentialScheduler.synchronizedScheduler(new WriteTask());
@Override
public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
assert state == State.INITIAL;
Objects.requireNonNull(s);
assert subscriber == null;
subscriber = s;
if (debug.on()) debug.log("got subscriber: %s", s);
s.onSubscribe(subscription);
}
volatile String dbgTag;
String dbgString() {
String tag = dbgTag;
Object flow = connection.getConnectionFlow();
if (tag == null && flow != null) {
dbgTag = tag = "Http1Publisher(" + flow + ")";
} else if (tag == null) {
tag = "Http1Publisher(?)";
}
return tag;
}
final class WriteTask implements Runnable {
@Override
public void run() {
assert state != State.COMPLETED : "Unexpected state:" + state;
if (debug.on()) debug.log("WriteTask");
if (subscriber == null) {
if (debug.on()) debug.log("no subscriber yet");
return;
}
if (debug.on()) debug.log(() -> "hasOutgoing = " + hasOutgoing());
while (hasOutgoing() && demand.tryDecrement()) {
DataPair dp = getOutgoing();
if (dp.throwable != null) {
if (debug.on()) debug.log("onError");
// Do not call the subscriber's onError, it is not required.
writeScheduler.stop();
} else {
List<ByteBuffer> data = dp.data;
if (data == Http1BodySubscriber.COMPLETED) {
synchronized (lock) {
assert state == State.COMPLETING : "Unexpected state:" + state;
state = State.COMPLETED;
}
if (debug.on())
debug.log("completed, stopping %s", writeScheduler);
writeScheduler.stop();
// Do nothing more. Just do not publish anything further.
// The next Subscriber will eventually take over.
} else {
if (debug.on())
debug.log("onNext with " + Utils.remaining(data) + " bytes");
subscriber.onNext(data);
}
}
}
}
}
final class Http1WriteSubscription implements Flow.Subscription {
@Override
public void request(long n) {
if (cancelled)
return; //no-op
demand.increase(n);
if (debug.on())
debug.log("subscription request(%d), demand=%s", n, demand);
writeScheduler.runOrSchedule(client.theExecutor());
}
@Override
public void cancel() {
if (debug.on()) debug.log("subscription cancelled");
if (cancelled)
return; //no-op
cancelled = true;
writeScheduler.stop();
}
}
}
String dbgString() {
return "Http1Exchange";
}
}

View file

@ -0,0 +1,275 @@
/*
* Copyright (c) 2017, 2018, 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.net.ProtocolException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.net.http.HttpHeaders;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
class Http1HeaderParser {
private static final char CR = '\r';
private static final char LF = '\n';
private static final char HT = '\t';
private static final char SP = ' ';
private StringBuilder sb = new StringBuilder();
private String statusLine;
private int responseCode;
private HttpHeaders headers;
private Map<String,List<String>> privateMap = new HashMap<>();
enum State { STATUS_LINE,
STATUS_LINE_FOUND_CR,
STATUS_LINE_END,
STATUS_LINE_END_CR,
HEADER,
HEADER_FOUND_CR,
HEADER_FOUND_LF,
HEADER_FOUND_CR_LF,
HEADER_FOUND_CR_LF_CR,
FINISHED }
private State state = State.STATUS_LINE;
/** Returns the status-line. */
String statusLine() { return statusLine; }
/** Returns the response code. */
int responseCode() { return responseCode; }
/** Returns the headers, possibly empty. */
HttpHeaders headers() { assert state == State.FINISHED; return headers; }
/**
* Parses HTTP/1.X status-line and headers from the given bytes. Must be
* called successive times, with additional data, until returns true.
*
* All given ByteBuffers will be consumed, until ( possibly ) the last one
* ( when true is returned ), which may not be fully consumed.
*
* @param input the ( partial ) header data
* @return true iff the end of the headers block has been reached
*/
boolean parse(ByteBuffer input) throws ProtocolException {
requireNonNull(input, "null input");
while (input.hasRemaining() && state != State.FINISHED) {
switch (state) {
case STATUS_LINE:
readResumeStatusLine(input);
break;
case STATUS_LINE_FOUND_CR:
readStatusLineFeed(input);
break;
case STATUS_LINE_END:
maybeStartHeaders(input);
break;
case STATUS_LINE_END_CR:
maybeEndHeaders(input);
break;
case HEADER:
readResumeHeader(input);
break;
// fallthrough
case HEADER_FOUND_CR:
case HEADER_FOUND_LF:
resumeOrLF(input);
break;
case HEADER_FOUND_CR_LF:
resumeOrSecondCR(input);
break;
case HEADER_FOUND_CR_LF_CR:
resumeOrEndHeaders(input);
break;
default:
throw new InternalError(
"Unexpected state: " + String.valueOf(state));
}
}
return state == State.FINISHED;
}
private void readResumeStatusLine(ByteBuffer input) {
char c = 0;
while (input.hasRemaining() && (c =(char)input.get()) != CR) {
sb.append(c);
}
if (c == CR) {
state = State.STATUS_LINE_FOUND_CR;
}
}
private void readStatusLineFeed(ByteBuffer input) throws ProtocolException {
char c = (char)input.get();
if (c != LF) {
throw protocolException("Bad trailing char, \"%s\", when parsing status-line, \"%s\"",
c, sb.toString());
}
statusLine = sb.toString();
sb = new StringBuilder();
if (!statusLine.startsWith("HTTP/1.")) {
throw protocolException("Invalid status line: \"%s\"", statusLine);
}
if (statusLine.length() < 12) {
throw protocolException("Invalid status line: \"%s\"", statusLine);
}
responseCode = Integer.parseInt(statusLine.substring(9, 12));
state = State.STATUS_LINE_END;
}
private void maybeStartHeaders(ByteBuffer input) {
assert state == State.STATUS_LINE_END;
assert sb.length() == 0;
char c = (char)input.get();
if (c == CR) {
state = State.STATUS_LINE_END_CR;
} else {
sb.append(c);
state = State.HEADER;
}
}
private void maybeEndHeaders(ByteBuffer input) throws ProtocolException {
assert state == State.STATUS_LINE_END_CR;
assert sb.length() == 0;
char c = (char)input.get();
if (c == LF) {
headers = ImmutableHeaders.of(privateMap);
privateMap = null;
state = State.FINISHED; // no headers
} else {
throw protocolException("Unexpected \"%s\", after status-line CR", c);
}
}
private void readResumeHeader(ByteBuffer input) {
assert state == State.HEADER;
assert input.hasRemaining();
while (input.hasRemaining()) {
char c = (char)input.get();
if (c == CR) {
state = State.HEADER_FOUND_CR;
break;
} else if (c == LF) {
state = State.HEADER_FOUND_LF;
break;
}
if (c == HT)
c = SP;
sb.append(c);
}
}
private void addHeaderFromString(String headerString) {
assert sb.length() == 0;
int idx = headerString.indexOf(':');
if (idx == -1)
return;
String name = headerString.substring(0, idx).trim();
if (name.isEmpty())
return;
String value = headerString.substring(idx + 1, headerString.length()).trim();
privateMap.computeIfAbsent(name.toLowerCase(Locale.US),
k -> new ArrayList<>()).add(value);
}
private void resumeOrLF(ByteBuffer input) {
assert state == State.HEADER_FOUND_CR || state == State.HEADER_FOUND_LF;
char c = (char)input.get();
if (c == LF && state == State.HEADER_FOUND_CR) {
// header value will be flushed by
// resumeOrSecondCR if next line does not
// begin by SP or HT
state = State.HEADER_FOUND_CR_LF;
} else if (c == SP || c == HT) {
sb.append(SP); // parity with MessageHeaders
state = State.HEADER;
} else {
sb = new StringBuilder();
sb.append(c);
state = State.HEADER;
}
}
private void resumeOrSecondCR(ByteBuffer input) {
assert state == State.HEADER_FOUND_CR_LF;
char c = (char)input.get();
if (c == CR) {
if (sb.length() > 0) {
// no continuation line - flush
// previous header value.
String headerString = sb.toString();
sb = new StringBuilder();
addHeaderFromString(headerString);
}
state = State.HEADER_FOUND_CR_LF_CR;
} else if (c == SP || c == HT) {
assert sb.length() != 0;
sb.append(SP); // continuation line
state = State.HEADER;
} else {
if (sb.length() > 0) {
// no continuation line - flush
// previous header value.
String headerString = sb.toString();
sb = new StringBuilder();
addHeaderFromString(headerString);
}
sb.append(c);
state = State.HEADER;
}
}
private void resumeOrEndHeaders(ByteBuffer input) throws ProtocolException {
assert state == State.HEADER_FOUND_CR_LF_CR;
char c = (char)input.get();
if (c == LF) {
state = State.FINISHED;
headers = ImmutableHeaders.of(privateMap);
privateMap = null;
} else {
throw protocolException("Unexpected \"%s\", after CR LF CR", c);
}
}
private ProtocolException protocolException(String format, Object... args) {
return new ProtocolException(format(format, args));
}
}

View file

@ -0,0 +1,469 @@
/*
* Copyright (c) 2015, 2018, 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.lang.System.Logger.Level;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.net.InetSocketAddress;
import java.util.Objects;
import java.util.concurrent.Flow;
import java.util.function.BiPredicate;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import jdk.internal.net.http.Http1Exchange.Http1BodySubscriber;
import jdk.internal.net.http.common.HttpHeadersImpl;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.Utils;
import static java.nio.charset.StandardCharsets.US_ASCII;
/**
* An HTTP/1.1 request.
*/
class Http1Request {
private static final String COOKIE_HEADER = "Cookie";
private static final BiPredicate<String,List<String>> NOCOOKIES =
(k,v) -> !COOKIE_HEADER.equalsIgnoreCase(k);
private final HttpRequestImpl request;
private final Http1Exchange<?> http1Exchange;
private final HttpConnection connection;
private final HttpRequest.BodyPublisher requestPublisher;
private final HttpHeaders userHeaders;
private final HttpHeadersImpl systemHeaders;
private volatile boolean streaming;
private volatile long contentLength;
Http1Request(HttpRequestImpl request,
Http1Exchange<?> http1Exchange)
throws IOException
{
this.request = request;
this.http1Exchange = http1Exchange;
this.connection = http1Exchange.connection();
this.requestPublisher = request.requestPublisher; // may be null
this.userHeaders = request.getUserHeaders();
this.systemHeaders = request.getSystemHeaders();
}
private void logHeaders(String completeHeaders) {
if (Log.headers()) {
//StringBuilder sb = new StringBuilder(256);
//sb.append("REQUEST HEADERS:\n");
//Log.dumpHeaders(sb, " ", systemHeaders);
//Log.dumpHeaders(sb, " ", userHeaders);
//Log.logHeaders(sb.toString());
String s = completeHeaders.replaceAll("\r\n", "\n");
if (s.endsWith("\n\n")) s = s.substring(0, s.length() - 2);
Log.logHeaders("REQUEST HEADERS:\n{0}\n", s);
}
}
private void collectHeaders0(StringBuilder sb) {
BiPredicate<String,List<String>> filter =
connection.headerFilter(request);
// Filter out 'Cookie:' headers, we will collect them at the end.
BiPredicate<String,List<String>> nocookies =
NOCOOKIES.and(filter);
// If we're sending this request through a tunnel,
// then don't send any preemptive proxy-* headers that
// the authentication filter may have saved in its
// cache.
collectHeaders1(sb, systemHeaders, nocookies);
// If we're sending this request through a tunnel,
// don't send any user-supplied proxy-* headers
// to the target server.
collectHeaders1(sb, userHeaders, nocookies);
// Gather all 'Cookie:' headers and concatenate their
// values in a single line.
collectCookies(sb, COOKIE_HEADER,
systemHeaders, userHeaders, filter);
// terminate headers
sb.append('\r').append('\n');
}
// Concatenate any 'Cookie:' header in a single line, as mandated
// by RFC 6265, section 5.4:
//
// <<When the user agent generates an HTTP request, the user agent MUST
// NOT attach more than one Cookie header field.>>
//
// This constraint is relaxed for the HTTP/2 protocol, which
// explicitly allows sending multiple Cookie header fields.
// RFC 7540 section 8.1.2.5:
//
// <<To allow for better compression efficiency, the Cookie header
// field MAY be split into separate header fields, each with one or
// more cookie-pairs.>>
//
// This method will therefore concatenate multiple Cookie header field
// values into a single field, in a similar way than was implemented in
// the legacy HttpURLConnection.
//
// Note that at this point this method performs no further validation
// on the actual field-values, except to check that they do not contain
// any illegal character for header field values.
//
private void collectCookies(StringBuilder sb,
String key,
HttpHeaders system,
HttpHeaders user,
BiPredicate<String, List<String>> filter) {
List<String> systemList = system.allValues(key);
if (systemList != null && !filter.test(key, systemList)) systemList = null;
List<String> userList = user.allValues(key);
if (userList != null && !filter.test(key, userList)) userList = null;
boolean found = false;
if (systemList != null) {
for (String cookie : systemList) {
if (!found) {
found = true;
sb.append(key).append(':').append(' ');
} else {
sb.append(';').append(' ');
}
sb.append(cookie);
}
}
if (userList != null) {
for (String cookie : userList) {
if (!found) {
found = true;
sb.append(key).append(':').append(' ');
} else {
sb.append(';').append(' ');
}
sb.append(cookie);
}
}
if (found) sb.append('\r').append('\n');
}
private void collectHeaders1(StringBuilder sb, HttpHeaders headers,
BiPredicate<String, List<String>> filter) {
for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) {
String key = entry.getKey();
List<String> values = entry.getValue();
if (!filter.test(key, values)) continue;
for (String value : values) {
sb.append(key).append(':').append(' ')
.append(value)
.append('\r').append('\n');
}
}
}
private String getPathAndQuery(URI uri) {
String path = uri.getRawPath();
String query = uri.getRawQuery();
if (path == null || path.equals("")) {
path = "/";
}
if (query == null) {
query = "";
}
if (query.equals("")) {
return path;
} else {
return path + "?" + query;
}
}
private String authorityString(InetSocketAddress addr) {
return addr.getHostString() + ":" + addr.getPort();
}
private String hostString() {
URI uri = request.uri();
int port = uri.getPort();
String host = uri.getHost();
boolean defaultPort;
if (port == -1) {
defaultPort = true;
} else if (request.secure()) {
defaultPort = port == 443;
} else {
defaultPort = port == 80;
}
if (defaultPort) {
return host;
} else {
return host + ":" + Integer.toString(port);
}
}
private String requestURI() {
URI uri = request.uri();
String method = request.method();
if ((request.proxy() == null && !method.equals("CONNECT"))
|| request.isWebSocket()) {
return getPathAndQuery(uri);
}
if (request.secure()) {
if (request.method().equals("CONNECT")) {
// use authority for connect itself
return authorityString(request.authority());
} else {
// requests over tunnel do not require full URL
return getPathAndQuery(uri);
}
}
if (request.method().equals("CONNECT")) {
// use authority for connect itself
return authorityString(request.authority());
}
return uri == null? authorityString(request.authority()) : uri.toString();
}
private boolean finished;
synchronized boolean finished() {
return finished;
}
synchronized void setFinished() {
finished = true;
}
List<ByteBuffer> headers() {
if (Log.requests() && request != null) {
Log.logRequest(request.toString());
}
String uriString = requestURI();
StringBuilder sb = new StringBuilder(64);
sb.append(request.method())
.append(' ')
.append(uriString)
.append(" HTTP/1.1\r\n");
URI uri = request.uri();
if (uri != null) {
systemHeaders.setHeader("Host", hostString());
}
if (requestPublisher == null) {
// Not a user request, or maybe a method, e.g. GET, with no body.
contentLength = 0;
} else {
contentLength = requestPublisher.contentLength();
}
if (contentLength == 0) {
systemHeaders.setHeader("Content-Length", "0");
} else if (contentLength > 0) {
systemHeaders.setHeader("Content-Length", Long.toString(contentLength));
streaming = false;
} else {
streaming = true;
systemHeaders.setHeader("Transfer-encoding", "chunked");
}
collectHeaders0(sb);
String hs = sb.toString();
logHeaders(hs);
ByteBuffer b = ByteBuffer.wrap(hs.getBytes(US_ASCII));
return List.of(b);
}
Http1BodySubscriber continueRequest() {
Http1BodySubscriber subscriber;
if (streaming) {
subscriber = new StreamSubscriber();
requestPublisher.subscribe(subscriber);
} else {
if (contentLength == 0)
return null;
subscriber = new FixedContentSubscriber();
requestPublisher.subscribe(subscriber);
}
return subscriber;
}
final class StreamSubscriber extends Http1BodySubscriber {
StreamSubscriber() { super(debug); }
@Override
public void onSubscribe(Flow.Subscription subscription) {
if (isSubscribed()) {
Throwable t = new IllegalStateException("already subscribed");
http1Exchange.appendToOutgoing(t);
} else {
setSubscription(subscription);
}
}
@Override
public void onNext(ByteBuffer item) {
Objects.requireNonNull(item);
if (complete) {
Throwable t = new IllegalStateException("subscription already completed");
http1Exchange.appendToOutgoing(t);
} else {
int chunklen = item.remaining();
ArrayList<ByteBuffer> l = new ArrayList<>(3);
l.add(getHeader(chunklen));
l.add(item);
l.add(ByteBuffer.wrap(CRLF));
http1Exchange.appendToOutgoing(l);
}
}
@Override
public void onError(Throwable throwable) {
if (complete)
return;
cancelSubscription();
http1Exchange.appendToOutgoing(throwable);
}
@Override
public void onComplete() {
if (complete) {
Throwable t = new IllegalStateException("subscription already completed");
http1Exchange.appendToOutgoing(t);
} else {
ArrayList<ByteBuffer> l = new ArrayList<>(2);
l.add(ByteBuffer.wrap(EMPTY_CHUNK_BYTES));
l.add(ByteBuffer.wrap(CRLF));
complete = true;
//setFinished();
http1Exchange.appendToOutgoing(l);
http1Exchange.appendToOutgoing(COMPLETED);
setFinished(); // TODO: before or after,? does it matter?
}
}
}
final class FixedContentSubscriber extends Http1BodySubscriber {
private volatile long contentWritten;
FixedContentSubscriber() { super(debug); }
@Override
public void onSubscribe(Flow.Subscription subscription) {
if (isSubscribed()) {
Throwable t = new IllegalStateException("already subscribed");
http1Exchange.appendToOutgoing(t);
} else {
setSubscription(subscription);
}
}
@Override
public void onNext(ByteBuffer item) {
if (debug.on()) debug.log("onNext");
Objects.requireNonNull(item);
if (complete) {
Throwable t = new IllegalStateException("subscription already completed");
http1Exchange.appendToOutgoing(t);
} else {
long writing = item.remaining();
long written = (contentWritten += writing);
if (written > contentLength) {
cancelSubscription();
String msg = connection.getConnectionFlow()
+ " [" + Thread.currentThread().getName() +"] "
+ "Too many bytes in request body. Expected: "
+ contentLength + ", got: " + written;
http1Exchange.appendToOutgoing(new IOException(msg));
} else {
http1Exchange.appendToOutgoing(List.of(item));
}
}
}
@Override
public void onError(Throwable throwable) {
if (debug.on()) debug.log("onError");
if (complete) // TODO: error?
return;
cancelSubscription();
http1Exchange.appendToOutgoing(throwable);
}
@Override
public void onComplete() {
if (debug.on()) debug.log("onComplete");
if (complete) {
Throwable t = new IllegalStateException("subscription already completed");
http1Exchange.appendToOutgoing(t);
} else {
complete = true;
long written = contentWritten;
if (contentLength > written) {
cancelSubscription();
Throwable t = new IOException(connection.getConnectionFlow()
+ " [" + Thread.currentThread().getName() +"] "
+ "Too few bytes returned by the publisher ("
+ written + "/"
+ contentLength + ")");
http1Exchange.appendToOutgoing(t);
} else {
http1Exchange.appendToOutgoing(COMPLETED);
}
}
}
}
private static final byte[] CRLF = {'\r', '\n'};
private static final byte[] EMPTY_CHUNK_BYTES = {'0', '\r', '\n'};
/** Returns a header for a particular chunk size */
private static ByteBuffer getHeader(int size) {
String hexStr = Integer.toHexString(size);
byte[] hexBytes = hexStr.getBytes(US_ASCII);
byte[] header = new byte[hexStr.length()+2];
System.arraycopy(hexBytes, 0, header, 0, hexBytes.length);
header[hexBytes.length] = CRLF[0];
header[hexBytes.length+1] = CRLF[1];
return ByteBuffer.wrap(header);
}
final Logger debug = Utils.getDebugLogger(this::toString, Utils.DEBUG);
}

View file

@ -0,0 +1,763 @@
/*
* Copyright (c) 2015, 2018, 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.EOFException;
import java.lang.System.Logger.Level;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function;
import java.net.http.HttpHeaders;
import java.net.http.HttpResponse;
import jdk.internal.net.http.ResponseContent.BodyParser;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.Utils;
import static java.net.http.HttpClient.Version.HTTP_1_1;
import static java.net.http.HttpResponse.BodySubscribers.discarding;
/**
* Handles a HTTP/1.1 response (headers + body).
* There can be more than one of these per Http exchange.
*/
class Http1Response<T> {
private volatile ResponseContent content;
private final HttpRequestImpl request;
private Response response;
private final HttpConnection connection;
private HttpHeaders headers;
private int responseCode;
private final Http1Exchange<T> exchange;
private boolean return2Cache; // return connection to cache when finished
private final HeadersReader headersReader; // used to read the headers
private final BodyReader bodyReader; // used to read the body
private final Http1AsyncReceiver asyncReceiver;
private volatile EOFException eof;
// max number of bytes of (fixed length) body to ignore on redirect
private final static int MAX_IGNORE = 1024;
// Revisit: can we get rid of this?
static enum State {INITIAL, READING_HEADERS, READING_BODY, DONE}
private volatile State readProgress = State.INITIAL;
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
final static AtomicLong responseCount = new AtomicLong();
final long id = responseCount.incrementAndGet();
Http1Response(HttpConnection conn,
Http1Exchange<T> exchange,
Http1AsyncReceiver asyncReceiver) {
this.readProgress = State.INITIAL;
this.request = exchange.request();
this.exchange = exchange;
this.connection = conn;
this.asyncReceiver = asyncReceiver;
headersReader = new HeadersReader(this::advance);
bodyReader = new BodyReader(this::advance);
}
String dbgTag;
private String dbgString() {
String dbg = dbgTag;
if (dbg == null) {
String cdbg = connection.dbgTag;
if (cdbg != null) {
dbgTag = dbg = "Http1Response(id=" + id + ", " + cdbg + ")";
} else {
dbg = "Http1Response(id=" + id + ")";
}
}
return dbg;
}
// The ClientRefCountTracker is used to track the state
// of a pending operation. Altough there usually is a single
// point where the operation starts, it may terminate at
// different places.
private final class ClientRefCountTracker {
final HttpClientImpl client = connection.client();
// state & 0x01 != 0 => acquire called
// state & 0x02 != 0 => tryRelease called
byte state;
public synchronized void acquire() {
if (state == 0) {
// increment the reference count on the HttpClientImpl
// to prevent the SelectorManager thread from exiting
// until our operation is complete.
if (debug.on())
debug.log("Operation started: incrementing ref count for %s", client);
client.reference();
state = 0x01;
} else {
if (debug.on())
debug.log("Operation ref count for %s is already %s",
client, ((state & 0x2) == 0x2) ? "released." : "incremented!" );
assert (state & 0x01) == 0 : "reference count already incremented";
}
}
public synchronized void tryRelease() {
if (state == 0x01) {
// decrement the reference count on the HttpClientImpl
// to allow the SelectorManager thread to exit if no
// other operation is pending and the facade is no
// longer referenced.
if (debug.on())
debug.log("Operation finished: decrementing ref count for %s", client);
client.unreference();
} else if (state == 0) {
if (debug.on())
debug.log("Operation finished: releasing ref count for %s", client);
} else if ((state & 0x02) == 0x02) {
if (debug.on())
debug.log("ref count for %s already released", client);
}
state |= 0x02;
}
}
public CompletableFuture<Response> readHeadersAsync(Executor executor) {
if (debug.on())
debug.log("Reading Headers: (remaining: "
+ asyncReceiver.remaining() +") " + readProgress);
// with expect continue we will resume reading headers + body.
asyncReceiver.unsubscribe(bodyReader);
bodyReader.reset();
Http1HeaderParser hd = new Http1HeaderParser();
readProgress = State.READING_HEADERS;
headersReader.start(hd);
asyncReceiver.subscribe(headersReader);
CompletableFuture<State> cf = headersReader.completion();
assert cf != null : "parsing not started";
Function<State, Response> lambda = (State completed) -> {
assert completed == State.READING_HEADERS;
if (debug.on())
debug.log("Reading Headers: creating Response object;"
+ " state is now " + readProgress);
asyncReceiver.unsubscribe(headersReader);
responseCode = hd.responseCode();
headers = hd.headers();
response = new Response(request,
exchange.getExchange(),
headers,
connection,
responseCode,
HTTP_1_1);
if (Log.headers()) {
StringBuilder sb = new StringBuilder("RESPONSE HEADERS:\n");
Log.dumpHeaders(sb, " ", headers);
Log.logHeaders(sb.toString());
}
return response;
};
if (executor != null) {
return cf.thenApplyAsync(lambda, executor);
} else {
return cf.thenApply(lambda);
}
}
private boolean finished;
synchronized void completed() {
finished = true;
}
synchronized boolean finished() {
return finished;
}
int fixupContentLen(int clen) {
if (request.method().equalsIgnoreCase("HEAD")) {
return 0;
}
if (clen == -1) {
if (headers.firstValue("Transfer-encoding").orElse("")
.equalsIgnoreCase("chunked")) {
return -1;
}
return 0;
}
return clen;
}
/**
* Read up to MAX_IGNORE bytes discarding
*/
public CompletableFuture<Void> ignoreBody(Executor executor) {
int clen = (int)headers.firstValueAsLong("Content-Length").orElse(-1);
if (clen == -1 || clen > MAX_IGNORE) {
connection.close();
return MinimalFuture.completedFuture(null); // not treating as error
} else {
return readBody(discarding(), true, executor);
}
}
static final Flow.Subscription NOP = new Flow.Subscription() {
@Override
public void request(long n) { }
public void cancel() { }
};
/**
* The Http1AsyncReceiver ensures that all calls to
* the subscriber, including onSubscribe, occur sequentially.
* There could however be some race conditions that could happen
* in case of unexpected errors thrown at unexpected places, which
* may cause onError to be called multiple times.
* The Http1BodySubscriber will ensure that the user subscriber
* is actually completed only once - and only after it is
* subscribed.
* @param <U> The type of response.
*/
final static class Http1BodySubscriber<U> implements HttpResponse.BodySubscriber<U> {
final HttpResponse.BodySubscriber<U> userSubscriber;
final AtomicBoolean completed = new AtomicBoolean();
volatile Throwable withError;
volatile boolean subscribed;
Http1BodySubscriber(HttpResponse.BodySubscriber<U> userSubscriber) {
this.userSubscriber = userSubscriber;
}
// propagate the error to the user subscriber, even if not
// subscribed yet.
private void propagateError(Throwable t) {
assert t != null;
try {
// if unsubscribed at this point, it will not
// get subscribed later - so do it now and
// propagate the error
if (subscribed == false) {
subscribed = true;
userSubscriber.onSubscribe(NOP);
}
} finally {
// if onError throws then there is nothing to do
// here: let the caller deal with it by logging
// and closing the connection.
userSubscriber.onError(t);
}
}
// complete the subscriber, either normally or exceptionally
// ensure that the subscriber is completed only once.
private void complete(Throwable t) {
if (completed.compareAndSet(false, true)) {
t = withError = Utils.getCompletionCause(t);
if (t == null) {
assert subscribed;
try {
userSubscriber.onComplete();
} catch (Throwable x) {
propagateError(t = withError = Utils.getCompletionCause(x));
// rethrow and let the caller deal with it.
// (i.e: log and close the connection)
// arguably we could decide to not throw and let the
// connection be reused since we should have received and
// parsed all the bytes when we reach here.
throw x;
}
} else {
propagateError(t);
}
}
}
@Override
public CompletionStage<U> getBody() {
return userSubscriber.getBody();
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
if (!subscribed) {
subscribed = true;
userSubscriber.onSubscribe(subscription);
} else {
// could be already subscribed and completed
// if an unexpected error occurred before the actual
// subscription - though that's not supposed
// happen.
assert completed.get();
}
}
@Override
public void onNext(List<ByteBuffer> item) {
assert !completed.get();
userSubscriber.onNext(item);
}
@Override
public void onError(Throwable throwable) {
complete(throwable);
}
@Override
public void onComplete() {
complete(null);
}
}
public <U> CompletableFuture<U> readBody(HttpResponse.BodySubscriber<U> p,
boolean return2Cache,
Executor executor) {
this.return2Cache = return2Cache;
final Http1BodySubscriber<U> subscriber = new Http1BodySubscriber<>(p);
final CompletableFuture<U> cf = new MinimalFuture<>();
int clen0 = (int)headers.firstValueAsLong("Content-Length").orElse(-1);
final int clen = fixupContentLen(clen0);
// expect-continue reads headers and body twice.
// if we reach here, we must reset the headersReader state.
asyncReceiver.unsubscribe(headersReader);
headersReader.reset();
ClientRefCountTracker refCountTracker = new ClientRefCountTracker();
// We need to keep hold on the client facade until the
// tracker has been incremented.
connection.client().reference();
executor.execute(() -> {
try {
content = new ResponseContent(
connection, clen, headers, subscriber,
this::onFinished
);
if (cf.isCompletedExceptionally()) {
// if an error occurs during subscription
connection.close();
return;
}
// increment the reference count on the HttpClientImpl
// to prevent the SelectorManager thread from exiting until
// the body is fully read.
refCountTracker.acquire();
bodyReader.start(content.getBodyParser(
(t) -> {
try {
if (t != null) {
subscriber.onError(t);
connection.close();
cf.completeExceptionally(t);
}
} finally {
bodyReader.onComplete(t);
}
}));
CompletableFuture<State> bodyReaderCF = bodyReader.completion();
asyncReceiver.subscribe(bodyReader);
assert bodyReaderCF != null : "parsing not started";
// Make sure to keep a reference to asyncReceiver from
// within this
CompletableFuture<?> trailingOp = bodyReaderCF.whenComplete((s,t) -> {
t = Utils.getCompletionCause(t);
try {
if (t == null) {
if (debug.on()) debug.log("Finished reading body: " + s);
assert s == State.READING_BODY;
}
if (t != null) {
subscriber.onError(t);
cf.completeExceptionally(t);
}
} catch (Throwable x) {
// not supposed to happen
asyncReceiver.onReadError(x);
} finally {
// we're done: release the ref count for
// the current operation.
refCountTracker.tryRelease();
}
});
connection.addTrailingOperation(trailingOp);
} catch (Throwable t) {
if (debug.on()) debug.log("Failed reading body: " + t);
try {
subscriber.onError(t);
cf.completeExceptionally(t);
} finally {
asyncReceiver.onReadError(t);
}
} finally {
connection.client().unreference();
}
});
try {
p.getBody().whenComplete((U u, Throwable t) -> {
if (t == null)
cf.complete(u);
else
cf.completeExceptionally(t);
});
} catch (Throwable t) {
cf.completeExceptionally(t);
asyncReceiver.setRetryOnError(false);
asyncReceiver.onReadError(t);
}
return cf.whenComplete((s,t) -> {
if (t != null) {
// If an exception occurred, release the
// ref count for the current operation, as
// it may never be triggered otherwise
// (BodySubscriber ofInputStream)
// If there was no exception then the
// ref count will be/have been released when
// the last byte of the response is/was received
refCountTracker.tryRelease();
}
});
}
private void onFinished() {
asyncReceiver.clear();
if (return2Cache) {
Log.logTrace("Attempting to return connection to the pool: {0}", connection);
// TODO: need to do something here?
// connection.setAsyncCallbacks(null, null, null);
// don't return the connection to the cache if EOF happened.
if (debug.on())
debug.log(connection.getConnectionFlow() + ": return to HTTP/1.1 pool");
connection.closeOrReturnToCache(eof == null ? headers : null);
}
}
HttpHeaders responseHeaders() {
return headers;
}
int responseCode() {
return responseCode;
}
// ================ Support for plugging into Http1Receiver =================
// ============================================================================
// Callback: Error receiver: Consumer of Throwable.
void onReadError(Throwable t) {
Log.logError(t);
Receiver<?> receiver = receiver(readProgress);
if (t instanceof EOFException) {
debug.log(Level.DEBUG, "onReadError: received EOF");
eof = (EOFException) t;
}
CompletableFuture<?> cf = receiver == null ? null : receiver.completion();
debug.log(Level.DEBUG, () -> "onReadError: cf is "
+ (cf == null ? "null"
: (cf.isDone() ? "already completed"
: "not yet completed")));
if (cf != null) {
cf.completeExceptionally(t);
} else {
debug.log(Level.DEBUG, "onReadError", t);
}
debug.log(Level.DEBUG, () -> "closing connection: cause is " + t);
connection.close();
}
// ========================================================================
private State advance(State previous) {
assert readProgress == previous;
switch(previous) {
case READING_HEADERS:
asyncReceiver.unsubscribe(headersReader);
return readProgress = State.READING_BODY;
case READING_BODY:
asyncReceiver.unsubscribe(bodyReader);
return readProgress = State.DONE;
default:
throw new InternalError("can't advance from " + previous);
}
}
Receiver<?> receiver(State state) {
switch(state) {
case READING_HEADERS: return headersReader;
case READING_BODY: return bodyReader;
default: return null;
}
}
static abstract class Receiver<T>
implements Http1AsyncReceiver.Http1AsyncDelegate {
abstract void start(T parser);
abstract CompletableFuture<State> completion();
// accepts a buffer from upstream.
// this should be implemented as a simple call to
// accept(ref, parser, cf)
public abstract boolean tryAsyncReceive(ByteBuffer buffer);
public abstract void onReadError(Throwable t);
// handle a byte buffer received from upstream.
// this method should set the value of Http1Response.buffer
// to ref.get() before beginning parsing.
abstract void handle(ByteBuffer buf, T parser,
CompletableFuture<State> cf);
// resets this objects state so that it can be reused later on
// typically puts the reference to parser and completion to null
abstract void reset();
// accepts a byte buffer received from upstream
// returns true if the buffer is fully parsed and more data can
// be accepted, false otherwise.
final boolean accept(ByteBuffer buf, T parser,
CompletableFuture<State> cf) {
if (cf == null || parser == null || cf.isDone()) return false;
handle(buf, parser, cf);
return !cf.isDone();
}
public abstract void onSubscribe(AbstractSubscription s);
public abstract AbstractSubscription subscription();
}
// Invoked with each new ByteBuffer when reading headers...
final class HeadersReader extends Receiver<Http1HeaderParser> {
final Consumer<State> onComplete;
volatile Http1HeaderParser parser;
volatile CompletableFuture<State> cf;
volatile long count; // bytes parsed (for debug)
volatile AbstractSubscription subscription;
HeadersReader(Consumer<State> onComplete) {
this.onComplete = onComplete;
}
@Override
public AbstractSubscription subscription() {
return subscription;
}
@Override
public void onSubscribe(AbstractSubscription s) {
this.subscription = s;
s.request(1);
}
@Override
void reset() {
cf = null;
parser = null;
count = 0;
subscription = null;
}
// Revisit: do we need to support restarting?
@Override
final void start(Http1HeaderParser hp) {
count = 0;
cf = new MinimalFuture<>();
parser = hp;
}
@Override
CompletableFuture<State> completion() {
return cf;
}
@Override
public final boolean tryAsyncReceive(ByteBuffer ref) {
boolean hasDemand = subscription.demand().tryDecrement();
assert hasDemand;
boolean needsMore = accept(ref, parser, cf);
if (needsMore) subscription.request(1);
return needsMore;
}
@Override
public final void onReadError(Throwable t) {
Http1Response.this.onReadError(t);
}
@Override
final void handle(ByteBuffer b,
Http1HeaderParser parser,
CompletableFuture<State> cf) {
assert cf != null : "parsing not started";
assert parser != null : "no parser";
try {
count += b.remaining();
if (debug.on())
debug.log("Sending " + b.remaining() + "/" + b.capacity()
+ " bytes to header parser");
if (parser.parse(b)) {
count -= b.remaining();
if (debug.on())
debug.log("Parsing headers completed. bytes=" + count);
onComplete.accept(State.READING_HEADERS);
cf.complete(State.READING_HEADERS);
}
} catch (Throwable t) {
if (debug.on())
debug.log("Header parser failed to handle buffer: " + t);
cf.completeExceptionally(t);
}
}
@Override
public void close(Throwable error) {
// if there's no error nothing to do: the cf should/will
// be completed.
if (error != null) {
CompletableFuture<State> cf = this.cf;
if (cf != null) {
if (debug.on())
debug.log("close: completing header parser CF with " + error);
cf.completeExceptionally(error);
}
}
}
}
// Invoked with each new ByteBuffer when reading bodies...
final class BodyReader extends Receiver<BodyParser> {
final Consumer<State> onComplete;
volatile BodyParser parser;
volatile CompletableFuture<State> cf;
volatile AbstractSubscription subscription;
BodyReader(Consumer<State> onComplete) {
this.onComplete = onComplete;
}
@Override
void reset() {
parser = null;
cf = null;
subscription = null;
}
// Revisit: do we need to support restarting?
@Override
final void start(BodyParser parser) {
cf = new MinimalFuture<>();
this.parser = parser;
}
@Override
CompletableFuture<State> completion() {
return cf;
}
@Override
public final boolean tryAsyncReceive(ByteBuffer b) {
return accept(b, parser, cf);
}
@Override
public final void onReadError(Throwable t) {
Http1Response.this.onReadError(t);
}
@Override
public AbstractSubscription subscription() {
return subscription;
}
@Override
public void onSubscribe(AbstractSubscription s) {
this.subscription = s;
try {
parser.onSubscribe(s);
} catch (Throwable t) {
cf.completeExceptionally(t);
throw t;
}
}
@Override
final void handle(ByteBuffer b,
BodyParser parser,
CompletableFuture<State> cf) {
assert cf != null : "parsing not started";
assert parser != null : "no parser";
try {
if (debug.on())
debug.log("Sending " + b.remaining() + "/" + b.capacity()
+ " bytes to body parser");
parser.accept(b);
} catch (Throwable t) {
if (debug.on())
debug.log("Body parser failed to handle buffer: " + t);
if (!cf.isDone()) {
cf.completeExceptionally(t);
}
}
}
final void onComplete(Throwable closedExceptionally) {
if (cf.isDone()) return;
if (closedExceptionally != null) {
cf.completeExceptionally(closedExceptionally);
} else {
onComplete.accept(State.READING_BODY);
cf.complete(State.READING_BODY);
}
}
@Override
public final void close(Throwable error) {
CompletableFuture<State> cf = this.cf;
if (cf != null && !cf.isDone()) {
// we want to make sure dependent actions are triggered
// in order to make sure the client reference count
// is decremented
if (error != null) {
if (debug.on())
debug.log("close: completing body parser CF with " + error);
cf.completeExceptionally(error);
} else {
if (debug.on())
debug.log("close: completing body parser CF");
cf.complete(State.READING_BODY);
}
}
}
@Override
public String toString() {
return super.toString() + "/parser=" + String.valueOf(parser);
}
}
}

View file

@ -0,0 +1,257 @@
/*
* Copyright (c) 2015, 2018, 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.net.InetSocketAddress;
import java.net.URI;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletableFuture;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.Utils;
import jdk.internal.net.http.frame.SettingsFrame;
import static jdk.internal.net.http.frame.SettingsFrame.INITIAL_WINDOW_SIZE;
import static jdk.internal.net.http.frame.SettingsFrame.ENABLE_PUSH;
import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE;
import static jdk.internal.net.http.frame.SettingsFrame.MAX_CONCURRENT_STREAMS;
import static jdk.internal.net.http.frame.SettingsFrame.MAX_FRAME_SIZE;
/**
* Http2 specific aspects of HttpClientImpl
*/
class Http2ClientImpl {
final static Logger debug =
Utils.getDebugLogger("Http2ClientImpl"::toString, Utils.DEBUG);
private final HttpClientImpl client;
Http2ClientImpl(HttpClientImpl client) {
this.client = client;
}
/* Map key is "scheme:host:port" */
private final Map<String,Http2Connection> connections = new ConcurrentHashMap<>();
private final Set<String> failures = Collections.synchronizedSet(new HashSet<>());
/**
* When HTTP/2 requested only. The following describes the aggregate behavior including the
* calling code. In all cases, the HTTP2 connection cache
* is checked first for a suitable connection and that is returned if available.
* If not, a new connection is opened, except in https case when a previous negotiate failed.
* In that case, we want to continue using http/1.1. When a connection is to be opened and
* if multiple requests are sent in parallel then each will open a new connection.
*
* If negotiation/upgrade succeeds then
* one connection will be put in the cache and the others will be closed
* after the initial request completes (not strictly necessary for h2, only for h2c)
*
* If negotiate/upgrade fails, then any opened connections remain open (as http/1.1)
* and will be used and cached in the http/1 cache. Note, this method handles the
* https failure case only (by completing the CF with an ALPN exception, handled externally)
* The h2c upgrade is handled externally also.
*
* Specific CF behavior of this method.
* 1. completes with ALPN exception: h2 negotiate failed for first time. failure recorded.
* 2. completes with other exception: failure not recorded. Caller must handle
* 3. completes normally with null: no connection in cache for h2c or h2 failed previously
* 4. completes normally with connection: h2 or h2c connection in cache. Use it.
*/
CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req) {
URI uri = req.uri();
InetSocketAddress proxy = req.proxy();
String key = Http2Connection.keyFor(uri, proxy);
synchronized (this) {
Http2Connection connection = connections.get(key);
if (connection != null) {
if (connection.closed) {
if (debug.on())
debug.log("removing found closed connection: %s", connection);
connections.remove(key);
} else {
// fast path if connection already exists
if (debug.on())
debug.log("found connection in the pool: %s", connection);
return MinimalFuture.completedFuture(connection);
}
}
if (!req.secure() || failures.contains(key)) {
// secure: negotiate failed before. Use http/1.1
// !secure: no connection available in cache. Attempt upgrade
if (debug.on()) debug.log("not found in connection pool");
return MinimalFuture.completedFuture(null);
}
}
return Http2Connection
.createAsync(req, this)
.whenComplete((conn, t) -> {
synchronized (Http2ClientImpl.this) {
if (conn != null) {
offerConnection(conn);
} else {
Throwable cause = Utils.getCompletionCause(t);
if (cause instanceof Http2Connection.ALPNException)
failures.add(key);
}
}
});
}
/*
* Cache the given connection, if no connection to the same
* destination exists. If one exists, then we let the initial stream
* complete but allow it to close itself upon completion.
* This situation should not arise with https because the request
* has not been sent as part of the initial alpn negotiation
*/
boolean offerConnection(Http2Connection c) {
if (debug.on()) debug.log("offering to the connection pool: %s", c);
if (c.closed) {
if (debug.on())
debug.log("skipping offered closed connection: %s", c);
return false;
}
String key = c.key();
synchronized(this) {
Http2Connection c1 = connections.putIfAbsent(key, c);
if (c1 != null) {
c.setSingleStream(true);
if (debug.on())
debug.log("existing entry in connection pool for %s", key);
return false;
}
if (debug.on())
debug.log("put in the connection pool: %s", c);
return true;
}
}
void deleteConnection(Http2Connection c) {
if (debug.on())
debug.log("removing from the connection pool: %s", c);
synchronized (this) {
connections.remove(c.key());
if (debug.on())
debug.log("removed from the connection pool: %s", c);
}
}
void stop() {
if (debug.on()) debug.log("stopping");
connections.values().forEach(this::close);
connections.clear();
}
private void close(Http2Connection h2c) {
try { h2c.close(); } catch (Throwable t) {}
}
HttpClientImpl client() {
return client;
}
/** Returns the client settings as a base64 (url) encoded string */
String getSettingsString() {
SettingsFrame sf = getClientSettings();
byte[] settings = sf.toByteArray(); // without the header
Base64.Encoder encoder = Base64.getUrlEncoder()
.withoutPadding();
return encoder.encodeToString(settings);
}
private static final int K = 1024;
private static int getParameter(String property, int min, int max, int defaultValue) {
int value = Utils.getIntegerNetProperty(property, defaultValue);
// use default value if misconfigured
if (value < min || value > max) {
Log.logError("Property value for {0}={1} not in [{2}..{3}]: " +
"using default={4}", property, value, min, max, defaultValue);
value = defaultValue;
}
return value;
}
// used for the connection window, to have a connection window size
// bigger than the initial stream window size.
int getConnectionWindowSize(SettingsFrame clientSettings) {
// Maximum size is 2^31-1. Don't allow window size to be less
// than the stream window size. HTTP/2 specify a default of 64 * K -1,
// but we use 2^26 by default for better performance.
int streamWindow = clientSettings.getParameter(INITIAL_WINDOW_SIZE);
// The default is the max between the stream window size
// and the connection window size.
int defaultValue = Math.min(Integer.MAX_VALUE,
Math.max(streamWindow, K*K*32));
return getParameter(
"jdk.httpclient.connectionWindowSize",
streamWindow, Integer.MAX_VALUE, defaultValue);
}
SettingsFrame getClientSettings() {
SettingsFrame frame = new SettingsFrame();
// default defined for HTTP/2 is 4 K, we use 16 K.
frame.setParameter(HEADER_TABLE_SIZE, getParameter(
"jdk.httpclient.hpack.maxheadertablesize",
0, Integer.MAX_VALUE, 16 * K));
// O: does not accept push streams. 1: accepts push streams.
frame.setParameter(ENABLE_PUSH, getParameter(
"jdk.httpclient.enablepush",
0, 1, 1));
// HTTP/2 recommends to set the number of concurrent streams
// no lower than 100. We use 100. 0 means no stream would be
// accepted. That would render the client to be non functional,
// so we won't let 0 be configured for our Http2ClientImpl.
frame.setParameter(MAX_CONCURRENT_STREAMS, getParameter(
"jdk.httpclient.maxstreams",
1, Integer.MAX_VALUE, 100));
// Maximum size is 2^31-1. Don't allow window size to be less
// than the minimum frame size as this is likely to be a
// configuration error. HTTP/2 specify a default of 64 * K -1,
// but we use 16 M for better performance.
frame.setParameter(INITIAL_WINDOW_SIZE, getParameter(
"jdk.httpclient.windowsize",
16 * K, Integer.MAX_VALUE, 16*K*K));
// HTTP/2 specify a minimum size of 16 K, a maximum size of 2^24-1,
// and a default of 16 K. We use 16 K as default.
frame.setParameter(MAX_FRAME_SIZE, getParameter(
"jdk.httpclient.maxframesize",
16 * K, 16 * K * K -1, 16 * K));
return frame;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,127 @@
/*
* Copyright (c) 2015, 2018, 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.net.Authenticator;
import java.net.CookieHandler;
import java.net.ProxySelector;
import java.util.concurrent.Executor;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import java.net.http.HttpClient;
import jdk.internal.net.http.common.Utils;
import static java.util.Objects.requireNonNull;
public class HttpClientBuilderImpl implements HttpClient.Builder {
CookieHandler cookieHandler;
HttpClient.Redirect followRedirects;
ProxySelector proxy;
Authenticator authenticator;
HttpClient.Version version;
Executor executor;
// Security parameters
SSLContext sslContext;
SSLParameters sslParams;
int priority = -1;
@Override
public HttpClientBuilderImpl cookieHandler(CookieHandler cookieHandler) {
requireNonNull(cookieHandler);
this.cookieHandler = cookieHandler;
return this;
}
@Override
public HttpClientBuilderImpl sslContext(SSLContext sslContext) {
requireNonNull(sslContext);
this.sslContext = sslContext;
return this;
}
@Override
public HttpClientBuilderImpl sslParameters(SSLParameters sslParameters) {
requireNonNull(sslParameters);
this.sslParams = Utils.copySSLParameters(sslParameters);
return this;
}
@Override
public HttpClientBuilderImpl executor(Executor s) {
requireNonNull(s);
this.executor = s;
return this;
}
@Override
public HttpClientBuilderImpl followRedirects(HttpClient.Redirect policy) {
requireNonNull(policy);
this.followRedirects = policy;
return this;
}
@Override
public HttpClientBuilderImpl version(HttpClient.Version version) {
requireNonNull(version);
this.version = version;
return this;
}
@Override
public HttpClientBuilderImpl priority(int priority) {
if (priority < 1 || priority > 256) {
throw new IllegalArgumentException("priority must be between 1 and 256");
}
this.priority = priority;
return this;
}
@Override
public HttpClientBuilderImpl proxy(ProxySelector proxy) {
requireNonNull(proxy);
this.proxy = proxy;
return this;
}
@Override
public HttpClientBuilderImpl authenticator(Authenticator a) {
requireNonNull(a);
this.authenticator = a;
return this;
}
@Override
public HttpClient build() {
return HttpClientImpl.create(this);
}
}

View file

@ -0,0 +1,155 @@
/*
* Copyright (c) 2017, 2018, 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.lang.ref.Reference;
import java.net.Authenticator;
import java.net.CookieHandler;
import java.net.ProxySelector;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.PushPromiseHandler;
import java.net.http.WebSocket;
import jdk.internal.net.http.common.OperationTrackers.Trackable;
import jdk.internal.net.http.common.OperationTrackers.Tracker;
/**
* An HttpClientFacade is a simple class that wraps an HttpClient implementation
* and delegates everything to its implementation delegate.
*/
final class HttpClientFacade extends HttpClient implements Trackable {
final HttpClientImpl impl;
/**
* Creates an HttpClientFacade.
*/
HttpClientFacade(HttpClientImpl impl) {
this.impl = impl;
}
@Override // for tests
public Tracker getOperationsTracker() {
return impl.getOperationsTracker();
}
@Override
public Optional<CookieHandler> cookieHandler() {
return impl.cookieHandler();
}
@Override
public Redirect followRedirects() {
return impl.followRedirects();
}
@Override
public Optional<ProxySelector> proxy() {
return impl.proxy();
}
@Override
public SSLContext sslContext() {
return impl.sslContext();
}
@Override
public SSLParameters sslParameters() {
return impl.sslParameters();
}
@Override
public Optional<Authenticator> authenticator() {
return impl.authenticator();
}
@Override
public HttpClient.Version version() {
return impl.version();
}
@Override
public Optional<Executor> executor() {
return impl.executor();
}
@Override
public <T> HttpResponse<T>
send(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)
throws IOException, InterruptedException
{
try {
return impl.send(req, responseBodyHandler);
} finally {
Reference.reachabilityFence(this);
}
}
@Override
public <T> CompletableFuture<HttpResponse<T>>
sendAsync(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler) {
try {
return impl.sendAsync(req, responseBodyHandler);
} finally {
Reference.reachabilityFence(this);
}
}
@Override
public <T> CompletableFuture<HttpResponse<T>>
sendAsync(HttpRequest req,
BodyHandler<T> responseBodyHandler,
PushPromiseHandler<T> pushPromiseHandler){
try {
return impl.sendAsync(req, responseBodyHandler, pushPromiseHandler);
} finally {
Reference.reachabilityFence(this);
}
}
@Override
public WebSocket.Builder newWebSocketBuilder() {
try {
return impl.newWebSocketBuilder();
} finally {
Reference.reachabilityFence(this);
}
}
@Override
public String toString() {
// Used by tests to get the client's id.
return impl.toString();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,478 @@
/*
* Copyright (c) 2015, 2018, 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.Closeable;
import java.io.IOException;
import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.Flow;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Version;
import java.net.http.HttpHeaders;
import jdk.internal.net.http.common.Demand;
import jdk.internal.net.http.common.FlowTube;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.SequentialScheduler;
import jdk.internal.net.http.common.SequentialScheduler.DeferredCompleter;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Utils;
import static java.net.http.HttpClient.Version.HTTP_2;
/**
* Wraps socket channel layer and takes care of SSL also.
*
* Subtypes are:
* PlainHttpConnection: regular direct TCP connection to server
* PlainProxyConnection: plain text proxy connection
* PlainTunnelingConnection: opens plain text (CONNECT) tunnel to server
* AsyncSSLConnection: TLS channel direct to server
* AsyncSSLTunnelConnection: TLS channel via (CONNECT) proxy tunnel
*/
abstract class HttpConnection implements Closeable {
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
final static Logger DEBUG_LOGGER = Utils.getDebugLogger(
() -> "HttpConnection(SocketTube(?))", Utils.DEBUG);
/** The address this connection is connected to. Could be a server or a proxy. */
final InetSocketAddress address;
private final HttpClientImpl client;
private final TrailingOperations trailingOperations;
HttpConnection(InetSocketAddress address, HttpClientImpl client) {
this.address = address;
this.client = client;
trailingOperations = new TrailingOperations();
}
private static final class TrailingOperations {
private final Map<CompletionStage<?>, Boolean> operations =
new IdentityHashMap<>();
void add(CompletionStage<?> cf) {
synchronized(operations) {
cf.whenComplete((r,t)-> remove(cf));
operations.put(cf, Boolean.TRUE);
}
}
boolean remove(CompletionStage<?> cf) {
synchronized(operations) {
return operations.remove(cf);
}
}
}
final void addTrailingOperation(CompletionStage<?> cf) {
trailingOperations.add(cf);
}
// final void removeTrailingOperation(CompletableFuture<?> cf) {
// trailingOperations.remove(cf);
// }
final HttpClientImpl client() {
return client;
}
//public abstract void connect() throws IOException, InterruptedException;
public abstract CompletableFuture<Void> connectAsync();
/** Tells whether, or not, this connection is connected to its destination. */
abstract boolean connected();
/** Tells whether, or not, this connection is secure ( over SSL ) */
abstract boolean isSecure();
/**
* Tells whether, or not, this connection is proxied.
* Returns true for tunnel connections, or clear connection to
* any host through proxy.
*/
abstract boolean isProxied();
/** Tells whether, or not, this connection is open. */
final boolean isOpen() {
return channel().isOpen() &&
(connected() ? !getConnectionFlow().isFinished() : true);
}
interface HttpPublisher extends FlowTube.TubePublisher {
void enqueue(List<ByteBuffer> buffers) throws IOException;
void enqueueUnordered(List<ByteBuffer> buffers) throws IOException;
void signalEnqueued() throws IOException;
}
/**
* Returns the HTTP publisher associated with this connection. May be null
* if invoked before connecting.
*/
abstract HttpPublisher publisher();
// HTTP/2 MUST use TLS version 1.2 or higher for HTTP/2 over TLS
private static final Predicate<String> testRequiredHTTP2TLSVersion = proto ->
proto.equals("TLSv1.2") || proto.equals("TLSv1.3");
/**
* Returns true if the given client's SSL parameter protocols contains at
* least one TLS version that HTTP/2 requires.
*/
private static final boolean hasRequiredHTTP2TLSVersion(HttpClient client) {
String[] protos = client.sslParameters().getProtocols();
if (protos != null) {
return Arrays.stream(protos).filter(testRequiredHTTP2TLSVersion).findAny().isPresent();
} else {
return false;
}
}
/**
* Factory for retrieving HttpConnections. A connection can be retrieved
* from the connection pool, or a new one created if none available.
*
* The given {@code addr} is the ultimate destination. Any proxies,
* etc, are determined from the request. Returns a concrete instance which
* is one of the following:
* {@link PlainHttpConnection}
* {@link PlainTunnelingConnection}
*
* The returned connection, if not from the connection pool, must have its,
* connect() or connectAsync() method invoked, which ( when it completes
* successfully ) renders the connection usable for requests.
*/
public static HttpConnection getConnection(InetSocketAddress addr,
HttpClientImpl client,
HttpRequestImpl request,
Version version) {
// The default proxy selector may select a proxy whose address is
// unresolved. We must resolve the address before connecting to it.
InetSocketAddress proxy = Utils.resolveAddress(request.proxy());
HttpConnection c = null;
boolean secure = request.secure();
ConnectionPool pool = client.connectionPool();
if (!secure) {
c = pool.getConnection(false, addr, proxy);
if (c != null && c.isOpen() /* may have been eof/closed when in the pool */) {
final HttpConnection conn = c;
if (DEBUG_LOGGER.on())
DEBUG_LOGGER.log(conn.getConnectionFlow()
+ ": plain connection retrieved from HTTP/1.1 pool");
return c;
} else {
return getPlainConnection(addr, proxy, request, client);
}
} else { // secure
if (version != HTTP_2) { // only HTTP/1.1 connections are in the pool
c = pool.getConnection(true, addr, proxy);
}
if (c != null && c.isOpen()) {
final HttpConnection conn = c;
if (DEBUG_LOGGER.on())
DEBUG_LOGGER.log(conn.getConnectionFlow()
+ ": SSL connection retrieved from HTTP/1.1 pool");
return c;
} else {
String[] alpn = null;
if (version == HTTP_2 && hasRequiredHTTP2TLSVersion(client)) {
alpn = new String[] { "h2", "http/1.1" };
}
return getSSLConnection(addr, proxy, alpn, request, client);
}
}
}
private static HttpConnection getSSLConnection(InetSocketAddress addr,
InetSocketAddress proxy,
String[] alpn,
HttpRequestImpl request,
HttpClientImpl client) {
if (proxy != null)
return new AsyncSSLTunnelConnection(addr, client, alpn, proxy,
proxyTunnelHeaders(request));
else
return new AsyncSSLConnection(addr, client, alpn);
}
/**
* This method is used to build a filter that will accept or
* veto (header-name, value) tuple for transmission on the
* wire.
* The filter is applied to the headers when sending the headers
* to the remote party.
* Which tuple is accepted/vetoed depends on:
* <pre>
* - whether the connection is a tunnel connection
* [talking to a server through a proxy tunnel]
* - whether the method is CONNECT
* [establishing a CONNECT tunnel through a proxy]
* - whether the request is using a proxy
* (and the connection is not a tunnel)
* [talking to a server through a proxy]
* - whether the request is a direct connection to
* a server (no tunnel, no proxy).
* </pre>
* @param request
* @return
*/
BiPredicate<String,List<String>> headerFilter(HttpRequestImpl request) {
if (isTunnel()) {
// talking to a server through a proxy tunnel
// don't send proxy-* headers to a plain server
assert !request.isConnect();
return Utils.NO_PROXY_HEADERS_FILTER;
} else if (request.isConnect()) {
// establishing a proxy tunnel
// check for proxy tunnel disabled schemes
// assert !this.isTunnel();
assert request.proxy() == null;
return Utils.PROXY_TUNNEL_FILTER;
} else if (request.proxy() != null) {
// talking to a server through a proxy (no tunnel)
// check for proxy disabled schemes
// assert !isTunnel() && !request.isConnect();
return Utils.PROXY_FILTER;
} else {
// talking to a server directly (no tunnel, no proxy)
// don't send proxy-* headers to a plain server
// assert request.proxy() == null && !request.isConnect();
return Utils.NO_PROXY_HEADERS_FILTER;
}
}
// Composes a new immutable HttpHeaders that combines the
// user and system header but only keeps those headers that
// start with "proxy-"
private static HttpHeaders proxyTunnelHeaders(HttpRequestImpl request) {
Map<String, List<String>> combined = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
combined.putAll(request.getSystemHeaders().map());
combined.putAll(request.headers().map()); // let user override system
// keep only proxy-* - and also strip authorization headers
// for disabled schemes
return ImmutableHeaders.of(combined, Utils.PROXY_TUNNEL_FILTER);
}
/* Returns either a plain HTTP connection or a plain tunnelling connection
* for proxied WebSocket */
private static HttpConnection getPlainConnection(InetSocketAddress addr,
InetSocketAddress proxy,
HttpRequestImpl request,
HttpClientImpl client) {
if (request.isWebSocket() && proxy != null)
return new PlainTunnelingConnection(addr, proxy, client,
proxyTunnelHeaders(request));
if (proxy == null)
return new PlainHttpConnection(addr, client);
else
return new PlainProxyConnection(proxy, client);
}
void closeOrReturnToCache(HttpHeaders hdrs) {
if (hdrs == null) {
// the connection was closed by server, eof
close();
return;
}
if (!isOpen()) {
return;
}
HttpClientImpl client = client();
if (client == null) {
close();
return;
}
ConnectionPool pool = client.connectionPool();
boolean keepAlive = hdrs.firstValue("Connection")
.map((s) -> !s.equalsIgnoreCase("close"))
.orElse(true);
if (keepAlive) {
Log.logTrace("Returning connection to the pool: {0}", this);
pool.returnToPool(this);
} else {
close();
}
}
/* Tells whether or not this connection is a tunnel through a proxy */
boolean isTunnel() { return false; }
abstract SocketChannel channel();
final InetSocketAddress address() {
return address;
}
abstract ConnectionPool.CacheKey cacheKey();
/**
* Closes this connection, by returning the socket to its connection pool.
*/
@Override
public abstract void close();
abstract FlowTube getConnectionFlow();
/**
* A publisher that makes it possible to publish (write) ordered (normal
* priority) and unordered (high priority) buffers downstream.
*/
final class PlainHttpPublisher implements HttpPublisher {
final Object reading;
PlainHttpPublisher() {
this(new Object());
}
PlainHttpPublisher(Object readingLock) {
this.reading = readingLock;
}
final ConcurrentLinkedDeque<List<ByteBuffer>> queue = new ConcurrentLinkedDeque<>();
final ConcurrentLinkedDeque<List<ByteBuffer>> priority = new ConcurrentLinkedDeque<>();
volatile Flow.Subscriber<? super List<ByteBuffer>> subscriber;
volatile HttpWriteSubscription subscription;
final SequentialScheduler writeScheduler =
new SequentialScheduler(this::flushTask);
@Override
public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
synchronized (reading) {
//assert this.subscription == null;
//assert this.subscriber == null;
if (subscription == null) {
subscription = new HttpWriteSubscription();
}
this.subscriber = subscriber;
}
// TODO: should we do this in the flow?
subscriber.onSubscribe(subscription);
signal();
}
void flushTask(DeferredCompleter completer) {
try {
HttpWriteSubscription sub = subscription;
if (sub != null) sub.flush();
} finally {
completer.complete();
}
}
void signal() {
writeScheduler.runOrSchedule();
}
final class HttpWriteSubscription implements Flow.Subscription {
final Demand demand = new Demand();
@Override
public void request(long n) {
if (n <= 0) throw new IllegalArgumentException("non-positive request");
demand.increase(n);
if (debug.on())
debug.log("HttpPublisher: got request of " + n + " from "
+ getConnectionFlow());
writeScheduler.runOrSchedule();
}
@Override
public void cancel() {
if (debug.on())
debug.log("HttpPublisher: cancelled by " + getConnectionFlow());
}
private boolean isEmpty() {
return queue.isEmpty() && priority.isEmpty();
}
private List<ByteBuffer> poll() {
List<ByteBuffer> elem = priority.poll();
return elem == null ? queue.poll() : elem;
}
void flush() {
while (!isEmpty() && demand.tryDecrement()) {
List<ByteBuffer> elem = poll();
if (debug.on())
debug.log("HttpPublisher: sending "
+ Utils.remaining(elem) + " bytes ("
+ elem.size() + " buffers) to "
+ getConnectionFlow());
subscriber.onNext(elem);
}
}
}
@Override
public void enqueue(List<ByteBuffer> buffers) throws IOException {
queue.add(buffers);
int bytes = buffers.stream().mapToInt(ByteBuffer::remaining).sum();
debug.log("added %d bytes to the write queue", bytes);
}
@Override
public void enqueueUnordered(List<ByteBuffer> buffers) throws IOException {
// Unordered frames are sent before existing frames.
int bytes = buffers.stream().mapToInt(ByteBuffer::remaining).sum();
priority.add(buffers);
debug.log("added %d bytes in the priority write queue", bytes);
}
@Override
public void signalEnqueued() throws IOException {
debug.log("signalling the publisher of the write queue");
signal();
}
}
String dbgTag;
final String dbgString() {
FlowTube flow = getConnectionFlow();
String tag = dbgTag;
if (tag == null && flow != null) {
dbgTag = tag = this.getClass().getSimpleName() + "(" + flow + ")";
} else if (tag == null) {
tag = this.getClass().getSimpleName() + "(?)";
}
return tag;
}
@Override
public String toString() {
return "HttpConnection: " + channel().toString();
}
}

View file

@ -0,0 +1,233 @@
/*
* Copyright (c) 2015, 2018, 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.net.URI;
import java.time.Duration;
import java.util.Locale;
import java.util.Optional;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublisher;
import jdk.internal.net.http.common.HttpHeadersImpl;
import jdk.internal.net.http.common.Utils;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static jdk.internal.net.http.common.Utils.isValidName;
import static jdk.internal.net.http.common.Utils.isValidValue;
import static jdk.internal.net.http.common.Utils.newIAE;
public class HttpRequestBuilderImpl implements HttpRequest.Builder {
private HttpHeadersImpl userHeaders;
private URI uri;
private String method;
private boolean expectContinue;
private BodyPublisher bodyPublisher;
private volatile Optional<HttpClient.Version> version;
private Duration duration;
public HttpRequestBuilderImpl(URI uri) {
requireNonNull(uri, "uri must be non-null");
checkURI(uri);
this.uri = uri;
this.userHeaders = new HttpHeadersImpl();
this.method = "GET"; // default, as per spec
this.version = Optional.empty();
}
public HttpRequestBuilderImpl() {
this.userHeaders = new HttpHeadersImpl();
this.method = "GET"; // default, as per spec
this.version = Optional.empty();
}
@Override
public HttpRequestBuilderImpl uri(URI uri) {
requireNonNull(uri, "uri must be non-null");
checkURI(uri);
this.uri = uri;
return this;
}
static void checkURI(URI uri) {
String scheme = uri.getScheme();
if (scheme == null)
throw newIAE("URI with undefined scheme");
scheme = scheme.toLowerCase(Locale.US);
if (!(scheme.equals("https") || scheme.equals("http"))) {
throw newIAE("invalid URI scheme %s", scheme);
}
if (uri.getHost() == null) {
throw newIAE("unsupported URI %s", uri);
}
}
@Override
public HttpRequestBuilderImpl copy() {
HttpRequestBuilderImpl b = new HttpRequestBuilderImpl();
b.uri = this.uri;
b.userHeaders = this.userHeaders.deepCopy();
b.method = this.method;
b.expectContinue = this.expectContinue;
b.bodyPublisher = bodyPublisher;
b.uri = uri;
b.duration = duration;
b.version = version;
return b;
}
private void checkNameAndValue(String name, String value) {
requireNonNull(name, "name");
requireNonNull(value, "value");
if (!isValidName(name)) {
throw newIAE("invalid header name: \"%s\"", name);
}
if (!Utils.ALLOWED_HEADERS.test(name)) {
throw newIAE("restricted header name: \"%s\"", name);
}
if (!isValidValue(value)) {
throw newIAE("invalid header value: \"%s\"", value);
}
}
@Override
public HttpRequestBuilderImpl setHeader(String name, String value) {
checkNameAndValue(name, value);
userHeaders.setHeader(name, value);
return this;
}
@Override
public HttpRequestBuilderImpl header(String name, String value) {
checkNameAndValue(name, value);
userHeaders.addHeader(name, value);
return this;
}
@Override
public HttpRequestBuilderImpl headers(String... params) {
requireNonNull(params);
if (params.length == 0 || params.length % 2 != 0) {
throw newIAE("wrong number, %d, of parameters", params.length);
}
for (int i = 0; i < params.length; i += 2) {
String name = params[i];
String value = params[i + 1];
header(name, value);
}
return this;
}
@Override
public HttpRequestBuilderImpl expectContinue(boolean enable) {
expectContinue = enable;
return this;
}
@Override
public HttpRequestBuilderImpl version(HttpClient.Version version) {
requireNonNull(version);
this.version = Optional.of(version);
return this;
}
HttpHeadersImpl headers() { return userHeaders; }
URI uri() { return uri; }
String method() { return method; }
boolean expectContinue() { return expectContinue; }
BodyPublisher bodyPublisher() { return bodyPublisher; }
Optional<HttpClient.Version> version() { return version; }
@Override
public HttpRequest.Builder GET() {
return method0("GET", null);
}
@Override
public HttpRequest.Builder POST(BodyPublisher body) {
return method0("POST", requireNonNull(body));
}
@Override
public HttpRequest.Builder DELETE() {
return method0("DELETE", null);
}
@Override
public HttpRequest.Builder PUT(BodyPublisher body) {
return method0("PUT", requireNonNull(body));
}
@Override
public HttpRequest.Builder method(String method, BodyPublisher body) {
requireNonNull(method);
if (method.equals(""))
throw newIAE("illegal method <empty string>");
if (method.equals("CONNECT"))
throw newIAE("method CONNECT is not supported");
if (!Utils.isValidName(method))
throw newIAE("illegal method \""
+ method.replace("\n","\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
+ "\"");
return method0(method, requireNonNull(body));
}
private HttpRequest.Builder method0(String method, BodyPublisher body) {
assert method != null;
assert !method.equals("");
this.method = method;
this.bodyPublisher = body;
return this;
}
@Override
public HttpRequest build() {
if (uri == null)
throw new IllegalStateException("uri is null");
assert method != null;
return new HttpRequestImpl(this);
}
@Override
public HttpRequest.Builder timeout(Duration duration) {
requireNonNull(duration);
if (duration.isNegative() || Duration.ZERO.equals(duration))
throw new IllegalArgumentException("Invalid duration: " + duration);
this.duration = duration;
return this;
}
Duration timeout() { return duration; }
}

View file

@ -0,0 +1,371 @@
/*
* Copyright (c) 2015, 2018, 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.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
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 jdk.internal.net.http.common.HttpHeadersImpl;
import jdk.internal.net.http.common.Utils;
import jdk.internal.net.http.websocket.WebSocketRequest;
import static jdk.internal.net.http.common.Utils.ALLOWED_HEADERS;
class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
private final HttpHeaders userHeaders;
private final HttpHeadersImpl systemHeaders;
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 volatile AccessControlContext acc;
private final Duration timeout; // may be null
private final Optional<HttpClient.Version> version;
private static String userAgent() {
PrivilegedAction<String> pa = () -> System.getProperty("java.version");
String version = AccessController.doPrivileged(pa);
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 = ImmutableHeaders.of(builder.headers().map(), ALLOWED_HEADERS);
this.systemHeaders = new HttpHeadersImpl();
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 = ImmutableHeaders.validate(request.headers());
if (request instanceof HttpRequestImpl) {
// all cases exception WebSocket should have a new system headers
this.isWebSocket = ((HttpRequestImpl) request).isWebSocket;
if (isWebSocket) {
this.systemHeaders = ((HttpRequestImpl) request).systemHeaders;
} else {
this.systemHeaders = new HttpHeadersImpl();
}
} else {
HttpRequestBuilderImpl.checkURI(requestURI);
checkTimeout(timeout);
this.systemHeaders = new HttpHeadersImpl();
}
this.systemHeaders.setHeader("User-Agent", USER_AGENT);
this.uri = requestURI;
if (isWebSocket) {
// WebSocket determines and sets the proxy itself
this.proxy = ((HttpRequestImpl) request).proxy;
} else {
if (ps != null)
this.proxy = retrieveProxy(ps, uri);
else
this.proxy = null;
}
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) {
return new HttpRequestImpl(uri, method, other);
}
/** Returns a new instance suitable for authentication. */
public static HttpRequestImpl newInstanceForAuthentication(HttpRequestImpl other) {
return new HttpRequestImpl(other.uri(), other.method(), other);
}
/**
* 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) {
assert method == null || Utils.isValidName(method);
this.method = method == null? "GET" : method;
this.userHeaders = other.userHeaders;
this.isWebSocket = other.isWebSocket;
this.systemHeaders = new HttpHeadersImpl();
this.systemHeaders.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 = other.requestPublisher; // may be null
this.acc = other.acc;
this.timeout = other.timeout;
this.version = other.version();
this.authority = null;
}
/* used for creating CONNECT requests */
HttpRequestImpl(String method, InetSocketAddress authority, HttpHeaders 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.systemHeaders = new HttpHeadersImpl();
this.userHeaders = ImmutableHeaders.of(headers);
this.uri = URI.create("socket://" + authority.getHostString() + ":"
+ Integer.toString(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,
HttpHeadersImpl headers)
throws IOException
{
return new HttpRequestImpl(parent, headers);
}
// only used for push requests
private HttpRequestImpl(HttpRequestImpl parent, HttpHeadersImpl 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 = ImmutableHeaders.of(headers.map(), ALLOWED_HEADERS);
this.systemHeaders = parent.systemHeaders;
this.expectContinue = parent.expectContinue;
this.secure = parent.secure;
this.requestPublisher = parent.requestPublisher;
this.acc = parent.acc;
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(Http2ClientImpl h2client) {
systemHeaders.setHeader("Connection", "Upgrade, HTTP2-Settings");
systemHeaders.setHeader("Upgrade", "h2c");
systemHeaders.setHeader("HTTP2-Settings", h2client.getSettingsString());
}
@Override
public boolean expectContinue() { return expectContinue; }
/** Retrieves the proxy, from the given ProxySelector, if there is one. */
private static Proxy retrieveProxy(ProxySelector ps, URI uri) {
Proxy proxy = null;
List<Proxy> pl = ps.select(uri);
if (!pl.isEmpty()) {
Proxy p = pl.get(0);
if (p.type() == Proxy.Type.HTTP)
proxy = p;
}
return proxy;
}
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 = proxy;
}
@Override
public void isWebSocket(boolean is) {
isWebSocket = is;
}
boolean isWebSocket() {
return isWebSocket;
}
@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; }
HttpHeadersImpl getSystemHeaders() { return systemHeaders; }
@Override
public Optional<HttpClient.Version> version() { return version; }
void addSystemHeader(String name, String value) {
systemHeaders.addHeader(name, value);
}
@Override
public void setSystemHeader(String name, String value) {
systemHeaders.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) {
PrivilegedAction<InetSocketAddress> pa = () -> new InetSocketAddress(host, port);
return AccessController.doPrivileged(pa);
} else {
return InetSocketAddress.createUnresolved(host, port);
}
}
}

View file

@ -0,0 +1,177 @@
/*
* Copyright (c) 2015, 2018, 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.URI;
import java.nio.ByteBuffer;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import javax.net.ssl.SSLSession;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import jdk.internal.net.http.websocket.RawChannel;
/**
* The implementation class for HttpResponse
*/
class HttpResponseImpl<T> implements HttpResponse<T>, RawChannel.Provider {
final int responseCode;
final Exchange<T> exchange;
final HttpRequest initialRequest;
final Optional<HttpResponse<T>> previousResponse;
final HttpHeaders headers;
final Optional<SSLSession> sslSession;
final URI uri;
final HttpClient.Version version;
RawChannel rawchan;
final HttpConnection connection;
final Stream<T> stream;
final T body;
public HttpResponseImpl(HttpRequest initialRequest,
Response response,
HttpResponse<T> previousResponse,
T body,
Exchange<T> exch) {
this.responseCode = response.statusCode();
this.exchange = exch;
this.initialRequest = initialRequest;
this.previousResponse = Optional.ofNullable(previousResponse);
this.headers = response.headers();
//this.trailers = trailers;
this.sslSession = Optional.ofNullable(response.getSSLSession());
this.uri = response.request().uri();
this.version = response.version();
this.connection = connection(exch);
this.stream = null;
this.body = body;
}
private HttpConnection connection(Exchange<?> exch) {
if (exch == null || exch.exchImpl == null) {
assert responseCode == 407;
return null; // case of Proxy 407
}
return exch.exchImpl.connection();
}
private ExchangeImpl<?> exchangeImpl() {
return exchange != null ? exchange.exchImpl : stream;
}
@Override
public int statusCode() {
return responseCode;
}
@Override
public HttpRequest request() {
return initialRequest;
}
@Override
public Optional<HttpResponse<T>> previousResponse() {
return previousResponse;
}
@Override
public HttpHeaders headers() {
return headers;
}
@Override
public T body() {
return body;
}
@Override
public Optional<SSLSession> sslSession() {
return sslSession;
}
@Override
public URI uri() {
return uri;
}
@Override
public HttpClient.Version version() {
return version;
}
// keepalive flag determines whether connection is closed or kept alive
// by reading/skipping data
/**
* Returns a RawChannel that may be used for WebSocket protocol.
* @implNote This implementation does not support RawChannel over
* HTTP/2 connections.
* @return a RawChannel that may be used for WebSocket protocol.
* @throws UnsupportedOperationException if getting a RawChannel over
* this connection is not supported.
* @throws IOException if an I/O exception occurs while retrieving
* the channel.
*/
@Override
public synchronized RawChannel rawChannel() throws IOException {
if (rawchan == null) {
ExchangeImpl<?> exchImpl = exchangeImpl();
if (!(exchImpl instanceof Http1Exchange)) {
// RawChannel is only used for WebSocket - and WebSocket
// is not supported over HTTP/2 yet, so we should not come
// here. Getting a RawChannel over HTTP/2 might be supported
// in the future, but it would entail retrieving any left over
// bytes that might have been read but not consumed by the
// HTTP/2 connection.
throw new UnsupportedOperationException("RawChannel is not supported over HTTP/2");
}
// Http1Exchange may have some remaining bytes in its
// internal buffer.
Supplier<ByteBuffer> initial = ((Http1Exchange<?>)exchImpl)::drainLeftOverBytes;
rawchan = new RawChannelTube(connection, initial);
}
return rawchan;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
String method = request().method();
URI uri = request().uri();
String uristring = uri == null ? "" : uri.toString();
sb.append('(')
.append(method)
.append(" ")
.append(uristring)
.append(") ")
.append(statusCode());
return sb.toString();
}
}

View file

@ -0,0 +1,116 @@
/*
* Copyright (c) 2015, 2018, 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.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.net.http.HttpHeaders;
import jdk.internal.net.http.common.HttpHeadersImpl;
import jdk.internal.net.http.common.Utils;
import static java.util.Collections.emptyMap;
import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableMap;
import static java.util.Objects.requireNonNull;
final class ImmutableHeaders extends HttpHeaders {
private final Map<String, List<String>> map;
public static ImmutableHeaders empty() {
return of(emptyMap());
}
public static ImmutableHeaders of(Map<String, List<String>> src) {
return of(src, x -> true);
}
public static ImmutableHeaders of(HttpHeaders headers) {
return (headers instanceof ImmutableHeaders)
? (ImmutableHeaders)headers
: of(headers.map());
}
static ImmutableHeaders validate(HttpHeaders headers) {
if (headers instanceof ImmutableHeaders) {
return of(headers);
}
if (headers instanceof HttpHeadersImpl) {
return of(headers);
}
Map<String, List<String>> map = headers.map();
return new ImmutableHeaders(map, Utils.VALIDATE_USER_HEADER);
}
public static ImmutableHeaders of(Map<String, List<String>> src,
Predicate<? super String> keyAllowed) {
requireNonNull(src, "src");
requireNonNull(keyAllowed, "keyAllowed");
return new ImmutableHeaders(src, headerAllowed(keyAllowed));
}
public static ImmutableHeaders of(Map<String, List<String>> src,
BiPredicate<? super String, ? super List<String>> headerAllowed) {
requireNonNull(src, "src");
requireNonNull(headerAllowed, "headerAllowed");
return new ImmutableHeaders(src, headerAllowed);
}
private ImmutableHeaders(Map<String, List<String>> src,
BiPredicate<? super String, ? super List<String>> headerAllowed) {
Map<String, List<String>> m = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
src.entrySet().stream()
.forEach(e -> addIfAllowed(e, headerAllowed, m));
this.map = unmodifiableMap(m);
}
private static void addIfAllowed(Map.Entry<String, List<String>> e,
BiPredicate<? super String, ? super List<String>> headerAllowed,
Map<String, List<String>> map) {
String key = e.getKey();
List<String> values = unmodifiableValues(e.getValue());
if (headerAllowed.test(key, values)) {
map.put(key, values);
}
}
private static List<String> unmodifiableValues(List<String> values) {
return unmodifiableList(new ArrayList<>(Objects.requireNonNull(values)));
}
private static BiPredicate<String, List<String>> headerAllowed(Predicate<? super String> keyAllowed) {
return (n,v) -> keyAllowed.test(n);
}
@Override
public Map<String, List<String>> map() {
return map;
}
}

View file

@ -0,0 +1,466 @@
/*
* Copyright (c) 2018, 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.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.Flow;
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Subscription;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import jdk.internal.net.http.common.Demand;
import java.net.http.HttpResponse.BodySubscriber;
import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.SequentialScheduler;
/** An adapter between {@code BodySubscriber} and {@code Flow.Subscriber<String>}. */
public final class LineSubscriberAdapter<S extends Subscriber<? super String>,R>
implements BodySubscriber<R> {
private final CompletableFuture<R> cf = new MinimalFuture<>();
private final S subscriber;
private final Function<? super S, ? extends R> finisher;
private final Charset charset;
private final String eol;
private volatile LineSubscription downstream;
private LineSubscriberAdapter(S subscriber,
Function<? super S, ? extends R> finisher,
Charset charset,
String eol) {
if (eol != null && eol.isEmpty())
throw new IllegalArgumentException("empty line separator");
this.subscriber = Objects.requireNonNull(subscriber);
this.finisher = Objects.requireNonNull(finisher);
this.charset = Objects.requireNonNull(charset);
this.eol = eol;
}
@Override
public void onSubscribe(Subscription subscription) {
downstream = LineSubscription.create(subscription,
charset,
eol,
subscriber,
cf);
subscriber.onSubscribe(downstream);
}
@Override
public void onNext(List<ByteBuffer> item) {
try {
downstream.submit(item);
} catch (Throwable t) {
onError(t);
}
}
@Override
public void onError(Throwable throwable) {
try {
downstream.signalError(throwable);
} finally {
cf.completeExceptionally(throwable);
}
}
@Override
public void onComplete() {
try {
downstream.signalComplete();
} finally {
cf.complete(finisher.apply(subscriber));
}
}
@Override
public CompletionStage<R> getBody() {
return cf;
}
public static <S extends Subscriber<? super String>, R> LineSubscriberAdapter<S, R>
create(S subscriber, Function<? super S, ? extends R> finisher, Charset charset, String eol)
{
if (eol != null && eol.isEmpty())
throw new IllegalArgumentException("empty line separator");
return new LineSubscriberAdapter<>(Objects.requireNonNull(subscriber),
Objects.requireNonNull(finisher),
Objects.requireNonNull(charset),
eol);
}
static final class LineSubscription implements Flow.Subscription {
final Flow.Subscription upstreamSubscription;
final CharsetDecoder decoder;
final String newline;
final Demand downstreamDemand;
final ConcurrentLinkedDeque<ByteBuffer> queue;
final SequentialScheduler scheduler;
final Flow.Subscriber<? super String> upstream;
final CompletableFuture<?> cf;
private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
private final AtomicLong demanded = new AtomicLong();
private volatile boolean completed;
private volatile boolean cancelled;
private final char[] chars = new char[1024];
private final ByteBuffer leftover = ByteBuffer.wrap(new byte[64]);
private final CharBuffer buffer = CharBuffer.wrap(chars);
private final StringBuilder builder = new StringBuilder();
private String nextLine;
private LineSubscription(Flow.Subscription s,
CharsetDecoder dec,
String separator,
Flow.Subscriber<? super String> subscriber,
CompletableFuture<?> completion) {
downstreamDemand = new Demand();
queue = new ConcurrentLinkedDeque<>();
upstreamSubscription = Objects.requireNonNull(s);
decoder = Objects.requireNonNull(dec);
newline = separator;
upstream = Objects.requireNonNull(subscriber);
cf = Objects.requireNonNull(completion);
scheduler = SequentialScheduler.synchronizedScheduler(this::loop);
}
@Override
public void request(long n) {
if (cancelled) return;
if (downstreamDemand.increase(n)) {
scheduler.runOrSchedule();
}
}
@Override
public void cancel() {
cancelled = true;
upstreamSubscription.cancel();
}
public void submit(List<ByteBuffer> list) {
queue.addAll(list);
demanded.decrementAndGet();
scheduler.runOrSchedule();
}
public void signalComplete() {
completed = true;
scheduler.runOrSchedule();
}
public void signalError(Throwable error) {
if (errorRef.compareAndSet(null,
Objects.requireNonNull(error))) {
scheduler.runOrSchedule();
}
}
// This method looks at whether some bytes where left over (in leftover)
// from decoding the previous buffer when the previous buffer was in
// underflow. If so, it takes bytes one by one from the new buffer 'in'
// and combines them with the leftover bytes until 'in' is exhausted or a
// character was produced in 'out', resolving the previous underflow.
// Returns true if the buffer is still in underflow, false otherwise.
// However, in both situation some chars might have been produced in 'out'.
private boolean isUnderFlow(ByteBuffer in, CharBuffer out, boolean endOfInput)
throws CharacterCodingException {
int limit = leftover.position();
if (limit == 0) {
// no leftover
return false;
} else {
CoderResult res = null;
while (in.hasRemaining()) {
leftover.position(limit);
leftover.limit(++limit);
leftover.put(in.get());
leftover.position(0);
res = decoder.decode(leftover, out,
endOfInput && !in.hasRemaining());
int remaining = leftover.remaining();
if (remaining > 0) {
assert leftover.position() == 0;
leftover.position(remaining);
} else {
leftover.position(0);
}
leftover.limit(leftover.capacity());
if (res.isUnderflow() && remaining > 0 && in.hasRemaining()) {
continue;
}
if (res.isError()) {
res.throwException();
}
assert !res.isOverflow();
return false;
}
return !endOfInput;
}
}
// extract characters from start to end and remove them from
// the StringBuilder
private static String take(StringBuilder b, int start, int end) {
assert start == 0;
String line;
if (end == start) return "";
line = b.substring(start, end);
b.delete(start, end);
return line;
}
// finds end of line, returns -1 if not found, or the position after
// the line delimiter if found, removing the delimiter in the process.
private static int endOfLine(StringBuilder b, String eol, boolean endOfInput) {
int len = b.length();
if (eol != null) { // delimiter explicitly specified
int i = b.indexOf(eol);
if (i >= 0) {
// remove the delimiter and returns the position
// of the char after it.
b.delete(i, i + eol.length());
return i;
}
} else { // no delimiter specified, behaves as BufferedReader::readLine
boolean crfound = false;
for (int i = 0; i < len; i++) {
char c = b.charAt(i);
if (c == '\n') {
// '\n' or '\r\n' found.
// remove the delimiter and returns the position
// of the char after it.
b.delete(crfound ? i - 1 : i, i + 1);
return crfound ? i - 1 : i;
} else if (crfound) {
// previous char was '\r', c != '\n'
assert i != 0;
// remove the delimiter and returns the position
// of the char after it.
b.delete(i - 1, i);
return i - 1;
}
crfound = c == '\r';
}
if (crfound && endOfInput) {
// remove the delimiter and returns the position
// of the char after it.
b.delete(len - 1, len);
return len - 1;
}
}
return endOfInput && len > 0 ? len : -1;
}
// Looks at whether the StringBuilder contains a line.
// Returns null if more character are needed.
private static String nextLine(StringBuilder b, String eol, boolean endOfInput) {
int next = endOfLine(b, eol, endOfInput);
return (next > -1) ? take(b, 0, next) : null;
}
// Attempts to read the next line. Returns the next line if
// the delimiter was found, null otherwise. The delimiters are
// consumed.
private String nextLine()
throws CharacterCodingException {
assert nextLine == null;
LINES:
while (nextLine == null) {
boolean endOfInput = completed && queue.isEmpty();
nextLine = nextLine(builder, newline,
endOfInput && leftover.position() == 0);
if (nextLine != null) return nextLine;
ByteBuffer b;
BUFFERS:
while ((b = queue.peek()) != null) {
if (!b.hasRemaining()) {
queue.poll();
continue BUFFERS;
}
BYTES:
while (b.hasRemaining()) {
buffer.position(0);
buffer.limit(buffer.capacity());
boolean endofInput = completed && queue.size() <= 1;
if (isUnderFlow(b, buffer, endofInput)) {
assert !b.hasRemaining();
if (buffer.position() > 0) {
buffer.flip();
builder.append(buffer);
}
continue BUFFERS;
}
CoderResult res = decoder.decode(b, buffer, endofInput);
if (res.isError()) res.throwException();
if (buffer.position() > 0) {
buffer.flip();
builder.append(buffer);
continue LINES;
}
if (res.isUnderflow() && b.hasRemaining()) {
//System.out.println("underflow: adding " + b.remaining() + " bytes");
leftover.put(b);
assert !b.hasRemaining();
continue BUFFERS;
}
}
}
assert queue.isEmpty();
if (endOfInput) {
// Time to cleanup: there may be some undecoded leftover bytes
// We need to flush them out.
// The decoder has been configured to replace malformed/unmappable
// chars with some replacement, in order to behave like
// InputStreamReader.
leftover.flip();
buffer.position(0);
buffer.limit(buffer.capacity());
// decode() must be called just before flush, even if there
// is nothing to decode. We must do this even if leftover
// has no remaining bytes.
CoderResult res = decoder.decode(leftover, buffer, endOfInput);
if (buffer.position() > 0) {
buffer.flip();
builder.append(buffer);
}
if (res.isError()) res.throwException();
// Now call decoder.flush()
buffer.position(0);
buffer.limit(buffer.capacity());
res = decoder.flush(buffer);
if (buffer.position() > 0) {
buffer.flip();
builder.append(buffer);
}
if (res.isError()) res.throwException();
// It's possible that we reach here twice - just for the
// purpose of checking that no bytes were left over, so
// we reset leftover/decoder to make the function reentrant.
leftover.position(0);
leftover.limit(leftover.capacity());
decoder.reset();
// if some chars were produced then this call will
// return them.
return nextLine = nextLine(builder, newline, endOfInput);
}
return null;
}
return null;
}
// The main sequential scheduler loop.
private void loop() {
try {
while (!cancelled) {
Throwable error = errorRef.get();
if (error != null) {
cancelled = true;
scheduler.stop();
upstream.onError(error);
cf.completeExceptionally(error);
return;
}
if (nextLine == null) nextLine = nextLine();
if (nextLine == null) {
if (completed) {
scheduler.stop();
if (leftover.position() != 0) {
// Underflow: not all bytes could be
// decoded, but no more bytes will be coming.
// This should not happen as we should already
// have got a MalformedInputException, or
// replaced the unmappable chars.
errorRef.compareAndSet(null,
new IllegalStateException(
"premature end of input ("
+ leftover.position()
+ " undecoded bytes)"));
continue;
} else {
upstream.onComplete();
}
return;
} else if (demanded.get() == 0
&& !downstreamDemand.isFulfilled()) {
long incr = Math.max(1, downstreamDemand.get());
demanded.addAndGet(incr);
upstreamSubscription.request(incr);
continue;
} else return;
}
assert nextLine != null;
assert newline != null && !nextLine.endsWith(newline)
|| !nextLine.endsWith("\n") || !nextLine.endsWith("\r");
if (downstreamDemand.tryDecrement()) {
String forward = nextLine;
nextLine = null;
upstream.onNext(forward);
} else return; // no demand: come back later
}
} catch (Throwable t) {
try {
upstreamSubscription.cancel();
} finally {
signalError(t);
}
}
}
static LineSubscription create(Flow.Subscription s,
Charset charset,
String lineSeparator,
Flow.Subscriber<? super String> upstream,
CompletableFuture<?> cf) {
return new LineSubscription(Objects.requireNonNull(s),
Objects.requireNonNull(charset).newDecoder()
// use the same decoder configuration than
// java.io.InputStreamReader
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE),
lineSeparator,
Objects.requireNonNull(upstream),
Objects.requireNonNull(cf));
}
}
}

View file

@ -0,0 +1,373 @@
/*
* Copyright (c) 2015, 2018, 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.lang.System.Logger.Level;
import java.time.Duration;
import java.util.Iterator;
import java.util.LinkedList;
import java.security.AccessControlContext;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.PushPromiseHandler;
import java.net.http.HttpTimeoutException;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.ConnectionExpiredException;
import jdk.internal.net.http.common.Utils;
import static jdk.internal.net.http.common.MinimalFuture.completedFuture;
import static jdk.internal.net.http.common.MinimalFuture.failedFuture;
/**
* Encapsulates multiple Exchanges belonging to one HttpRequestImpl.
* - manages filters
* - retries due to filters.
* - I/O errors and most other exceptions get returned directly to user
*
* Creates a new Exchange for each request/response interaction
*/
class MultiExchange<T> {
static final Logger debug =
Utils.getDebugLogger("MultiExchange"::toString, Utils.DEBUG);
private final HttpRequest userRequest; // the user request
private final HttpRequestImpl request; // a copy of the user request
final AccessControlContext acc;
final HttpClientImpl client;
final HttpResponse.BodyHandler<T> responseHandler;
final Executor executor;
final AtomicInteger attempts = new AtomicInteger();
HttpRequestImpl currentreq; // used for retries & redirect
HttpRequestImpl previousreq; // used for retries & redirect
Exchange<T> exchange; // the current exchange
Exchange<T> previous;
volatile Throwable retryCause;
volatile boolean expiredOnce;
volatile HttpResponse<T> response = null;
// Maximum number of times a request will be retried/redirected
// for any reason
static final int DEFAULT_MAX_ATTEMPTS = 5;
static final int max_attempts = Utils.getIntegerNetProperty(
"jdk.httpclient.redirects.retrylimit", DEFAULT_MAX_ATTEMPTS
);
private final LinkedList<HeaderFilter> filters;
TimedEvent timedEvent;
volatile boolean cancelled;
final PushGroup<T> pushGroup;
/**
* Filter fields. These are attached as required by filters
* and only used by the filter implementations. This could be
* generalised into Objects that are passed explicitly to the filters
* (one per MultiExchange object, and one per Exchange object possibly)
*/
volatile AuthenticationFilter.AuthInfo serverauth, proxyauth;
// RedirectHandler
volatile int numberOfRedirects = 0;
/**
* MultiExchange with one final response.
*/
MultiExchange(HttpRequest userRequest,
HttpRequestImpl requestImpl,
HttpClientImpl client,
HttpResponse.BodyHandler<T> responseHandler,
PushPromiseHandler<T> pushPromiseHandler,
AccessControlContext acc) {
this.previous = null;
this.userRequest = userRequest;
this.request = requestImpl;
this.currentreq = request;
this.previousreq = null;
this.client = client;
this.filters = client.filterChain();
this.acc = acc;
this.executor = client.theExecutor();
this.responseHandler = responseHandler;
if (pushPromiseHandler != null) {
Executor executor = acc == null
? this.executor
: new PrivilegedExecutor(this.executor, acc);
this.pushGroup = new PushGroup<>(pushPromiseHandler, request, executor);
} else {
pushGroup = null;
}
this.exchange = new Exchange<>(request, this);
}
private synchronized Exchange<T> getExchange() {
return exchange;
}
HttpClientImpl client() {
return client;
}
HttpClient.Version version() {
HttpClient.Version vers = request.version().orElse(client.version());
if (vers == HttpClient.Version.HTTP_2 && !request.secure() && request.proxy() != null)
vers = HttpClient.Version.HTTP_1_1;
return vers;
}
private synchronized void setExchange(Exchange<T> exchange) {
if (this.exchange != null && exchange != this.exchange) {
this.exchange.released();
}
this.exchange = exchange;
}
private void cancelTimer() {
if (timedEvent != null) {
client.cancelTimer(timedEvent);
}
}
private void requestFilters(HttpRequestImpl r) throws IOException {
Log.logTrace("Applying request filters");
for (HeaderFilter filter : filters) {
Log.logTrace("Applying {0}", filter);
filter.request(r, this);
}
Log.logTrace("All filters applied");
}
private HttpRequestImpl responseFilters(Response response) throws IOException
{
Log.logTrace("Applying response filters");
Iterator<HeaderFilter> reverseItr = filters.descendingIterator();
while (reverseItr.hasNext()) {
HeaderFilter filter = reverseItr.next();
Log.logTrace("Applying {0}", filter);
HttpRequestImpl newreq = filter.response(response);
if (newreq != null) {
Log.logTrace("New request: stopping filters");
return newreq;
}
}
Log.logTrace("All filters applied");
return null;
}
public void cancel(IOException cause) {
cancelled = true;
getExchange().cancel(cause);
}
public CompletableFuture<HttpResponse<T>> responseAsync() {
CompletableFuture<Void> start = new MinimalFuture<>();
CompletableFuture<HttpResponse<T>> cf = responseAsync0(start);
start.completeAsync( () -> null, executor); // trigger execution
return cf;
}
private CompletableFuture<HttpResponse<T>>
responseAsync0(CompletableFuture<Void> start) {
return start.thenCompose( v -> responseAsyncImpl())
.thenCompose((Response r) -> {
Exchange<T> exch = getExchange();
return exch.readBodyAsync(responseHandler)
.thenApply((T body) -> {
this.response =
new HttpResponseImpl<>(r.request(), r, this.response, body, exch);
return this.response;
});
});
}
private CompletableFuture<Response> responseAsyncImpl() {
CompletableFuture<Response> cf;
if (attempts.incrementAndGet() > max_attempts) {
cf = failedFuture(new IOException("Too many retries", retryCause));
} else {
if (currentreq.timeout().isPresent()) {
timedEvent = new TimedEvent(currentreq.timeout().get());
client.registerTimer(timedEvent);
}
try {
// 1. apply request filters
// if currentreq == previousreq the filters have already
// been applied once. Applying them a second time might
// cause some headers values to be added twice: for
// instance, the same cookie might be added again.
if (currentreq != previousreq) {
requestFilters(currentreq);
}
} catch (IOException e) {
return failedFuture(e);
}
Exchange<T> exch = getExchange();
// 2. get response
cf = exch.responseAsync()
.thenCompose((Response response) -> {
HttpRequestImpl newrequest;
try {
// 3. apply response filters
newrequest = responseFilters(response);
} catch (IOException e) {
return failedFuture(e);
}
// 4. check filter result and repeat or continue
if (newrequest == null) {
if (attempts.get() > 1) {
Log.logError("Succeeded on attempt: " + attempts);
}
return completedFuture(response);
} else {
this.response =
new HttpResponseImpl<>(currentreq, response, this.response, null, exch);
Exchange<T> oldExch = exch;
return exch.ignoreBody().handle((r,t) -> {
previousreq = currentreq;
currentreq = newrequest;
expiredOnce = false;
setExchange(new Exchange<>(currentreq, this, acc));
return responseAsyncImpl();
}).thenCompose(Function.identity());
} })
.handle((response, ex) -> {
// 5. handle errors and cancel any timer set
cancelTimer();
if (ex == null) {
assert response != null;
return completedFuture(response);
}
// all exceptions thrown are handled here
CompletableFuture<Response> errorCF = getExceptionalCF(ex);
if (errorCF == null) {
return responseAsyncImpl();
} else {
return errorCF;
} })
.thenCompose(Function.identity());
}
return cf;
}
private static boolean retryPostValue() {
String s = Utils.getNetProperty("jdk.httpclient.enableAllMethodRetry");
if (s == "" || "true".equals(s))
return true;
return false;
}
/** True if ALL ( even non-idempotent ) requests can be automatic retried. */
private static final boolean RETRY_ALWAYS = retryPostValue();
/** Returns true is given request has an idempotent method. */
private static boolean isIdempotentRequest(HttpRequest request) {
String method = request.method();
switch (method) {
case "GET" :
case "HEAD" :
return true;
default :
return false;
}
}
/** Returns true if the given request can be automatically retried. */
private static boolean canRetryRequest(HttpRequest request) {
if (isIdempotentRequest(request))
return true;
if (RETRY_ALWAYS)
return true;
return false;
}
/**
* Takes a Throwable and returns a suitable CompletableFuture that is
* completed exceptionally, or null.
*/
private CompletableFuture<Response> getExceptionalCF(Throwable t) {
if ((t instanceof CompletionException) || (t instanceof ExecutionException)) {
if (t.getCause() != null) {
t = t.getCause();
}
}
if (cancelled && t instanceof IOException) {
t = new HttpTimeoutException("request timed out");
} else if (t instanceof ConnectionExpiredException) {
Throwable cause = t;
if (t.getCause() != null) {
cause = t.getCause(); // unwrap the ConnectionExpiredException
}
if (!canRetryRequest(currentreq)) {
return failedFuture(cause); // fails with original cause
}
// allow the retry mechanism to do its work
retryCause = cause;
if (!expiredOnce) {
if (debug.on())
debug.log("ConnectionExpiredException (async): retrying...", t);
expiredOnce = true;
// The connection was abruptly closed.
// We return null to retry the same request a second time.
// The request filters have already been applied to the
// currentreq, so we set previousreq = currentreq to
// prevent them from being applied again.
previousreq = currentreq;
return null;
} else {
if (debug.on())
debug.log("ConnectionExpiredException (async): already retried once.", t);
if (t.getCause() != null) t = t.getCause();
}
}
return failedFuture(t);
}
class TimedEvent extends TimeoutEvent {
TimedEvent(Duration duration) {
super(duration);
}
@Override
public void handle() {
if (debug.on())
debug.log("Cancelling MultiExchange due to timeout for request %s",
request);
cancel(new HttpTimeoutException("request timed out"));
}
}
}

View file

@ -0,0 +1,228 @@
/*
* Copyright (c) 2015, 2018, 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.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.concurrent.CompletableFuture;
import jdk.internal.net.http.common.FlowTube;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.Utils;
/**
* Plain raw TCP connection direct to destination.
* The connection operates in asynchronous non-blocking mode.
* All reads and writes are done non-blocking.
*/
class PlainHttpConnection extends HttpConnection {
private final Object reading = new Object();
protected final SocketChannel chan;
private final SocketTube tube; // need SocketTube to call signalClosed().
private final PlainHttpPublisher writePublisher = new PlainHttpPublisher(reading);
private volatile boolean connected;
private boolean closed;
// should be volatile to provide proper synchronization(visibility) action
final class ConnectEvent extends AsyncEvent {
private final CompletableFuture<Void> cf;
ConnectEvent(CompletableFuture<Void> cf) {
this.cf = cf;
}
@Override
public SelectableChannel channel() {
return chan;
}
@Override
public int interestOps() {
return SelectionKey.OP_CONNECT;
}
@Override
public void handle() {
try {
assert !connected : "Already connected";
assert !chan.isBlocking() : "Unexpected blocking channel";
if (debug.on())
debug.log("ConnectEvent: finishing connect");
boolean finished = chan.finishConnect();
assert finished : "Expected channel to be connected";
if (debug.on())
debug.log("ConnectEvent: connect finished: %s Local addr: %s",
finished, chan.getLocalAddress());
connected = true;
// complete async since the event runs on the SelectorManager thread
cf.completeAsync(() -> null, client().theExecutor());
} catch (Throwable e) {
client().theExecutor().execute( () -> cf.completeExceptionally(e));
}
}
@Override
public void abort(IOException ioe) {
close();
client().theExecutor().execute( () -> cf.completeExceptionally(ioe));
}
}
@Override
public CompletableFuture<Void> connectAsync() {
CompletableFuture<Void> cf = new MinimalFuture<>();
try {
assert !connected : "Already connected";
assert !chan.isBlocking() : "Unexpected blocking channel";
boolean finished = false;
PrivilegedExceptionAction<Boolean> pa =
() -> chan.connect(Utils.resolveAddress(address));
try {
finished = AccessController.doPrivileged(pa);
} catch (PrivilegedActionException e) {
cf.completeExceptionally(e.getCause());
}
if (finished) {
if (debug.on()) debug.log("connect finished without blocking");
connected = true;
cf.complete(null);
} else {
if (debug.on()) debug.log("registering connect event");
client().registerEvent(new ConnectEvent(cf));
}
} catch (Throwable throwable) {
cf.completeExceptionally(throwable);
}
return cf;
}
@Override
SocketChannel channel() {
return chan;
}
@Override
final FlowTube getConnectionFlow() {
return tube;
}
PlainHttpConnection(InetSocketAddress addr, HttpClientImpl client) {
super(addr, client);
try {
this.chan = SocketChannel.open();
chan.configureBlocking(false);
int bufsize = client.getReceiveBufferSize();
if (!trySetReceiveBufferSize(bufsize)) {
trySetReceiveBufferSize(256*1024);
}
chan.setOption(StandardSocketOptions.TCP_NODELAY, true);
// wrap the connected channel in a Tube for async reading and writing
tube = new SocketTube(client(), chan, Utils::getBuffer);
} catch (IOException e) {
throw new InternalError(e);
}
}
private boolean trySetReceiveBufferSize(int bufsize) {
try {
chan.setOption(StandardSocketOptions.SO_RCVBUF, bufsize);
if (debug.on())
debug.log("Receive buffer size is %s",
chan.getOption(StandardSocketOptions.SO_RCVBUF));
return true;
} catch(IOException x) {
if (debug.on())
debug.log("Failed to set receive buffer size to %d on %s",
bufsize, chan);
}
return false;
}
@Override
HttpPublisher publisher() { return writePublisher; }
@Override
public String toString() {
return "PlainHttpConnection: " + super.toString();
}
/**
* Closes this connection
*/
@Override
public void close() {
synchronized (this) {
if (closed) {
return;
}
closed = true;
}
try {
Log.logTrace("Closing: " + toString());
if (debug.on())
debug.log("Closing channel: " + client().debugInterestOps(chan));
chan.close();
tube.signalClosed();
} catch (IOException e) {
Log.logTrace("Closing resulted in " + e);
}
}
@Override
ConnectionPool.CacheKey cacheKey() {
return new ConnectionPool.CacheKey(address, null);
}
@Override
synchronized boolean connected() {
return connected;
}
@Override
boolean isSecure() {
return false;
}
@Override
boolean isProxied() {
return false;
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2015, 2018, 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.net.InetSocketAddress;
class PlainProxyConnection extends PlainHttpConnection {
PlainProxyConnection(InetSocketAddress proxy, HttpClientImpl client) {
super(proxy, client);
}
@Override
ConnectionPool.CacheKey cacheKey() {
return new ConnectionPool.CacheKey(null, address);
}
@Override
public boolean isProxied() { return true; }
}

View file

@ -0,0 +1,149 @@
/*
* Copyright (c) 2015, 2018, 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.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.net.http.HttpHeaders;
import jdk.internal.net.http.common.FlowTube;
import jdk.internal.net.http.common.MinimalFuture;
import static java.net.http.HttpResponse.BodyHandlers.discarding;
/**
* A plain text socket tunnel through a proxy. Uses "CONNECT" but does not
* encrypt. Used by WebSocket, as well as HTTP over SSL + Proxy.
* Wrapped in SSLTunnelConnection or AsyncSSLTunnelConnection for encryption.
*/
final class PlainTunnelingConnection extends HttpConnection {
final PlainHttpConnection delegate;
final HttpHeaders proxyHeaders;
final InetSocketAddress proxyAddr;
private volatile boolean connected;
protected PlainTunnelingConnection(InetSocketAddress addr,
InetSocketAddress proxy,
HttpClientImpl client,
HttpHeaders proxyHeaders) {
super(addr, client);
this.proxyAddr = proxy;
this.proxyHeaders = proxyHeaders;
delegate = new PlainHttpConnection(proxy, client);
}
@Override
public CompletableFuture<Void> connectAsync() {
if (debug.on()) debug.log("Connecting plain connection");
return delegate.connectAsync()
.thenCompose((Void v) -> {
if (debug.on()) debug.log("sending HTTP/1.1 CONNECT");
HttpClientImpl client = client();
assert client != null;
HttpRequestImpl req = new HttpRequestImpl("CONNECT", address, proxyHeaders);
MultiExchange<Void> mulEx = new MultiExchange<>(null, req,
client, discarding(), null, null);
Exchange<Void> connectExchange = new Exchange<>(req, mulEx);
return connectExchange
.responseAsyncImpl(delegate)
.thenCompose((Response resp) -> {
CompletableFuture<Void> cf = new MinimalFuture<>();
if (debug.on()) debug.log("got response: %d", resp.statusCode());
if (resp.statusCode() == 407) {
return connectExchange.ignoreBody().handle((r,t) -> {
// close delegate after reading body: we won't
// be reusing that connection anyway.
delegate.close();
ProxyAuthenticationRequired authenticationRequired =
new ProxyAuthenticationRequired(resp);
cf.completeExceptionally(authenticationRequired);
return cf;
}).thenCompose(Function.identity());
} else if (resp.statusCode() != 200) {
delegate.close();
cf.completeExceptionally(new IOException(
"Tunnel failed, got: "+ resp.statusCode()));
} else {
// get the initial/remaining bytes
ByteBuffer b = ((Http1Exchange<?>)connectExchange.exchImpl).drainLeftOverBytes();
int remaining = b.remaining();
assert remaining == 0: "Unexpected remaining: " + remaining;
connected = true;
cf.complete(null);
}
return cf;
});
});
}
@Override
boolean isTunnel() { return true; }
@Override
HttpPublisher publisher() { return delegate.publisher(); }
@Override
boolean connected() {
return connected;
}
@Override
SocketChannel channel() {
return delegate.channel();
}
@Override
FlowTube getConnectionFlow() {
return delegate.getConnectionFlow();
}
@Override
ConnectionPool.CacheKey cacheKey() {
return new ConnectionPool.CacheKey(null, proxyAddr);
}
@Override
public void close() {
delegate.close();
connected = false;
}
@Override
boolean isSecure() {
return false;
}
@Override
boolean isProxied() {
return true;
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2015, 2018, 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.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Objects;
import java.util.concurrent.Executor;
/**
* Executes tasks within a given access control context, and by a given executor.
*/
class PrivilegedExecutor implements Executor {
/** The underlying executor. May be provided by the user. */
final Executor executor;
/** The ACC to execute the tasks within. */
final AccessControlContext acc;
public PrivilegedExecutor(Executor executor, AccessControlContext acc) {
Objects.requireNonNull(executor);
Objects.requireNonNull(acc);
this.executor = executor;
this.acc = acc;
}
private static class PrivilegedRunnable implements Runnable {
private final Runnable r;
private final AccessControlContext acc;
PrivilegedRunnable(Runnable r, AccessControlContext acc) {
this.r = r;
this.acc = acc;
}
@Override
public void run() {
PrivilegedAction<Void> pa = () -> { r.run(); return null; };
AccessController.doPrivileged(pa, acc);
}
}
@Override
public void execute(Runnable r) {
executor.execute(new PrivilegedRunnable(r, acc));
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2018, 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;
/**
* Signals that a proxy has refused a CONNECT request with a
* 407 error code.
*/
final class ProxyAuthenticationRequired extends IOException {
private static final long serialVersionUID = 0;
final transient Response proxyResponse;
/**
* Constructs a {@code ProxyAuthenticationRequired} with the specified detail
* message and cause.
*
* @param proxyResponse the response from the proxy
*/
public ProxyAuthenticationRequired(Response proxyResponse) {
super("Proxy Authentication Required");
assert proxyResponse.statusCode() == 407;
this.proxyResponse = proxyResponse;
}
}

View file

@ -0,0 +1,138 @@
/*
* Copyright (c) 2016, 2018, 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.util.Iterator;
import java.util.concurrent.Flow;
import jdk.internal.net.http.common.Demand;
import jdk.internal.net.http.common.SequentialScheduler;
/**
* A Publisher that publishes items obtained from the given Iterable. Each new
* subscription gets a new Iterator.
*/
class PullPublisher<T> implements Flow.Publisher<T> {
// Only one of `iterable` and `throwable` can be non-null. throwable is
// non-null when an error has been encountered, by the creator of
// PullPublisher, while subscribing the subscriber, but before subscribe has
// completed.
private final Iterable<T> iterable;
private final Throwable throwable;
PullPublisher(Iterable<T> iterable, Throwable throwable) {
this.iterable = iterable;
this.throwable = throwable;
}
PullPublisher(Iterable<T> iterable) {
this(iterable, null);
}
@Override
public void subscribe(Flow.Subscriber<? super T> subscriber) {
Subscription sub;
if (throwable != null) {
assert iterable == null : "non-null iterable: " + iterable;
sub = new Subscription(subscriber, null, throwable);
} else {
assert throwable == null : "non-null exception: " + throwable;
sub = new Subscription(subscriber, iterable.iterator(), null);
}
subscriber.onSubscribe(sub);
if (throwable != null) {
sub.pullScheduler.runOrSchedule();
}
}
private class Subscription implements Flow.Subscription {
private final Flow.Subscriber<? super T> subscriber;
private final Iterator<T> iter;
private volatile boolean completed;
private volatile boolean cancelled;
private volatile Throwable error;
final SequentialScheduler pullScheduler = new SequentialScheduler(new PullTask());
private final Demand demand = new Demand();
Subscription(Flow.Subscriber<? super T> subscriber,
Iterator<T> iter,
Throwable throwable) {
this.subscriber = subscriber;
this.iter = iter;
this.error = throwable;
}
final class PullTask extends SequentialScheduler.CompleteRestartableTask {
@Override
protected void run() {
if (completed || cancelled) {
return;
}
Throwable t = error;
if (t != null) {
completed = true;
pullScheduler.stop();
subscriber.onError(t);
return;
}
while (demand.tryDecrement() && !cancelled) {
if (!iter.hasNext()) {
break;
} else {
subscriber.onNext(iter.next());
}
}
if (!iter.hasNext() && !cancelled) {
completed = true;
pullScheduler.stop();
subscriber.onComplete();
}
}
}
@Override
public void request(long n) {
if (cancelled)
return; // no-op
if (n <= 0) {
error = new IllegalArgumentException("illegal non-positive request:" + n);
} else {
demand.increase(n);
}
pullScheduler.runOrSchedule();
}
@Override
public void cancel() {
cancelled = true;
}
}
}

View file

@ -0,0 +1,171 @@
/*
* Copyright (c) 2016, 2018, 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.security.AccessControlContext;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.PushPromiseHandler;
import java.util.concurrent.Executor;
import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.Log;
/**
* One PushGroup object is associated with the parent Stream of the pushed
* Streams. This keeps track of all common state associated with the pushes.
*/
class PushGroup<T> {
private final HttpRequest initiatingRequest;
final CompletableFuture<Void> noMorePushesCF;
volatile Throwable error; // any exception that occurred during pushes
// user's subscriber object
final PushPromiseHandler<T> pushPromiseHandler;
private final Executor executor;
int numberOfPushes;
int remainingPushes;
boolean noMorePushes = false;
PushGroup(PushPromiseHandler<T> pushPromiseHandler,
HttpRequestImpl initiatingRequest,
Executor executor) {
this(pushPromiseHandler, initiatingRequest, new MinimalFuture<>(), executor);
}
// Check mainBodyHandler before calling nested constructor.
private PushGroup(HttpResponse.PushPromiseHandler<T> pushPromiseHandler,
HttpRequestImpl initiatingRequest,
CompletableFuture<HttpResponse<T>> mainResponse,
Executor executor) {
this.noMorePushesCF = new MinimalFuture<>();
this.pushPromiseHandler = pushPromiseHandler;
this.initiatingRequest = initiatingRequest;
this.executor = executor;
}
interface Acceptor<T> {
BodyHandler<T> bodyHandler();
CompletableFuture<HttpResponse<T>> cf();
boolean accepted();
}
private static class AcceptorImpl<T> implements Acceptor<T> {
private final Executor executor;
private volatile HttpResponse.BodyHandler<T> bodyHandler;
private volatile CompletableFuture<HttpResponse<T>> cf;
AcceptorImpl(Executor executor) {
this.executor = executor;
}
CompletableFuture<HttpResponse<T>> accept(BodyHandler<T> bodyHandler) {
Objects.requireNonNull(bodyHandler);
if (this.bodyHandler != null)
throw new IllegalStateException("non-null bodyHandler");
this.bodyHandler = bodyHandler;
cf = new MinimalFuture<>();
return cf.whenCompleteAsync((r,t) -> {}, executor);
}
@Override public BodyHandler<T> bodyHandler() { return bodyHandler; }
@Override public CompletableFuture<HttpResponse<T>> cf() { return cf; }
@Override public boolean accepted() { return cf != null; }
}
Acceptor<T> acceptPushRequest(HttpRequest pushRequest) {
AcceptorImpl<T> acceptor = new AcceptorImpl<>(executor);
try {
pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, acceptor::accept);
} catch (Throwable t) {
if (acceptor.accepted()) {
CompletableFuture<?> cf = acceptor.cf();
cf.completeExceptionally(t);
}
throw t;
}
synchronized (this) {
if (acceptor.accepted()) {
numberOfPushes++;
remainingPushes++;
}
return acceptor;
}
}
// This is called when the main body response completes because it means
// no more PUSH_PROMISEs are possible
synchronized void noMorePushes(boolean noMore) {
noMorePushes = noMore;
checkIfCompleted();
noMorePushesCF.complete(null);
}
synchronized CompletableFuture<Void> pushesCF() {
return noMorePushesCF;
}
synchronized boolean noMorePushes() {
return noMorePushes;
}
synchronized void pushCompleted() {
remainingPushes--;
checkIfCompleted();
}
synchronized void checkIfCompleted() {
if (Log.trace()) {
Log.logTrace("PushGroup remainingPushes={0} error={1} noMorePushes={2}",
remainingPushes,
(error==null)?error:error.getClass().getSimpleName(),
noMorePushes);
}
if (remainingPushes == 0 && error == null && noMorePushes) {
if (Log.trace()) {
Log.logTrace("push completed");
}
}
}
synchronized void pushError(Throwable t) {
if (t == null) {
return;
}
this.error = t;
}
}

View file

@ -0,0 +1,436 @@
/*
* Copyright (c) 2018, 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 jdk.internal.net.http.common.Demand;
import jdk.internal.net.http.common.FlowTube;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.Utils;
import jdk.internal.net.http.websocket.RawChannel;
import java.io.EOFException;
import java.io.IOException;
import java.lang.ref.Cleaner;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.lang.System.Logger.Level;
/*
* I/O abstraction used to implement WebSocket.
*
*/
public class RawChannelTube implements RawChannel {
final HttpConnection connection;
final FlowTube tube;
final WritePublisher writePublisher;
final ReadSubscriber readSubscriber;
final Supplier<ByteBuffer> initial;
final AtomicBoolean inited = new AtomicBoolean();
final AtomicBoolean outputClosed = new AtomicBoolean();
final AtomicBoolean inputClosed = new AtomicBoolean();
final AtomicBoolean closed = new AtomicBoolean();
final String dbgTag;
final Logger debug;
private static final Cleaner cleaner =
Utils.ASSERTIONSENABLED && Utils.DEBUG_WS ? Cleaner.create() : null;
RawChannelTube(HttpConnection connection,
Supplier<ByteBuffer> initial) {
this.connection = connection;
this.tube = connection.getConnectionFlow();
this.initial = initial;
this.writePublisher = new WritePublisher();
this.readSubscriber = new ReadSubscriber();
dbgTag = "[WebSocket] RawChannelTube(" + tube.toString() +")";
debug = Utils.getWebSocketLogger(dbgTag::toString, Utils.DEBUG_WS);
connection.client().webSocketOpen();
connectFlows();
if (Utils.ASSERTIONSENABLED && Utils.DEBUG_WS) {
// this is just for debug...
cleaner.register(this, new CleanupChecker(closed, debug));
}
}
// Make sure no back reference to RawChannelTube can exist
// from this class. In particular it would be dangerous
// to reference connection, since connection has a reference
// to SocketTube with which a RawChannelTube is registered.
// Ditto for HttpClientImpl, which might have a back reference
// to the connection.
static final class CleanupChecker implements Runnable {
final AtomicBoolean closed;
final System.Logger debug;
CleanupChecker(AtomicBoolean closed, System.Logger debug) {
this.closed = closed;
this.debug = debug;
}
@Override
public void run() {
if (!closed.get()) {
debug.log(Level.DEBUG,
"RawChannelTube was not closed before being released");
}
}
}
private void connectFlows() {
if (debug.on()) debug.log("connectFlows");
tube.connectFlows(writePublisher, readSubscriber);
}
class WriteSubscription implements Flow.Subscription {
final Flow.Subscriber<? super List<ByteBuffer>> subscriber;
final Demand demand = new Demand();
volatile boolean cancelled;
WriteSubscription(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
this.subscriber = subscriber;
}
@Override
public void request(long n) {
if (debug.on()) debug.log("WriteSubscription::request %d", n);
demand.increase(n);
RawEvent event;
while ((event = writePublisher.events.poll()) != null) {
if (debug.on()) debug.log("WriteSubscriber: handling event");
event.handle();
if (demand.isFulfilled()) break;
}
}
@Override
public void cancel() {
cancelled = true;
if (debug.on()) debug.log("WriteSubscription::cancel");
shutdownOutput();
RawEvent event;
while ((event = writePublisher.events.poll()) != null) {
if (debug.on()) debug.log("WriteSubscriber: handling event");
event.handle();
}
}
}
class WritePublisher implements FlowTube.TubePublisher {
final ConcurrentLinkedQueue<RawEvent> events = new ConcurrentLinkedQueue<>();
volatile WriteSubscription writeSubscription;
@Override
public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
if (debug.on()) debug.log("WritePublisher::subscribe");
WriteSubscription subscription = new WriteSubscription(subscriber);
subscriber.onSubscribe(subscription);
writeSubscription = subscription;
}
}
class ReadSubscriber implements FlowTube.TubeSubscriber {
volatile Flow.Subscription readSubscription;
volatile boolean completed;
long initialRequest;
final ConcurrentLinkedQueue<RawEvent> events = new ConcurrentLinkedQueue<>();
final ConcurrentLinkedQueue<ByteBuffer> buffers = new ConcurrentLinkedQueue<>();
final AtomicReference<Throwable> errorRef = new AtomicReference<>();
void checkEvents() {
Flow.Subscription subscription = readSubscription;
if (subscription != null) {
Throwable error = errorRef.get();
while (!buffers.isEmpty() || error != null || closed.get() || completed) {
RawEvent event = events.poll();
if (event == null) break;
if (debug.on()) debug.log("ReadSubscriber: handling event");
event.handle();
}
}
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
//buffers.add(initial.get());
long n;
synchronized (this) {
readSubscription = subscription;
n = initialRequest;
initialRequest = 0;
}
if (debug.on()) debug.log("ReadSubscriber::onSubscribe");
if (n > 0) {
Throwable error = errorRef.get();
if (error == null && !closed.get() && !completed) {
if (debug.on()) debug.log("readSubscription: requesting " + n);
subscription.request(n);
}
}
checkEvents();
}
@Override
public void onNext(List<ByteBuffer> item) {
if (debug.on()) debug.log(() -> "ReadSubscriber::onNext "
+ Utils.remaining(item) + " bytes");
buffers.addAll(item);
checkEvents();
}
@Override
public void onError(Throwable throwable) {
if (closed.get() || errorRef.compareAndSet(null, throwable)) {
if (debug.on()) debug.log("ReadSubscriber::onError", throwable);
if (buffers.isEmpty()) {
checkEvents();
shutdownInput();
}
}
}
@Override
public void onComplete() {
if (debug.on()) debug.log("ReadSubscriber::onComplete");
completed = true;
if (buffers.isEmpty()) {
checkEvents();
shutdownInput();
}
}
}
/*
* Registers given event whose callback will be called once only (i.e.
* register new event for each callback).
*
* Memory consistency effects: actions in a thread calling registerEvent
* happen-before any subsequent actions in the thread calling event.handle
*/
public void registerEvent(RawEvent event) throws IOException {
int interestOps = event.interestOps();
if ((interestOps & SelectionKey.OP_WRITE) != 0) {
if (debug.on()) debug.log("register write event");
if (outputClosed.get()) throw new IOException("closed output");
writePublisher.events.add(event);
WriteSubscription writeSubscription = writePublisher.writeSubscription;
if (writeSubscription != null) {
while (!writeSubscription.demand.isFulfilled()) {
event = writePublisher.events.poll();
if (event == null) break;
event.handle();
}
}
}
if ((interestOps & SelectionKey.OP_READ) != 0) {
if (debug.on()) debug.log("register read event");
if (inputClosed.get()) throw new IOException("closed input");
readSubscriber.events.add(event);
readSubscriber.checkEvents();
if (readSubscriber.buffers.isEmpty()
&& !readSubscriber.events.isEmpty()) {
Flow.Subscription readSubscription =
readSubscriber.readSubscription;
if (readSubscription == null) {
synchronized (readSubscriber) {
readSubscription = readSubscriber.readSubscription;
if (readSubscription == null) {
readSubscriber.initialRequest = 1;
return;
}
}
}
assert readSubscription != null;
if (debug.on()) debug.log("readSubscription: requesting 1");
readSubscription.request(1);
}
}
}
/**
* Hands over the initial bytes. Once the bytes have been returned they are
* no longer available and the method will throw an {@link
* IllegalStateException} on each subsequent invocation.
*
* @return the initial bytes
* @throws IllegalStateException
* if the method has been already invoked
*/
public ByteBuffer initialByteBuffer() throws IllegalStateException {
if (inited.compareAndSet(false, true)) {
return initial.get();
} else throw new IllegalStateException("initial buffer already drained");
}
/*
* Returns a ByteBuffer with the data read or null if EOF is reached. Has no
* remaining bytes if no data available at the moment.
*/
public ByteBuffer read() throws IOException {
if (debug.on()) debug.log("read");
Flow.Subscription readSubscription = readSubscriber.readSubscription;
if (readSubscription == null) return Utils.EMPTY_BYTEBUFFER;
ByteBuffer buffer = readSubscriber.buffers.poll();
if (buffer != null) {
if (debug.on()) debug.log("read: " + buffer.remaining());
return buffer;
}
Throwable error = readSubscriber.errorRef.get();
if (error != null) error = Utils.getIOException(error);
if (error instanceof EOFException) {
if (debug.on()) debug.log("read: EOFException");
shutdownInput();
return null;
}
if (error != null) {
if (debug.on()) debug.log("read: " + error);
if (closed.get()) {
return null;
}
shutdownInput();
throw Utils.getIOException(error);
}
if (readSubscriber.completed) {
if (debug.on()) debug.log("read: EOF");
shutdownInput();
return null;
}
if (inputClosed.get()) {
if (debug.on()) debug.log("read: CLOSED");
throw new IOException("closed output");
}
if (debug.on()) debug.log("read: nothing to read");
return Utils.EMPTY_BYTEBUFFER;
}
/*
* Writes a sequence of bytes to this channel from a subsequence of the
* given buffers.
*/
public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
if (outputClosed.get()) {
if (debug.on()) debug.log("write: CLOSED");
throw new IOException("closed output");
}
WriteSubscription writeSubscription = writePublisher.writeSubscription;
if (writeSubscription == null) {
if (debug.on()) debug.log("write: unsubscribed: 0");
return 0;
}
if (writeSubscription.cancelled) {
if (debug.on()) debug.log("write: CANCELLED");
shutdownOutput();
throw new IOException("closed output");
}
if (writeSubscription.demand.tryDecrement()) {
List<ByteBuffer> buffers = copy(srcs, offset, length);
long res = Utils.remaining(buffers);
if (debug.on()) debug.log("write: writing %d", res);
writeSubscription.subscriber.onNext(buffers);
return res;
} else {
if (debug.on()) debug.log("write: no demand: 0");
return 0;
}
}
/**
* Shutdown the connection for reading without closing the channel.
*
* <p> Once shutdown for reading then further reads on the channel will
* return {@code null}, the end-of-stream indication. If the input side of
* the connection is already shutdown then invoking this method has no
* effect.
*
* @throws ClosedChannelException
* If this channel is closed
* @throws IOException
* If some other I/O error occurs
*/
public void shutdownInput() {
if (inputClosed.compareAndSet(false, true)) {
if (debug.on()) debug.log("shutdownInput");
// TransportImpl will eventually call RawChannel::close.
// We must not call it here as this would close the socket
// and can cause an exception to back fire before
// TransportImpl and WebSocketImpl have updated their state.
}
}
/**
* Shutdown the connection for writing without closing the channel.
*
* <p> Once shutdown for writing then further attempts to write to the
* channel will throw {@link ClosedChannelException}. If the output side of
* the connection is already shutdown then invoking this method has no
* effect.
*
* @throws ClosedChannelException
* If this channel is closed
* @throws IOException
* If some other I/O error occurs
*/
public void shutdownOutput() {
if (outputClosed.compareAndSet(false, true)) {
if (debug.on()) debug.log("shutdownOutput");
// TransportImpl will eventually call RawChannel::close.
// We must not call it here as this would close the socket
// and can cause an exception to back fire before
// TransportImpl and WebSocketImpl have updated their state.
}
}
/**
* Closes this channel.
*
* @throws IOException
* If an I/O error occurs
*/
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
if (debug.on()) debug.log("close");
connection.client().webSocketClose();
connection.close();
}
}
private static List<ByteBuffer> copy(ByteBuffer[] src, int offset, int len) {
int count = Math.min(len, src.length - offset);
if (count <= 0) return Utils.EMPTY_BB_LIST;
if (count == 1) return List.of(Utils.copy(src[offset]));
if (count == 2) return List.of(Utils.copy(src[offset]), Utils.copy(src[offset+1]));
List<ByteBuffer> list = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
list.add(Utils.copy(src[offset + i]));
}
return list;
}
}

View file

@ -0,0 +1,137 @@
/*
* Copyright (c) 2015, 2018, 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.io.UncheckedIOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Utils;
class RedirectFilter implements HeaderFilter {
HttpRequestImpl request;
HttpClientImpl client;
HttpClient.Redirect policy;
String method;
MultiExchange<?> exchange;
static final int DEFAULT_MAX_REDIRECTS = 5;
URI uri;
static final int max_redirects = Utils.getIntegerNetProperty(
"jdk.httpclient.redirects.retrylimit", DEFAULT_MAX_REDIRECTS
);
// A public no-arg constructor is required by FilterFactory
public RedirectFilter() {}
@Override
public synchronized void request(HttpRequestImpl r, MultiExchange<?> e) throws IOException {
this.request = r;
this.client = e.client();
this.policy = client.followRedirects();
this.method = r.method();
this.uri = r.uri();
this.exchange = e;
}
@Override
public synchronized HttpRequestImpl response(Response r) throws IOException {
return handleResponse(r);
}
private static String redirectedMethod(int statusCode, String orig) {
switch (statusCode) {
case 301:
case 302:
return orig.equals("POST") ? "GET" : orig;
case 303:
return "GET";
case 307:
case 308:
return orig;
default:
// unexpected but return orig
return orig;
}
}
/**
* Checks to see if a new request is needed and returns it.
* Null means response is ok to return to user.
*/
private HttpRequestImpl handleResponse(Response r) {
int rcode = r.statusCode();
if (rcode == 200 || policy == HttpClient.Redirect.NEVER) {
return null;
}
if (rcode >= 300 && rcode <= 399) {
URI redir = getRedirectedURI(r.headers());
String newMethod = redirectedMethod(rcode, method);
Log.logTrace("response code: {0}, redirected URI: {1}", rcode, redir);
if (canRedirect(redir) && ++exchange.numberOfRedirects < max_redirects) {
Log.logTrace("redirect to: {0} with method: {1}", redir, newMethod);
return HttpRequestImpl.newInstanceForRedirection(redir, newMethod, request);
} else {
Log.logTrace("not redirecting");
return null;
}
}
return null;
}
private URI getRedirectedURI(HttpHeaders headers) {
URI redirectedURI;
redirectedURI = headers.firstValue("Location")
.map(URI::create)
.orElseThrow(() -> new UncheckedIOException(
new IOException("Invalid redirection")));
// redirect could be relative to original URL, but if not
// then redirect is used.
redirectedURI = uri.resolve(redirectedURI);
return redirectedURI;
}
private boolean canRedirect(URI redir) {
String newScheme = redir.getScheme();
String oldScheme = uri.getScheme();
switch (policy) {
case ALWAYS:
return true;
case NEVER:
return false;
case NORMAL:
return newScheme.equalsIgnoreCase(oldScheme)
|| newScheme.equalsIgnoreCase("https");
default:
throw new InternalError();
}
}
}

View file

@ -0,0 +1,424 @@
/*
* Copyright (c) 2016, 2018, 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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Flow;
import java.util.concurrent.Flow.Publisher;
import java.util.function.Supplier;
import java.net.http.HttpRequest.BodyPublisher;
import jdk.internal.net.http.common.Utils;
public final class RequestPublishers {
private RequestPublishers() { }
public static class ByteArrayPublisher implements BodyPublisher {
private volatile Flow.Publisher<ByteBuffer> delegate;
private final int length;
private final byte[] content;
private final int offset;
private final int bufSize;
public ByteArrayPublisher(byte[] content) {
this(content, 0, content.length);
}
public ByteArrayPublisher(byte[] content, int offset, int length) {
this(content, offset, length, Utils.BUFSIZE);
}
/* bufSize exposed for testing purposes */
ByteArrayPublisher(byte[] content, int offset, int length, int bufSize) {
this.content = content;
this.offset = offset;
this.length = length;
this.bufSize = bufSize;
}
List<ByteBuffer> copy(byte[] content, int offset, int length) {
List<ByteBuffer> bufs = new ArrayList<>();
while (length > 0) {
ByteBuffer b = ByteBuffer.allocate(Math.min(bufSize, length));
int max = b.capacity();
int tocopy = Math.min(max, length);
b.put(content, offset, tocopy);
offset += tocopy;
length -= tocopy;
b.flip();
bufs.add(b);
}
return bufs;
}
@Override
public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
List<ByteBuffer> copy = copy(content, offset, length);
this.delegate = new PullPublisher<>(copy);
delegate.subscribe(subscriber);
}
@Override
public long contentLength() {
return length;
}
}
// This implementation has lots of room for improvement.
public static class IterablePublisher implements BodyPublisher {
private volatile Flow.Publisher<ByteBuffer> delegate;
private final Iterable<byte[]> content;
private volatile long contentLength;
public IterablePublisher(Iterable<byte[]> content) {
this.content = Objects.requireNonNull(content);
}
// The ByteBufferIterator will iterate over the byte[] arrays in
// the content one at the time.
//
class ByteBufferIterator implements Iterator<ByteBuffer> {
final ConcurrentLinkedQueue<ByteBuffer> buffers = new ConcurrentLinkedQueue<>();
final Iterator<byte[]> iterator = content.iterator();
@Override
public boolean hasNext() {
return !buffers.isEmpty() || iterator.hasNext();
}
@Override
public ByteBuffer next() {
ByteBuffer buffer = buffers.poll();
while (buffer == null) {
copy();
buffer = buffers.poll();
}
return buffer;
}
ByteBuffer getBuffer() {
return Utils.getBuffer();
}
void copy() {
byte[] bytes = iterator.next();
int length = bytes.length;
if (length == 0 && iterator.hasNext()) {
// avoid inserting empty buffers, except
// if that's the last.
return;
}
int offset = 0;
do {
ByteBuffer b = getBuffer();
int max = b.capacity();
int tocopy = Math.min(max, length);
b.put(bytes, offset, tocopy);
offset += tocopy;
length -= tocopy;
b.flip();
buffers.add(b);
} while (length > 0);
}
}
public Iterator<ByteBuffer> iterator() {
return new ByteBufferIterator();
}
@Override
public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
Iterable<ByteBuffer> iterable = this::iterator;
this.delegate = new PullPublisher<>(iterable);
delegate.subscribe(subscriber);
}
static long computeLength(Iterable<byte[]> bytes) {
long len = 0;
for (byte[] b : bytes) {
len = Math.addExact(len, (long)b.length);
}
return len;
}
@Override
public long contentLength() {
if (contentLength == 0) {
synchronized(this) {
if (contentLength == 0) {
contentLength = computeLength(content);
}
}
}
return contentLength;
}
}
public static class StringPublisher extends ByteArrayPublisher {
public StringPublisher(String content, Charset charset) {
super(content.getBytes(charset));
}
}
public static class EmptyPublisher implements BodyPublisher {
private final Flow.Publisher<ByteBuffer> delegate =
new PullPublisher<ByteBuffer>(Collections.emptyList(), null);
@Override
public long contentLength() {
return 0;
}
@Override
public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
delegate.subscribe(subscriber);
}
}
/**
* Publishes the content of a given file.
*
* Privileged actions are performed within a limited doPrivileged that only
* asserts the specific, read, file permission that was checked during the
* construction of this FilePublisher.
*/
public static class FilePublisher implements BodyPublisher {
private static final FilePermission[] EMPTY_FILE_PERMISSIONS = new FilePermission[0];
private final File file;
private final FilePermission[] filePermissions;
private static String pathForSecurityCheck(Path path) {
return path.toFile().getPath();
}
/**
* Factory for creating FilePublisher.
*
* Permission checks are performed here before construction of the
* FilePublisher. Permission checking and construction are deliberately
* and tightly co-located.
*/
public static FilePublisher create(Path path) throws FileNotFoundException {
FilePermission filePermission = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
String fn = pathForSecurityCheck(path);
FilePermission readPermission = new FilePermission(fn, "read");
sm.checkPermission(readPermission);
filePermission = readPermission;
}
// existence check must be after permission checks
if (Files.notExists(path))
throw new FileNotFoundException(path + " not found");
return new FilePublisher(path, filePermission);
}
private FilePublisher(Path name, FilePermission filePermission) {
assert filePermission != null ? filePermission.getActions().equals("read") : true;
file = name.toFile();
this.filePermissions = filePermission == null ? EMPTY_FILE_PERMISSIONS
: new FilePermission[] { filePermission };
}
@Override
public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
InputStream is;
if (System.getSecurityManager() == null) {
try {
is = new FileInputStream(file);
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
} else {
try {
PrivilegedExceptionAction<FileInputStream> pa =
() -> new FileInputStream(file);
is = AccessController.doPrivileged(pa, null, filePermissions);
} catch (PrivilegedActionException pae) {
throw new UncheckedIOException((IOException) pae.getCause());
}
}
PullPublisher<ByteBuffer> publisher =
new PullPublisher<>(() -> new StreamIterator(is));
publisher.subscribe(subscriber);
}
@Override
public long contentLength() {
if (System.getSecurityManager() == null) {
return file.length();
} else {
PrivilegedAction<Long> pa = () -> file.length();
return AccessController.doPrivileged(pa, null, filePermissions);
}
}
}
/**
* Reads one buffer ahead all the time, blocking in hasNext()
*/
public static class StreamIterator implements Iterator<ByteBuffer> {
final InputStream is;
final Supplier<? extends ByteBuffer> bufSupplier;
volatile ByteBuffer nextBuffer;
volatile boolean need2Read = true;
volatile boolean haveNext;
StreamIterator(InputStream is) {
this(is, Utils::getBuffer);
}
StreamIterator(InputStream is, Supplier<? extends ByteBuffer> bufSupplier) {
this.is = is;
this.bufSupplier = bufSupplier;
}
// Throwable error() {
// return error;
// }
private int read() {
nextBuffer = bufSupplier.get();
nextBuffer.clear();
byte[] buf = nextBuffer.array();
int offset = nextBuffer.arrayOffset();
int cap = nextBuffer.capacity();
try {
int n = is.read(buf, offset, cap);
if (n == -1) {
is.close();
return -1;
}
//flip
nextBuffer.limit(n);
nextBuffer.position(0);
return n;
} catch (IOException ex) {
return -1;
}
}
@Override
public synchronized boolean hasNext() {
if (need2Read) {
haveNext = read() != -1;
if (haveNext) {
need2Read = false;
}
return haveNext;
}
return haveNext;
}
@Override
public synchronized ByteBuffer next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
need2Read = true;
return nextBuffer;
}
}
public static class InputStreamPublisher implements BodyPublisher {
private final Supplier<? extends InputStream> streamSupplier;
public InputStreamPublisher(Supplier<? extends InputStream> streamSupplier) {
this.streamSupplier = Objects.requireNonNull(streamSupplier);
}
@Override
public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
PullPublisher<ByteBuffer> publisher;
InputStream is = streamSupplier.get();
if (is == null) {
Throwable t = new IOException("streamSupplier returned null");
publisher = new PullPublisher<>(null, t);
} else {
publisher = new PullPublisher<>(iterableOf(is), null);
}
publisher.subscribe(subscriber);
}
protected Iterable<ByteBuffer> iterableOf(InputStream is) {
return () -> new StreamIterator(is);
}
@Override
public long contentLength() {
return -1;
}
}
public static final class PublisherAdapter implements BodyPublisher {
private final Publisher<? extends ByteBuffer> publisher;
private final long contentLength;
public PublisherAdapter(Publisher<? extends ByteBuffer> publisher,
long contentLength) {
this.publisher = Objects.requireNonNull(publisher);
this.contentLength = contentLength;
}
@Override
public final long contentLength() {
return contentLength;
}
@Override
public final void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
publisher.subscribe(subscriber);
}
}
}

View file

@ -0,0 +1,131 @@
/*
* Copyright (c) 2016, 2018, 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.net.URI;
import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.InetSocketAddress;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSession;
import jdk.internal.net.http.common.Utils;
/**
* Response headers and status code.
*/
class Response {
final HttpHeaders headers;
final int statusCode;
final HttpRequestImpl request;
final Exchange<?> exchange;
final HttpClient.Version version;
final boolean isConnectResponse;
final SSLSession sslSession;
final InetSocketAddress localAddress;
Response(HttpRequestImpl req,
Exchange<?> exchange,
HttpHeaders headers,
HttpConnection connection,
int statusCode,
HttpClient.Version version) {
this(req, exchange, headers, connection, statusCode, version,
"CONNECT".equalsIgnoreCase(req.method()));
}
Response(HttpRequestImpl req,
Exchange<?> exchange,
HttpHeaders headers,
HttpConnection connection,
int statusCode,
HttpClient.Version version,
boolean isConnectResponse) {
this.headers = ImmutableHeaders.of(headers);
this.request = req;
this.version = version;
this.exchange = exchange;
this.statusCode = statusCode;
this.isConnectResponse = isConnectResponse;
if (connection != null) {
InetSocketAddress a;
try {
a = (InetSocketAddress)connection.channel().getLocalAddress();
} catch (IOException e) {
a = null;
}
this.localAddress = a;
if (connection instanceof AbstractAsyncSSLConnection) {
AbstractAsyncSSLConnection cc = (AbstractAsyncSSLConnection)connection;
SSLEngine engine = cc.getEngine();
sslSession = Utils.immutableSession(engine.getSession());
} else {
sslSession = null;
}
} else {
sslSession = null;
localAddress = null;
}
}
HttpRequestImpl request() {
return request;
}
HttpClient.Version version() {
return version;
}
HttpHeaders headers() {
return headers;
}
int statusCode() {
return statusCode;
}
SSLSession getSSLSession() {
return sslSession;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
String method = request().method();
URI uri = request().uri();
String uristring = uri == null ? "" : uri.toString();
sb.append('(')
.append(method)
.append(" ")
.append(uristring)
.append(") ")
.append(statusCode());
sb.append(" ").append(version);
if (localAddress != null)
sb.append(" Local port: ").append(localAddress.getPort());
return sb.toString();
}
}

View file

@ -0,0 +1,279 @@
/*
* Copyright (c) 2018, 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.File;
import java.io.FilePermission;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.ResponseInfo;
import java.net.http.HttpResponse.BodySubscriber;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jdk.internal.net.http.ResponseSubscribers.PathSubscriber;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
public final class ResponseBodyHandlers {
private ResponseBodyHandlers() { }
private static final String pathForSecurityCheck(Path path) {
return path.toFile().getPath();
}
/**
* A Path body handler.
*/
public static class PathBodyHandler implements BodyHandler<Path>{
private final Path file;
private final List<OpenOption> openOptions; // immutable list
private final FilePermission filePermission;
/**
* Factory for creating PathBodyHandler.
*
* Permission checks are performed here before construction of the
* PathBodyHandler. Permission checking and construction are
* deliberately and tightly co-located.
*/
public static PathBodyHandler create(Path file,
List<OpenOption> openOptions) {
FilePermission filePermission = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
String fn = pathForSecurityCheck(file);
FilePermission writePermission = new FilePermission(fn, "write");
sm.checkPermission(writePermission);
filePermission = writePermission;
}
return new PathBodyHandler(file, openOptions, filePermission);
}
private PathBodyHandler(Path file,
List<OpenOption> openOptions,
FilePermission filePermission) {
this.file = file;
this.openOptions = openOptions;
this.filePermission = filePermission;
}
@Override
public BodySubscriber<Path> apply(ResponseInfo responseInfo) {
return new PathSubscriber(file, openOptions, filePermission);
}
}
/** With push promise Map implementation */
public static class PushPromisesHandlerWithMap<T>
implements HttpResponse.PushPromiseHandler<T>
{
private final ConcurrentMap<HttpRequest,CompletableFuture<HttpResponse<T>>> pushPromisesMap;
private final Function<HttpRequest,BodyHandler<T>> pushPromiseHandler;
public PushPromisesHandlerWithMap(Function<HttpRequest,BodyHandler<T>> pushPromiseHandler,
ConcurrentMap<HttpRequest,CompletableFuture<HttpResponse<T>>> pushPromisesMap) {
this.pushPromiseHandler = pushPromiseHandler;
this.pushPromisesMap = pushPromisesMap;
}
@Override
public void applyPushPromise(
HttpRequest initiatingRequest, HttpRequest pushRequest,
Function<BodyHandler<T>,CompletableFuture<HttpResponse<T>>> acceptor)
{
URI initiatingURI = initiatingRequest.uri();
URI pushRequestURI = pushRequest.uri();
if (!initiatingURI.getHost().equalsIgnoreCase(pushRequestURI.getHost()))
return;
int initiatingPort = initiatingURI.getPort();
if (initiatingPort == -1 ) {
if ("https".equalsIgnoreCase(initiatingURI.getScheme()))
initiatingPort = 443;
else
initiatingPort = 80;
}
int pushPort = pushRequestURI.getPort();
if (pushPort == -1 ) {
if ("https".equalsIgnoreCase(pushRequestURI.getScheme()))
pushPort = 443;
else
pushPort = 80;
}
if (initiatingPort != pushPort)
return;
CompletableFuture<HttpResponse<T>> cf =
acceptor.apply(pushPromiseHandler.apply(pushRequest));
pushPromisesMap.put(pushRequest, cf);
}
}
// Similar to Path body handler, but for file download.
public static class FileDownloadBodyHandler implements BodyHandler<Path> {
private final Path directory;
private final List<OpenOption> openOptions;
private final FilePermission[] filePermissions; // may be null
/**
* Factory for creating FileDownloadBodyHandler.
*
* Permission checks are performed here before construction of the
* FileDownloadBodyHandler. Permission checking and construction are
* deliberately and tightly co-located.
*/
public static FileDownloadBodyHandler create(Path directory,
List<OpenOption> openOptions) {
FilePermission filePermissions[] = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
String fn = pathForSecurityCheck(directory);
FilePermission writePermission = new FilePermission(fn, "write");
String writePathPerm = fn + File.separatorChar + "*";
FilePermission writeInDirPermission = new FilePermission(writePathPerm, "write");
sm.checkPermission(writeInDirPermission);
FilePermission readPermission = new FilePermission(fn, "read");
sm.checkPermission(readPermission);
// read permission is only needed before determine the below checks
// only write permission is required when downloading to the file
filePermissions = new FilePermission[] { writePermission, writeInDirPermission };
}
// existence, etc, checks must be after permission checks
if (Files.notExists(directory))
throw new IllegalArgumentException("non-existent directory: " + directory);
if (!Files.isDirectory(directory))
throw new IllegalArgumentException("not a directory: " + directory);
if (!Files.isWritable(directory))
throw new IllegalArgumentException("non-writable directory: " + directory);
return new FileDownloadBodyHandler(directory, openOptions, filePermissions);
}
private FileDownloadBodyHandler(Path directory,
List<OpenOption> openOptions,
FilePermission... filePermissions) {
this.directory = directory;
this.openOptions = openOptions;
this.filePermissions = filePermissions;
}
/** The "attachment" disposition-type and separator. */
static final String DISPOSITION_TYPE = "attachment;";
/** The "filename" parameter. */
static final Pattern FILENAME = Pattern.compile("filename\\s*=", CASE_INSENSITIVE);
static final List<String> PROHIBITED = List.of(".", "..", "", "~" , "|");
static final UncheckedIOException unchecked(ResponseInfo rinfo,
String msg) {
String s = String.format("%s in response [%d, %s]", msg, rinfo.statusCode(), rinfo.headers());
return new UncheckedIOException(new IOException(s));
}
@Override
public BodySubscriber<Path> apply(ResponseInfo responseInfo) {
String dispoHeader = responseInfo.headers().firstValue("Content-Disposition")
.orElseThrow(() -> unchecked(responseInfo, "No Content-Disposition header"));
if (!dispoHeader.regionMatches(true, // ignoreCase
0, DISPOSITION_TYPE,
0, DISPOSITION_TYPE.length())) {
throw unchecked(responseInfo, "Unknown Content-Disposition type");
}
Matcher matcher = FILENAME.matcher(dispoHeader);
if (!matcher.find()) {
throw unchecked(responseInfo, "Bad Content-Disposition filename parameter");
}
int n = matcher.end();
int semi = dispoHeader.substring(n).indexOf(";");
String filenameParam;
if (semi < 0) {
filenameParam = dispoHeader.substring(n);
} else {
filenameParam = dispoHeader.substring(n, n + semi);
}
// strip all but the last path segment
int x = filenameParam.lastIndexOf("/");
if (x != -1) {
filenameParam = filenameParam.substring(x+1);
}
x = filenameParam.lastIndexOf("\\");
if (x != -1) {
filenameParam = filenameParam.substring(x+1);
}
filenameParam = filenameParam.trim();
if (filenameParam.startsWith("\"")) { // quoted-string
if (!filenameParam.endsWith("\"") || filenameParam.length() == 1) {
throw unchecked(responseInfo,
"Badly quoted Content-Disposition filename parameter");
}
filenameParam = filenameParam.substring(1, filenameParam.length() -1 );
} else { // token,
if (filenameParam.contains(" ")) { // space disallowed
throw unchecked(responseInfo,
"unquoted space in Content-Disposition filename parameter");
}
}
if (PROHIBITED.contains(filenameParam)) {
throw unchecked(responseInfo,
"Prohibited Content-Disposition filename parameter:"
+ filenameParam);
}
Path file = Paths.get(directory.toString(), filenameParam);
if (!file.startsWith(directory)) {
throw unchecked(responseInfo,
"Resulting file, " + file.toString() + ", outside of given directory");
}
return new PathSubscriber(file, openOptions, filePermissions);
}
}
}

View file

@ -0,0 +1,476 @@
/*
* Copyright (c) 2015, 2018, 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.lang.System.Logger.Level;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.net.http.HttpHeaders;
import java.net.http.HttpResponse;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.Utils;
/**
* Implements chunked/fixed transfer encodings of HTTP/1.1 responses.
*
* Call pushBody() to read the body (blocking). Data and errors are provided
* to given Consumers. After final buffer delivered, empty optional delivered
*/
class ResponseContent {
final HttpResponse.BodySubscriber<?> pusher;
final int contentLength;
final HttpHeaders headers;
// this needs to run before we complete the body
// so that connection can be returned to pool
private final Runnable onFinished;
private final String dbgTag;
ResponseContent(HttpConnection connection,
int contentLength,
HttpHeaders h,
HttpResponse.BodySubscriber<?> userSubscriber,
Runnable onFinished)
{
this.pusher = userSubscriber;
this.contentLength = contentLength;
this.headers = h;
this.onFinished = onFinished;
this.dbgTag = connection.dbgString() + "/ResponseContent";
}
static final int LF = 10;
static final int CR = 13;
private boolean chunkedContent, chunkedContentInitialized;
boolean contentChunked() throws IOException {
if (chunkedContentInitialized) {
return chunkedContent;
}
if (contentLength == -1) {
String tc = headers.firstValue("Transfer-Encoding")
.orElse("");
if (!tc.equals("")) {
if (tc.equalsIgnoreCase("chunked")) {
chunkedContent = true;
} else {
throw new IOException("invalid content");
}
} else {
chunkedContent = false;
}
}
chunkedContentInitialized = true;
return chunkedContent;
}
interface BodyParser extends Consumer<ByteBuffer> {
void onSubscribe(AbstractSubscription sub);
}
// Returns a parser that will take care of parsing the received byte
// buffers and forward them to the BodySubscriber.
// When the parser is done, it will call onComplete.
// If parsing was successful, the throwable parameter will be null.
// Otherwise it will be the exception that occurred
// Note: revisit: it might be better to use a CompletableFuture than
// a completion handler.
BodyParser getBodyParser(Consumer<Throwable> onComplete)
throws IOException {
if (contentChunked()) {
return new ChunkedBodyParser(onComplete);
} else {
return new FixedLengthBodyParser(contentLength, onComplete);
}
}
static enum ChunkState {READING_LENGTH, READING_DATA, DONE}
class ChunkedBodyParser implements BodyParser {
final ByteBuffer READMORE = Utils.EMPTY_BYTEBUFFER;
final Consumer<Throwable> onComplete;
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
final String dbgTag = ResponseContent.this.dbgTag + "/ChunkedBodyParser";
volatile Throwable closedExceptionally;
volatile int partialChunklen = 0; // partially read chunk len
volatile int chunklen = -1; // number of bytes in chunk
volatile int bytesremaining; // number of bytes in chunk left to be read incl CRLF
volatile boolean cr = false; // tryReadChunkLength has found CR
volatile int bytesToConsume; // number of bytes that still need to be consumed before proceeding
volatile ChunkState state = ChunkState.READING_LENGTH; // current state
volatile AbstractSubscription sub;
ChunkedBodyParser(Consumer<Throwable> onComplete) {
this.onComplete = onComplete;
}
String dbgString() {
return dbgTag;
}
@Override
public void onSubscribe(AbstractSubscription sub) {
if (debug.on())
debug.log("onSubscribe: " + pusher.getClass().getName());
pusher.onSubscribe(this.sub = sub);
}
@Override
public void accept(ByteBuffer b) {
if (closedExceptionally != null) {
if (debug.on())
debug.log("already closed: " + closedExceptionally);
return;
}
boolean completed = false;
try {
List<ByteBuffer> out = new ArrayList<>();
do {
if (tryPushOneHunk(b, out)) {
// We're done! (true if the final chunk was parsed).
if (!out.isEmpty()) {
// push what we have and complete
// only reduce demand if we actually push something.
// we would not have come here if there was no
// demand.
boolean hasDemand = sub.demand().tryDecrement();
assert hasDemand;
pusher.onNext(Collections.unmodifiableList(out));
if (debug.on()) debug.log("Chunks sent");
}
if (debug.on()) debug.log("done!");
assert closedExceptionally == null;
assert state == ChunkState.DONE;
onFinished.run();
pusher.onComplete();
if (debug.on()) debug.log("subscriber completed");
completed = true;
onComplete.accept(closedExceptionally); // should be null
break;
}
// the buffer may contain several hunks, and therefore
// we must loop while it's not exhausted.
} while (b.hasRemaining());
if (!completed && !out.isEmpty()) {
// push what we have.
// only reduce demand if we actually push something.
// we would not have come here if there was no
// demand.
boolean hasDemand = sub.demand().tryDecrement();
assert hasDemand;
pusher.onNext(Collections.unmodifiableList(out));
if (debug.on()) debug.log("Chunk sent");
}
assert state == ChunkState.DONE || !b.hasRemaining();
} catch(Throwable t) {
if (debug.on())
debug.log("Error while processing buffer: %s", (Object)t );
closedExceptionally = t;
if (!completed) onComplete.accept(t);
}
}
// reads and returns chunklen. Position of chunkbuf is first byte
// of chunk on return. chunklen includes the CR LF at end of chunk
// returns -1 if needs more bytes
private int tryReadChunkLen(ByteBuffer chunkbuf) throws IOException {
assert state == ChunkState.READING_LENGTH;
while (chunkbuf.hasRemaining()) {
int c = chunkbuf.get();
if (cr) {
if (c == LF) {
return partialChunklen;
} else {
throw new IOException("invalid chunk header");
}
}
if (c == CR) {
cr = true;
} else {
int digit = toDigit(c);
partialChunklen = partialChunklen * 16 + digit;
}
}
return -1;
}
// try to consume as many bytes as specified by bytesToConsume.
// returns the number of bytes that still need to be consumed.
// In practice this method is only called to consume one CRLF pair
// with bytesToConsume set to 2, so it will only return 0 (if completed),
// 1, or 2 (if chunkbuf doesn't have the 2 chars).
private int tryConsumeBytes(ByteBuffer chunkbuf) throws IOException {
int n = bytesToConsume;
if (n > 0) {
int e = Math.min(chunkbuf.remaining(), n);
// verifies some assertions
// this methods is called only to consume CRLF
if (Utils.ASSERTIONSENABLED) {
assert n <= 2 && e <= 2;
ByteBuffer tmp = chunkbuf.slice();
// if n == 2 assert that we will first consume CR
assert (n == 2 && e > 0) ? tmp.get() == CR : true;
// if n == 1 || n == 2 && e == 2 assert that we then consume LF
assert (n == 1 || e == 2) ? tmp.get() == LF : true;
}
chunkbuf.position(chunkbuf.position() + e);
n -= e;
bytesToConsume = n;
}
assert n >= 0;
return n;
}
/**
* Returns a ByteBuffer containing chunk of data or a "hunk" of data
* (a chunk of a chunk if the chunk size is larger than our ByteBuffers).
* If the given chunk does not have enough data this method return
* an empty ByteBuffer (READMORE).
* If we encounter the final chunk (an empty chunk) this method
* returns null.
*/
ByteBuffer tryReadOneHunk(ByteBuffer chunk) throws IOException {
int unfulfilled = bytesremaining;
int toconsume = bytesToConsume;
ChunkState st = state;
if (st == ChunkState.READING_LENGTH && chunklen == -1) {
if (debug.on()) debug.log(() -> "Trying to read chunk len"
+ " (remaining in buffer:"+chunk.remaining()+")");
int clen = chunklen = tryReadChunkLen(chunk);
if (clen == -1) return READMORE;
if (debug.on()) debug.log("Got chunk len %d", clen);
cr = false; partialChunklen = 0;
unfulfilled = bytesremaining = clen;
if (clen == 0) toconsume = bytesToConsume = 2; // that was the last chunk
else st = state = ChunkState.READING_DATA; // read the data
}
if (toconsume > 0) {
if (debug.on())
debug.log("Trying to consume bytes: %d (remaining in buffer: %s)",
toconsume, chunk.remaining());
if (tryConsumeBytes(chunk) > 0) {
return READMORE;
}
}
toconsume = bytesToConsume;
assert toconsume == 0;
if (st == ChunkState.READING_LENGTH) {
// we will come here only if chunklen was 0, after having
// consumed the trailing CRLF
int clen = chunklen;
assert clen == 0;
if (debug.on()) debug.log("No more chunks: %d", clen);
// the DONE state is not really needed but it helps with
// assertions...
state = ChunkState.DONE;
return null;
}
int clen = chunklen;
assert clen > 0;
assert st == ChunkState.READING_DATA;
ByteBuffer returnBuffer = READMORE; // May be a hunk or a chunk
if (unfulfilled > 0) {
int bytesread = chunk.remaining();
if (debug.on())
debug.log("Reading chunk: available %d, needed %d",
bytesread, unfulfilled);
int bytes2return = Math.min(bytesread, unfulfilled);
if (debug.on())
debug.log( "Returning chunk bytes: %d", bytes2return);
returnBuffer = Utils.sliceWithLimitedCapacity(chunk, bytes2return).asReadOnlyBuffer();
unfulfilled = bytesremaining -= bytes2return;
if (unfulfilled == 0) bytesToConsume = 2;
}
assert unfulfilled >= 0;
if (unfulfilled == 0) {
if (debug.on())
debug.log("No more bytes to read - %d yet to consume.",
unfulfilled);
// check whether the trailing CRLF is consumed, try to
// consume it if not. If tryConsumeBytes needs more bytes
// then we will come back here later - skipping the block
// that reads data because remaining==0, and finding
// that the two bytes are now consumed.
if (tryConsumeBytes(chunk) == 0) {
// we're done for this chunk! reset all states and
// prepare to read the next chunk.
chunklen = -1;
partialChunklen = 0;
cr = false;
state = ChunkState.READING_LENGTH;
if (debug.on()) debug.log("Ready to read next chunk");
}
}
if (returnBuffer == READMORE) {
if (debug.on()) debug.log("Need more data");
}
return returnBuffer;
}
// Attempt to parse and push one hunk from the buffer.
// Returns true if the final chunk was parsed.
// Returns false if we need to push more chunks.
private boolean tryPushOneHunk(ByteBuffer b, List<ByteBuffer> out)
throws IOException {
assert state != ChunkState.DONE;
ByteBuffer b1 = tryReadOneHunk(b);
if (b1 != null) {
//assert b1.hasRemaining() || b1 == READMORE;
if (b1.hasRemaining()) {
if (debug.on())
debug.log("Sending chunk to consumer (%d)", b1.remaining());
out.add(b1);
}
return false; // we haven't parsed the final chunk yet.
} else {
return true; // we're done! the final chunk was parsed.
}
}
private int toDigit(int b) throws IOException {
if (b >= 0x30 && b <= 0x39) {
return b - 0x30;
}
if (b >= 0x41 && b <= 0x46) {
return b - 0x41 + 10;
}
if (b >= 0x61 && b <= 0x66) {
return b - 0x61 + 10;
}
throw new IOException("Invalid chunk header byte " + b);
}
}
class FixedLengthBodyParser implements BodyParser {
final int contentLength;
final Consumer<Throwable> onComplete;
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
final String dbgTag = ResponseContent.this.dbgTag + "/FixedLengthBodyParser";
volatile int remaining;
volatile Throwable closedExceptionally;
volatile AbstractSubscription sub;
FixedLengthBodyParser(int contentLength, Consumer<Throwable> onComplete) {
this.contentLength = this.remaining = contentLength;
this.onComplete = onComplete;
}
String dbgString() {
return dbgTag;
}
@Override
public void onSubscribe(AbstractSubscription sub) {
if (debug.on())
debug.log("length=" + contentLength +", onSubscribe: "
+ pusher.getClass().getName());
pusher.onSubscribe(this.sub = sub);
try {
if (contentLength == 0) {
onFinished.run();
pusher.onComplete();
onComplete.accept(null);
}
} catch (Throwable t) {
closedExceptionally = t;
try {
pusher.onError(t);
} finally {
onComplete.accept(t);
}
}
}
@Override
public void accept(ByteBuffer b) {
if (closedExceptionally != null) {
if (debug.on())
debug.log("already closed: " + closedExceptionally);
return;
}
boolean completed = false;
try {
int unfulfilled = remaining;
if (debug.on())
debug.log("Parser got %d bytes (%d remaining / %d)",
b.remaining(), unfulfilled, contentLength);
assert unfulfilled != 0 || contentLength == 0 || b.remaining() == 0;
if (unfulfilled == 0 && contentLength > 0) return;
if (b.hasRemaining() && unfulfilled > 0) {
// only reduce demand if we actually push something.
// we would not have come here if there was no
// demand.
boolean hasDemand = sub.demand().tryDecrement();
assert hasDemand;
int amount = Math.min(b.remaining(), unfulfilled);
unfulfilled = remaining -= amount;
ByteBuffer buffer = Utils.sliceWithLimitedCapacity(b, amount);
pusher.onNext(List.of(buffer.asReadOnlyBuffer()));
}
if (unfulfilled == 0) {
// We're done! All data has been received.
if (debug.on())
debug.log("Parser got all expected bytes: completing");
assert closedExceptionally == null;
onFinished.run();
pusher.onComplete();
completed = true;
onComplete.accept(closedExceptionally); // should be null
} else {
assert b.remaining() == 0;
}
} catch (Throwable t) {
if (debug.on()) debug.log("Unexpected exception", t);
closedExceptionally = t;
if (!completed) {
onComplete.accept(t);
}
}
}
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2018, 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.net.http.HttpResponse.ResponseInfo;
import java.net.http.HttpHeaders;
import java.net.http.HttpClient;
class ResponseInfoImpl implements ResponseInfo {
private final int statusCode;
private final HttpHeaders headers;
private final HttpClient.Version version;
ResponseInfoImpl(Response response) {
this.statusCode = response.statusCode();
this.headers = response.headers();
this.version = response.version();
}
ResponseInfoImpl(int statusCode, HttpHeaders headers, HttpClient.Version version) {
this.statusCode = statusCode;
this.headers = headers;
this.version = version;
}
/**
* Provides the response status code
* @return the response status code
*/
public int statusCode() {
return statusCode;
}
/**
* Provides the response headers
* @return the response headers
*/
public HttpHeaders headers() {
return headers;
}
/**
* provides the response protocol version
* @return the response protocol version
*/
public HttpClient.Version version() {
return version;
}
}

View file

@ -0,0 +1,897 @@
/*
* Copyright (c) 2016, 2018, 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.BufferedReader;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.System.Logger.Level;
import java.net.http.HttpHeaders;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Flow;
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Subscription;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import java.net.http.HttpResponse.BodySubscriber;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.Utils;
import static java.nio.charset.StandardCharsets.UTF_8;
public class ResponseSubscribers {
public static class ConsumerSubscriber implements BodySubscriber<Void> {
private final Consumer<Optional<byte[]>> consumer;
private Flow.Subscription subscription;
private final CompletableFuture<Void> result = new MinimalFuture<>();
private final AtomicBoolean subscribed = new AtomicBoolean();
public ConsumerSubscriber(Consumer<Optional<byte[]>> consumer) {
this.consumer = Objects.requireNonNull(consumer);
}
@Override
public CompletionStage<Void> getBody() {
return result;
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
if (!subscribed.compareAndSet(false, true)) {
subscription.cancel();
} else {
this.subscription = subscription;
subscription.request(1);
}
}
@Override
public void onNext(List<ByteBuffer> items) {
for (ByteBuffer item : items) {
byte[] buf = new byte[item.remaining()];
item.get(buf);
consumer.accept(Optional.of(buf));
}
subscription.request(1);
}
@Override
public void onError(Throwable throwable) {
result.completeExceptionally(throwable);
}
@Override
public void onComplete() {
consumer.accept(Optional.empty());
result.complete(null);
}
}
/**
* A Subscriber that writes the flow of data to a given file.
*
* Privileged actions are performed within a limited doPrivileged that only
* asserts the specific, write, file permissions that were checked during
* the construction of this PathSubscriber.
*/
public static class PathSubscriber implements BodySubscriber<Path> {
private static final FilePermission[] EMPTY_FILE_PERMISSIONS = new FilePermission[0];
private final Path file;
private final OpenOption[] options;
private final FilePermission[] filePermissions;
private final CompletableFuture<Path> result = new MinimalFuture<>();
private volatile Flow.Subscription subscription;
private volatile FileChannel out;
private static final String pathForSecurityCheck(Path path) {
return path.toFile().getPath();
}
/**
* Factory for creating PathSubscriber.
*
* Permission checks are performed here before construction of the
* PathSubscriber. Permission checking and construction are deliberately
* and tightly co-located.
*/
public static PathSubscriber create(Path file,
List<OpenOption> options) {
FilePermission filePermission = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
String fn = pathForSecurityCheck(file);
FilePermission writePermission = new FilePermission(fn, "write");
sm.checkPermission(writePermission);
filePermission = writePermission;
}
return new PathSubscriber(file, options, filePermission);
}
// pp so handler implementations in the same package can construct
/*package-private*/ PathSubscriber(Path file,
List<OpenOption> options,
FilePermission... filePermissions) {
this.file = file;
this.options = options.stream().toArray(OpenOption[]::new);
this.filePermissions =
filePermissions == null ? EMPTY_FILE_PERMISSIONS : filePermissions;
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
if (System.getSecurityManager() == null) {
try {
out = FileChannel.open(file, options);
} catch (IOException ioe) {
result.completeExceptionally(ioe);
return;
}
} else {
try {
PrivilegedExceptionAction<FileChannel> pa =
() -> FileChannel.open(file, options);
out = AccessController.doPrivileged(pa, null, filePermissions);
} catch (PrivilegedActionException pae) {
Throwable t = pae.getCause() != null ? pae.getCause() : pae;
result.completeExceptionally(t);
subscription.cancel();
return;
}
}
subscription.request(1);
}
@Override
public void onNext(List<ByteBuffer> items) {
try {
out.write(items.toArray(Utils.EMPTY_BB_ARRAY));
} catch (IOException ex) {
Utils.close(out);
subscription.cancel();
result.completeExceptionally(ex);
}
subscription.request(1);
}
@Override
public void onError(Throwable e) {
result.completeExceptionally(e);
Utils.close(out);
}
@Override
public void onComplete() {
Utils.close(out);
result.complete(file);
}
@Override
public CompletionStage<Path> getBody() {
return result;
}
}
public static class ByteArraySubscriber<T> implements BodySubscriber<T> {
private final Function<byte[], T> finisher;
private final CompletableFuture<T> result = new MinimalFuture<>();
private final List<ByteBuffer> received = new ArrayList<>();
private volatile Flow.Subscription subscription;
public ByteArraySubscriber(Function<byte[],T> finisher) {
this.finisher = finisher;
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
if (this.subscription != null) {
subscription.cancel();
return;
}
this.subscription = subscription;
// We can handle whatever you've got
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(List<ByteBuffer> items) {
// incoming buffers are allocated by http client internally,
// and won't be used anywhere except this place.
// So it's free simply to store them for further processing.
assert Utils.hasRemaining(items);
received.addAll(items);
}
@Override
public void onError(Throwable throwable) {
received.clear();
result.completeExceptionally(throwable);
}
static private byte[] join(List<ByteBuffer> bytes) {
int size = Utils.remaining(bytes, Integer.MAX_VALUE);
byte[] res = new byte[size];
int from = 0;
for (ByteBuffer b : bytes) {
int l = b.remaining();
b.get(res, from, l);
from += l;
}
return res;
}
@Override
public void onComplete() {
try {
result.complete(finisher.apply(join(received)));
received.clear();
} catch (IllegalArgumentException e) {
result.completeExceptionally(e);
}
}
@Override
public CompletionStage<T> getBody() {
return result;
}
}
/**
* An InputStream built on top of the Flow API.
*/
public static class HttpResponseInputStream extends InputStream
implements BodySubscriber<InputStream>
{
final static int MAX_BUFFERS_IN_QUEUE = 1; // lock-step with the producer
// An immutable ByteBuffer sentinel to mark that the last byte was received.
private static final ByteBuffer LAST_BUFFER = ByteBuffer.wrap(new byte[0]);
private static final List<ByteBuffer> LAST_LIST = List.of(LAST_BUFFER);
private static final Logger debug =
Utils.getDebugLogger("HttpResponseInputStream"::toString, Utils.DEBUG);
// A queue of yet unprocessed ByteBuffers received from the flow API.
private final BlockingQueue<List<ByteBuffer>> buffers;
private volatile Flow.Subscription subscription;
private volatile boolean closed;
private volatile Throwable failed;
private volatile Iterator<ByteBuffer> currentListItr;
private volatile ByteBuffer currentBuffer;
private final AtomicBoolean subscribed = new AtomicBoolean();
public HttpResponseInputStream() {
this(MAX_BUFFERS_IN_QUEUE);
}
HttpResponseInputStream(int maxBuffers) {
int capacity = (maxBuffers <= 0 ? MAX_BUFFERS_IN_QUEUE : maxBuffers);
// 1 additional slot needed for LAST_LIST added by onComplete
this.buffers = new ArrayBlockingQueue<>(capacity + 1);
}
@Override
public CompletionStage<InputStream> getBody() {
// Returns the stream immediately, before the
// response body is received.
// This makes it possible for sendAsync().get().body()
// to complete before the response body is received.
return CompletableFuture.completedStage(this);
}
// Returns the current byte buffer to read from.
// If the current buffer has no remaining data, this method will take the
// next buffer from the buffers queue, possibly blocking until
// a new buffer is made available through the Flow API, or the
// end of the flow has been reached.
private ByteBuffer current() throws IOException {
while (currentBuffer == null || !currentBuffer.hasRemaining()) {
// Check whether the stream is closed or exhausted
if (closed || failed != null) {
throw new IOException("closed", failed);
}
if (currentBuffer == LAST_BUFFER) break;
try {
if (currentListItr == null || !currentListItr.hasNext()) {
// Take a new list of buffers from the queue, blocking
// if none is available yet...
if (debug.on()) debug.log("Taking list of Buffers");
List<ByteBuffer> lb = buffers.take();
currentListItr = lb.iterator();
if (debug.on()) debug.log("List of Buffers Taken");
// Check whether an exception was encountered upstream
if (closed || failed != null)
throw new IOException("closed", failed);
// Check whether we're done.
if (lb == LAST_LIST) {
currentListItr = null;
currentBuffer = LAST_BUFFER;
break;
}
// Request another upstream item ( list of buffers )
Flow.Subscription s = subscription;
if (s != null) {
if (debug.on()) debug.log("Increased demand by 1");
s.request(1);
}
assert currentListItr != null;
if (lb.isEmpty()) continue;
}
assert currentListItr != null;
assert currentListItr.hasNext();
if (debug.on()) debug.log("Next Buffer");
currentBuffer = currentListItr.next();
} catch (InterruptedException ex) {
// continue
}
}
assert currentBuffer == LAST_BUFFER || currentBuffer.hasRemaining();
return currentBuffer;
}
@Override
public int read(byte[] bytes, int off, int len) throws IOException {
// get the buffer to read from, possibly blocking if
// none is available
ByteBuffer buffer;
if ((buffer = current()) == LAST_BUFFER) return -1;
// don't attempt to read more than what is available
// in the current buffer.
int read = Math.min(buffer.remaining(), len);
assert read > 0 && read <= buffer.remaining();
// buffer.get() will do the boundary check for us.
buffer.get(bytes, off, read);
return read;
}
@Override
public int read() throws IOException {
ByteBuffer buffer;
if ((buffer = current()) == LAST_BUFFER) return -1;
return buffer.get() & 0xFF;
}
@Override
public void onSubscribe(Flow.Subscription s) {
try {
if (!subscribed.compareAndSet(false, true)) {
s.cancel();
} else {
// check whether the stream is already closed.
// if so, we should cancel the subscription
// immediately.
boolean closed;
synchronized (this) {
closed = this.closed;
if (!closed) {
this.subscription = s;
}
}
if (closed) {
s.cancel();
return;
}
assert buffers.remainingCapacity() > 1; // should contain at least 2
if (debug.on())
debug.log("onSubscribe: requesting "
+ Math.max(1, buffers.remainingCapacity() - 1));
s.request(Math.max(1, buffers.remainingCapacity() - 1));
}
} catch (Throwable t) {
failed = t;
try {
close();
} catch (IOException x) {
// OK
} finally {
onError(t);
}
}
}
@Override
public void onNext(List<ByteBuffer> t) {
Objects.requireNonNull(t);
try {
if (debug.on()) debug.log("next item received");
if (!buffers.offer(t)) {
throw new IllegalStateException("queue is full");
}
if (debug.on()) debug.log("item offered");
} catch (Throwable ex) {
failed = ex;
try {
close();
} catch (IOException ex1) {
// OK
} finally {
onError(ex);
}
}
}
@Override
public void onError(Throwable thrwbl) {
subscription = null;
failed = Objects.requireNonNull(thrwbl);
// The client process that reads the input stream might
// be blocked in queue.take().
// Tries to offer LAST_LIST to the queue. If the queue is
// full we don't care if we can't insert this buffer, as
// the client can't be blocked in queue.take() in that case.
// Adding LAST_LIST to the queue is harmless, as the client
// should find failed != null before handling LAST_LIST.
buffers.offer(LAST_LIST);
}
@Override
public void onComplete() {
subscription = null;
onNext(LAST_LIST);
}
@Override
public void close() throws IOException {
Flow.Subscription s;
synchronized (this) {
if (closed) return;
closed = true;
s = subscription;
subscription = null;
}
// s will be null if already completed
try {
if (s != null) {
s.cancel();
}
} finally {
buffers.offer(LAST_LIST);
super.close();
}
}
}
public static BodySubscriber<Stream<String>> createLineStream() {
return createLineStream(UTF_8);
}
public static BodySubscriber<Stream<String>> createLineStream(Charset charset) {
Objects.requireNonNull(charset);
BodySubscriber<InputStream> s = new HttpResponseInputStream();
return new MappingSubscriber<InputStream,Stream<String>>(s,
(InputStream stream) -> {
return new BufferedReader(new InputStreamReader(stream, charset))
.lines().onClose(() -> Utils.close(stream));
});
}
/**
* Currently this consumes all of the data and ignores it
*/
public static class NullSubscriber<T> implements BodySubscriber<T> {
private final CompletableFuture<T> cf = new MinimalFuture<>();
private final Optional<T> result;
private final AtomicBoolean subscribed = new AtomicBoolean();
public NullSubscriber(Optional<T> result) {
this.result = result;
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
if (!subscribed.compareAndSet(false, true)) {
subscription.cancel();
} else {
subscription.request(Long.MAX_VALUE);
}
}
@Override
public void onNext(List<ByteBuffer> items) {
Objects.requireNonNull(items);
}
@Override
public void onError(Throwable throwable) {
cf.completeExceptionally(throwable);
}
@Override
public void onComplete() {
if (result.isPresent()) {
cf.complete(result.get());
} else {
cf.complete(null);
}
}
@Override
public CompletionStage<T> getBody() {
return cf;
}
}
/** An adapter between {@code BodySubscriber} and {@code Flow.Subscriber}. */
public static final class SubscriberAdapter<S extends Subscriber<? super List<ByteBuffer>>,R>
implements BodySubscriber<R>
{
private final CompletableFuture<R> cf = new MinimalFuture<>();
private final S subscriber;
private final Function<? super S,? extends R> finisher;
private volatile Subscription subscription;
public SubscriberAdapter(S subscriber, Function<? super S,? extends R> finisher) {
this.subscriber = Objects.requireNonNull(subscriber);
this.finisher = Objects.requireNonNull(finisher);
}
@Override
public void onSubscribe(Subscription subscription) {
Objects.requireNonNull(subscription);
if (this.subscription != null) {
subscription.cancel();
} else {
this.subscription = subscription;
subscriber.onSubscribe(subscription);
}
}
@Override
public void onNext(List<ByteBuffer> item) {
Objects.requireNonNull(item);
try {
subscriber.onNext(item);
} catch (Throwable throwable) {
subscription.cancel();
onError(throwable);
}
}
@Override
public void onError(Throwable throwable) {
Objects.requireNonNull(throwable);
try {
subscriber.onError(throwable);
} finally {
cf.completeExceptionally(throwable);
}
}
@Override
public void onComplete() {
try {
subscriber.onComplete();
} finally {
try {
cf.complete(finisher.apply(subscriber));
} catch (Throwable throwable) {
cf.completeExceptionally(throwable);
}
}
}
@Override
public CompletionStage<R> getBody() {
return cf;
}
}
/**
* A body subscriber which receives input from an upstream subscriber
* and maps that subscriber's body type to a new type. The upstream subscriber
* delegates all flow operations directly to this object. The
* {@link CompletionStage} returned by {@link #getBody()}} takes the output
* of the upstream {@code getBody()} and applies the mapper function to
* obtain the new {@code CompletionStage} type.
*
* @param <T> the upstream body type
* @param <U> this subscriber's body type
*/
public static class MappingSubscriber<T,U> implements BodySubscriber<U> {
private final BodySubscriber<T> upstream;
private final Function<? super T,? extends U> mapper;
public MappingSubscriber(BodySubscriber<T> upstream,
Function<? super T,? extends U> mapper) {
this.upstream = Objects.requireNonNull(upstream);
this.mapper = Objects.requireNonNull(mapper);
}
@Override
public CompletionStage<U> getBody() {
return upstream.getBody().thenApply(mapper);
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
upstream.onSubscribe(subscription);
}
@Override
public void onNext(List<ByteBuffer> item) {
upstream.onNext(item);
}
@Override
public void onError(Throwable throwable) {
upstream.onError(throwable);
}
@Override
public void onComplete() {
upstream.onComplete();
}
}
// A BodySubscriber that returns a Publisher<List<ByteBuffer>>
static class PublishingBodySubscriber
implements BodySubscriber<Flow.Publisher<List<ByteBuffer>>> {
private final MinimalFuture<Flow.Subscription>
subscriptionCF = new MinimalFuture<>();
private final MinimalFuture<SubscriberRef>
subscribedCF = new MinimalFuture<>();
private AtomicReference<SubscriberRef>
subscriberRef = new AtomicReference<>();
private final CompletionStage<Flow.Publisher<List<ByteBuffer>>> body =
subscriptionCF.thenCompose(
(s) -> MinimalFuture.completedFuture(this::subscribe));
// We use the completionCF to ensure that only one of
// onError or onComplete is ever called.
private final MinimalFuture<Void> completionCF;
private PublishingBodySubscriber() {
completionCF = new MinimalFuture<>();
completionCF.whenComplete(
(r,t) -> subscribedCF.thenAccept( s -> complete(s, t)));
}
// An object that holds a reference to a Flow.Subscriber.
// The reference is cleared when the subscriber is completed - either
// normally or exceptionally, or when the subscription is cancelled.
static final class SubscriberRef {
volatile Flow.Subscriber<? super List<ByteBuffer>> ref;
SubscriberRef(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
ref = subscriber;
}
Flow.Subscriber<? super List<ByteBuffer>> get() {
return ref;
}
Flow.Subscriber<? super List<ByteBuffer>> clear() {
Flow.Subscriber<? super List<ByteBuffer>> res = ref;
ref = null;
return res;
}
}
// A subscription that wraps an upstream subscription and
// holds a reference to a subscriber. The subscriber reference
// is cleared when the subscription is cancelled
final static class SubscriptionRef implements Flow.Subscription {
final Flow.Subscription subscription;
final SubscriberRef subscriberRef;
SubscriptionRef(Flow.Subscription subscription,
SubscriberRef subscriberRef) {
this.subscription = subscription;
this.subscriberRef = subscriberRef;
}
@Override
public void request(long n) {
if (subscriberRef.get() != null) {
subscription.request(n);
}
}
@Override
public void cancel() {
subscription.cancel();
subscriberRef.clear();
}
void subscribe() {
Subscriber<?> subscriber = subscriberRef.get();
if (subscriber != null) {
subscriber.onSubscribe(this);
}
}
@Override
public String toString() {
return "SubscriptionRef/"
+ subscription.getClass().getName()
+ "@"
+ System.identityHashCode(subscription);
}
}
// This is a callback for the subscribedCF.
// Do not call directly!
private void complete(SubscriberRef ref, Throwable t) {
assert ref != null;
Subscriber<?> s = ref.clear();
// maybe null if subscription was cancelled
if (s == null) return;
if (t == null) {
try {
s.onComplete();
} catch (Throwable x) {
s.onError(x);
}
} else {
s.onError(t);
}
}
private void signalError(Throwable err) {
if (err == null) {
err = new NullPointerException("null throwable");
}
completionCF.completeExceptionally(err);
}
private void signalComplete() {
completionCF.complete(null);
}
private void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
Objects.requireNonNull(subscriber, "subscriber must not be null");
SubscriberRef ref = new SubscriberRef(subscriber);
if (subscriberRef.compareAndSet(null, ref)) {
subscriptionCF.thenAccept((s) -> {
SubscriptionRef subscription = new SubscriptionRef(s,ref);
try {
subscription.subscribe();
subscribedCF.complete(ref);
} catch (Throwable t) {
if (Log.errors()) {
Log.logError("Failed to call onSubscribe: " +
"cancelling subscription: " + t);
Log.logError(t);
}
subscription.cancel();
}
});
} else {
subscriber.onSubscribe(new Flow.Subscription() {
@Override public void request(long n) { }
@Override public void cancel() { }
});
subscriber.onError(new IllegalStateException(
"This publisher has already one subscriber"));
}
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
subscriptionCF.complete(subscription);
}
@Override
public void onNext(List<ByteBuffer> item) {
try {
// cannot be called before onSubscribe()
assert subscriptionCF.isDone();
SubscriberRef ref = subscriberRef.get();
// cannot be called before subscriber calls request(1)
assert ref != null;
Flow.Subscriber<? super List<ByteBuffer>>
subscriber = ref.get();
if (subscriber != null) {
// may be null if subscription was cancelled.
subscriber.onNext(item);
}
} catch (Throwable err) {
signalError(err);
subscriptionCF.thenAccept(s -> s.cancel());
}
}
@Override
public void onError(Throwable throwable) {
// cannot be called before onSubscribe();
assert suppress(subscriptionCF.isDone(),
"onError called before onSubscribe",
throwable);
// onError can be called before request(1), and therefore can
// be called before subscriberRef is set.
signalError(throwable);
}
@Override
public void onComplete() {
// cannot be called before onSubscribe()
if (!subscriptionCF.isDone()) {
signalError(new InternalError(
"onComplete called before onSubscribed"));
} else {
// onComplete can be called before request(1),
// and therefore can be called before subscriberRef
// is set.
signalComplete();
}
}
@Override
public CompletionStage<Flow.Publisher<List<ByteBuffer>>> getBody() {
return body;
}
private boolean suppress(boolean condition,
String assertion,
Throwable carrier) {
if (!condition) {
if (carrier != null) {
carrier.addSuppressed(new AssertionError(assertion));
} else if (Log.errors()) {
Log.logError(new AssertionError(assertion));
}
}
return true;
}
}
public static BodySubscriber<Flow.Publisher<List<ByteBuffer>>>
createPublisher() {
return new PublishingBodySubscriber();
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2015, 2018, 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.time.Duration;
import java.time.Instant;
import java.util.concurrent.atomic.AtomicLong;
/**
* Timeout event notified by selector thread. Executes the given handler if
* the timer not canceled first.
*
* Register with {@link HttpClientImpl#registerTimer(TimeoutEvent)}.
*
* Cancel with {@link HttpClientImpl#cancelTimer(TimeoutEvent)}.
*/
abstract class TimeoutEvent implements Comparable<TimeoutEvent> {
private static final AtomicLong COUNTER = new AtomicLong();
// we use id in compareTo to make compareTo consistent with equals
// see TimeoutEvent::compareTo below;
private final long id = COUNTER.incrementAndGet();
private final Instant deadline;
TimeoutEvent(Duration duration) {
deadline = Instant.now().plus(duration);
}
public abstract void handle();
public Instant deadline() {
return deadline;
}
@Override
public int compareTo(TimeoutEvent other) {
if (other == this) return 0;
// if two events have the same deadline, but are not equals, then the
// smaller is the one that was created before (has the smaller id).
// This is arbitrary and we don't really care which is smaller or
// greater, but we need a total order, so two events with the
// same deadline cannot compare == 0 if they are not equals.
final int compareDeadline = this.deadline.compareTo(other.deadline);
if (compareDeadline == 0 && !this.equals(other)) {
long diff = this.id - other.id; // should take care of wrap around
if (diff < 0) return -1;
else if (diff > 0) return 1;
else assert false : "Different events with same id and deadline";
}
return compareDeadline;
}
@Override
public String toString() {
return "TimeoutEvent[id=" + id + ", deadline=" + deadline + "]";
}
}

View file

@ -0,0 +1,330 @@
/*
* Copyright (c) 2016, 2018, 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.lang.System.Logger.Level;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.Utils;
/**
* A Send Window Flow-Controller that is used to control outgoing Connection
* and Stream flows, per HTTP/2 connection.
*
* A Http2Connection has its own unique single instance of a WindowController
* that it shares with its Streams. Each stream must acquire the appropriate
* amount of Send Window from the controller before sending data.
*
* WINDOW_UPDATE frames, both connection and stream specific, must notify the
* controller of their increments. SETTINGS frame's INITIAL_WINDOW_SIZE must
* notify the controller so that it can adjust the active stream's window size.
*/
final class WindowController {
static final Logger debug =
Utils.getDebugLogger("WindowController"::toString, Utils.DEBUG);
/**
* Default initial connection Flow-Control Send Window size, as per HTTP/2.
*/
private static final int DEFAULT_INITIAL_WINDOW_SIZE = 64 * 1024 - 1;
/** The connection Send Window size. */
private int connectionWindowSize;
/** A Map of the active streams, where the key is the stream id, and the
* value is the stream's Send Window size, which may be negative. */
private final Map<Integer,Integer> streams = new HashMap<>();
/** A Map of streams awaiting Send Window. The key is the stream id. The
* value is a pair of the Stream ( representing the key's stream id ) and
* the requested amount of send Window. */
private final Map<Integer, Map.Entry<Stream<?>, Integer>> pending
= new LinkedHashMap<>();
private final ReentrantLock controllerLock = new ReentrantLock();
/** A Controller with the default initial window size. */
WindowController() {
connectionWindowSize = DEFAULT_INITIAL_WINDOW_SIZE;
}
// /** A Controller with the given initial window size. */
// WindowController(int initialConnectionWindowSize) {
// connectionWindowSize = initialConnectionWindowSize;
// }
/** Registers the given stream with this controller. */
void registerStream(int streamid, int initialStreamWindowSize) {
controllerLock.lock();
try {
Integer old = streams.put(streamid, initialStreamWindowSize);
if (old != null)
throw new InternalError("Unexpected entry ["
+ old + "] for streamid: " + streamid);
} finally {
controllerLock.unlock();
}
}
/** Removes/De-registers the given stream with this controller. */
void removeStream(int streamid) {
controllerLock.lock();
try {
Integer old = streams.remove(streamid);
// Odd stream numbers (client streams) should have been registered.
// Even stream numbers (server streams - aka Push Streams) should
// not be registered
final boolean isClientStream = (streamid & 0x1) == 1;
if (old == null && isClientStream) {
throw new InternalError("Expected entry for streamid: " + streamid);
} else if (old != null && !isClientStream) {
throw new InternalError("Unexpected entry for streamid: " + streamid);
}
} finally {
controllerLock.unlock();
}
}
/**
* Attempts to acquire the requested amount of Send Window for the given
* stream.
*
* The actual amount of Send Window available may differ from the requested
* amount. The actual amount, returned by this method, is the minimum of,
* 1) the requested amount, 2) the stream's Send Window, and 3) the
* connection's Send Window.
*
* A negative or zero value is returned if there's no window available.
* When the result is negative or zero, this method arranges for the
* given stream's {@link Stream#signalWindowUpdate()} method to be invoke at
* a later time when the connection and/or stream window's have been
* increased. The {@code tryAcquire} method should then be invoked again to
* attempt to acquire the available window.
*/
int tryAcquire(int requestAmount, int streamid, Stream<?> stream) {
controllerLock.lock();
try {
Integer streamSize = streams.get(streamid);
if (streamSize == null)
throw new InternalError("Expected entry for streamid: "
+ streamid);
int x = Math.min(requestAmount,
Math.min(streamSize, connectionWindowSize));
if (x <= 0) { // stream window size may be negative
if (debug.on())
debug.log("Stream %d requesting %d but only %d available (stream: %d, connection: %d)",
streamid, requestAmount, Math.min(streamSize, connectionWindowSize),
streamSize, connectionWindowSize);
// If there's not enough window size available, put the
// caller in a pending list.
pending.put(streamid, Map.entry(stream, requestAmount));
return x;
}
// Remove the caller from the pending list ( if was waiting ).
pending.remove(streamid);
// Update window sizes and return the allocated amount to the caller.
streamSize -= x;
streams.put(streamid, streamSize);
connectionWindowSize -= x;
if (debug.on())
debug.log("Stream %d amount allocated %d, now %d available (stream: %d, connection: %d)",
streamid, x, Math.min(streamSize, connectionWindowSize),
streamSize, connectionWindowSize);
return x;
} finally {
controllerLock.unlock();
}
}
/**
* Increases the Send Window size for the connection.
*
* A number of awaiting requesters, from unfulfilled tryAcquire requests,
* may have their stream's {@link Stream#signalWindowUpdate()} method
* scheduled to run ( i.e. awake awaiters ).
*
* @return false if, and only if, the addition of the given amount would
* cause the Send Window to exceed 2^31-1 (overflow), otherwise true
*/
boolean increaseConnectionWindow(int amount) {
List<Stream<?>> candidates = null;
controllerLock.lock();
try {
int size = connectionWindowSize;
size += amount;
if (size < 0)
return false;
connectionWindowSize = size;
if (debug.on())
debug.log("Connection window size is now %d (amount added %d)",
size, amount);
// Notify waiting streams, until the new increased window size is
// effectively exhausted.
Iterator<Map.Entry<Integer,Map.Entry<Stream<?>,Integer>>> iter =
pending.entrySet().iterator();
while (iter.hasNext() && size > 0) {
Map.Entry<Integer,Map.Entry<Stream<?>,Integer>> item = iter.next();
Integer streamSize = streams.get(item.getKey());
if (streamSize == null) {
iter.remove();
} else {
Map.Entry<Stream<?>,Integer> e = item.getValue();
int requestedAmount = e.getValue();
// only wakes up the pending streams for which there is
// at least 1 byte of space in both windows
int minAmount = 1;
if (size >= minAmount && streamSize >= minAmount) {
size -= Math.min(streamSize, requestedAmount);
iter.remove();
if (candidates == null)
candidates = new ArrayList<>();
candidates.add(e.getKey());
}
}
}
} finally {
controllerLock.unlock();
}
if (candidates != null) {
candidates.forEach(Stream::signalWindowUpdate);
}
return true;
}
/**
* Increases the Send Window size for the given stream.
*
* If the given stream is awaiting window size, from an unfulfilled
* tryAcquire request, it will have its stream's {@link
* Stream#signalWindowUpdate()} method scheduled to run ( i.e. awoken ).
*
* @return false if, and only if, the addition of the given amount would
* cause the Send Window to exceed 2^31-1 (overflow), otherwise true
*/
boolean increaseStreamWindow(int amount, int streamid) {
Stream<?> s = null;
controllerLock.lock();
try {
Integer size = streams.get(streamid);
if (size == null) {
// The stream may have been cancelled.
if (debug.on())
debug.log("WARNING: No entry found for streamid: %s. May be cancelled?",
streamid);
} else {
size += amount;
if (size < 0)
return false;
streams.put(streamid, size);
if (debug.on())
debug.log("Stream %s window size is now %s (amount added %d)",
streamid, size, amount);
Map.Entry<Stream<?>, Integer> p = pending.get(streamid);
if (p != null) {
int minAmount = 1;
// only wakes up the pending stream if there is at least
// 1 byte of space in both windows
if (size >= minAmount
&& connectionWindowSize >= minAmount) {
pending.remove(streamid);
s = p.getKey();
}
}
}
} finally {
controllerLock.unlock();
}
if (s != null)
s.signalWindowUpdate();
return true;
}
/**
* Adjusts, either increases or decreases, the active streams registered
* with this controller. May result in a stream's Send Window size becoming
* negative.
*/
void adjustActiveStreams(int adjustAmount) {
assert adjustAmount != 0;
controllerLock.lock();
try {
for (Map.Entry<Integer,Integer> entry : streams.entrySet()) {
int streamid = entry.getKey();
// the API only supports sending on Streams initialed by
// the client, i.e. odd stream numbers
if (streamid != 0 && (streamid % 2) != 0) {
Integer size = entry.getValue();
size += adjustAmount;
streams.put(streamid, size);
if (debug.on())
debug.log("Stream %s window size is now %s (adjusting amount %d)",
streamid, size, adjustAmount);
}
}
} finally {
controllerLock.unlock();
}
}
/** Returns the Send Window size for the connection. */
int connectionWindowSize() {
controllerLock.lock();
try {
return connectionWindowSize;
} finally {
controllerLock.unlock();
}
}
// /** Returns the Send Window size for the given stream. */
// int streamWindowSize(int streamid) {
// controllerLock.lock();
// try {
// Integer size = streams.get(streamid);
// if (size == null)
// throw new InternalError("Expected entry for streamid: " + streamid);
// return size;
// } finally {
// controllerLock.unlock();
// }
// }
}

View file

@ -0,0 +1,90 @@
/*
* Copyright (c) 2015, 2018, 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 jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.frame.SettingsFrame;
import jdk.internal.net.http.frame.WindowUpdateFrame;
import jdk.internal.net.http.common.Utils;
import java.util.concurrent.atomic.AtomicInteger;
abstract class WindowUpdateSender {
final Logger debug =
Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
final int limit;
final Http2Connection connection;
final AtomicInteger received = new AtomicInteger(0);
WindowUpdateSender(Http2Connection connection) {
this(connection, connection.clientSettings.getParameter(SettingsFrame.INITIAL_WINDOW_SIZE));
}
WindowUpdateSender(Http2Connection connection, int initWindowSize) {
this(connection, connection.getMaxReceiveFrameSize(), initWindowSize);
}
WindowUpdateSender(Http2Connection connection, int maxFrameSize, int initWindowSize) {
this.connection = connection;
int v0 = Math.max(0, initWindowSize - maxFrameSize);
int v1 = (initWindowSize + (maxFrameSize - 1)) / maxFrameSize;
v1 = v1 * maxFrameSize / 2;
// send WindowUpdate heuristic:
// - we got data near half of window size
// or
// - remaining window size reached max frame size.
limit = Math.min(v0, v1);
if (debug.on())
debug.log("maxFrameSize=%d, initWindowSize=%d, limit=%d",
maxFrameSize, initWindowSize, limit);
}
abstract int getStreamId();
void update(int delta) {
if (debug.on()) debug.log("update: %d", delta);
if (received.addAndGet(delta) > limit) {
synchronized (this) {
int tosend = received.get();
if( tosend > limit) {
received.getAndAdd(-tosend);
sendWindowUpdate(tosend);
}
}
}
}
void sendWindowUpdate(int delta) {
if (debug.on()) debug.log("sending window update: %d", delta);
connection.sendUnorderedFrame(new WindowUpdateFrame(getStreamId(), delta));
}
String dbgString() {
return "WindowUpdateSender(stream: " + getStreamId() + ")";
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2015, 2018, 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.common;
import java.nio.ByteBuffer;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* The class provides reuse of ByteBuffers.
* It is supposed that all requested buffers have the same size for a long period of time.
* That is why there is no any logic splitting buffers into different buckets (by size). It's unnecessary.
*
* At the same moment it is allowed to change requested buffers size (all smaller buffers will be discarded).
* It may be needed for example, if after rehandshaking netPacketBufferSize was changed.
*/
public class ByteBufferPool {
private final java.util.Queue<ByteBuffer> pool = new ConcurrentLinkedQueue<>();
public ByteBufferPool() {
}
public ByteBufferReference get(int size) {
ByteBuffer buffer;
while ((buffer = pool.poll()) != null) {
if (buffer.capacity() >= size) {
return ByteBufferReference.of(buffer, this);
}
}
return ByteBufferReference.of(ByteBuffer.allocate(size), this);
}
public void release(ByteBuffer buffer) {
buffer.clear();
pool.offer(buffer);
}
}

View file

@ -0,0 +1,88 @@
/*
* Copyright (c) 2015, 2018, 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.common;
import java.nio.ByteBuffer;
import java.util.Objects;
import java.util.function.Supplier;
public class ByteBufferReference implements Supplier<ByteBuffer> {
private ByteBuffer buffer;
private final ByteBufferPool pool;
public static ByteBufferReference of(ByteBuffer buffer) {
return of(buffer, null);
}
public static ByteBufferReference of(ByteBuffer buffer, ByteBufferPool pool) {
Objects.requireNonNull(buffer);
return new ByteBufferReference(buffer, pool);
}
public static ByteBuffer[] toBuffers(ByteBufferReference... refs) {
ByteBuffer[] bufs = new ByteBuffer[refs.length];
for (int i = 0; i < refs.length; i++) {
bufs[i] = refs[i].get();
}
return bufs;
}
public static ByteBufferReference[] toReferences(ByteBuffer... buffers) {
ByteBufferReference[] refs = new ByteBufferReference[buffers.length];
for (int i = 0; i < buffers.length; i++) {
refs[i] = of(buffers[i]);
}
return refs;
}
public static void clear(ByteBufferReference[] refs) {
for(ByteBufferReference ref : refs) {
ref.clear();
}
}
private ByteBufferReference(ByteBuffer buffer, ByteBufferPool pool) {
this.buffer = buffer;
this.pool = pool;
}
@Override
public ByteBuffer get() {
ByteBuffer buf = this.buffer;
assert buf!=null : "getting ByteBuffer after clearance";
return buf;
}
public void clear() {
ByteBuffer buf = this.buffer;
assert buf!=null : "double ByteBuffer clearance";
this.buffer = null;
if (pool != null) {
pool.release(buf);
}
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2017, 2018, 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.common;
import java.io.IOException;
/**
* Signals that an end of file or end of stream has been reached
* unexpectedly before any protocol specific data has been received.
*/
public final class ConnectionExpiredException extends IOException {
private static final long serialVersionUID = 0;
/**
* Constructs a {@code ConnectionExpiredException} with the specified detail
* message and cause.
*
* @param s the detail message
* @param cause the throwable cause
*/
public ConnectionExpiredException(String s, Throwable cause) {
super(s, cause);
}
}

View file

@ -0,0 +1,268 @@
/*
* Copyright (c) 2015, 2018, 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.common;
import java.io.PrintStream;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.function.Supplier;
/**
* A {@code System.Logger} that forwards all messages to an underlying
* {@code System.Logger}, after adding some decoration. The logger also has the
* ability to additionally send the logged messages to System.err or System.out,
* whether the underlying logger is activated or not. In addition instance of
* {@code DebugLogger} support both {@link String#format(String, Object...)} and
* {@link java.text.MessageFormat#format(String, Object...)} formatting.
* String-like formatting is enabled by the presence of "%s" or "%d" in the
* format string. MessageFormat-like formatting is enabled by the presence of
* "{0" or "{1".
*
* <p> See {@link Utils#getDebugLogger(Supplier, boolean)} and
* {@link Utils#getHpackLogger(Supplier, boolean)}.
*/
final class DebugLogger implements Logger {
// deliberately not in the same subtree than standard loggers.
final static String HTTP_NAME = "jdk.internal.httpclient.debug";
final static String WS_NAME = "jdk.internal.httpclient.websocket.debug";
final static String HPACK_NAME = "jdk.internal.httpclient.hpack.debug";
final static System.Logger HTTP = System.getLogger(HTTP_NAME);
final static System.Logger WS = System.getLogger(WS_NAME);
final static System.Logger HPACK = System.getLogger(HPACK_NAME);
final static long START_NANOS = System.nanoTime();
private final Supplier<String> dbgTag;
private final Level errLevel;
private final Level outLevel;
private final System.Logger logger;
private final boolean debugOn;
private final boolean traceOn;
/**
* Create a logger for debug traces.The logger should only be used
* with levels whose severity is {@code <= DEBUG}.
*
* By default, this logger will forward all messages logged to the supplied
* {@code logger}.
* But in addition, if the message severity level is {@code >=} to
* the provided {@code errLevel} it will print the messages on System.err,
* and if the message severity level is {@code >=} to
* the provided {@code outLevel} it will also print the messages on System.out.
* <p>
* The logger will add some decoration to the printed message, in the form of
* {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
*
* @apiNote To obtain a logger that will always print things on stderr in
* addition to forwarding to the internal logger, use
* {@code new DebugLogger(logger, this::dbgTag, Level.OFF, Level.ALL);}.
* To obtain a logger that will only forward to the internal logger,
* use {@code new DebugLogger(logger, this::dbgTag, Level.OFF, Level.OFF);}.
*
* @param logger The internal logger to which messages will be forwarded.
* This should be either {@link #HPACK} or {@link #HTTP};
*
* @param dbgTag A lambda that returns a string that identifies the caller
* (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
* @param outLevel The level above which messages will be also printed on
* System.out (in addition to being forwarded to the internal logger).
* @param errLevel The level above which messages will be also printed on
* System.err (in addition to being forwarded to the internal logger).
*
* @return A logger for HTTP internal debug traces
*/
private DebugLogger(System.Logger logger,
Supplier<String> dbgTag,
Level outLevel,
Level errLevel) {
this.dbgTag = dbgTag;
this.errLevel = errLevel;
this.outLevel = outLevel;
this.logger = Objects.requireNonNull(logger);
// support only static configuration.
this.debugOn = isEnabled(Level.DEBUG);
this.traceOn = isEnabled(Level.TRACE);
}
@Override
public String getName() {
return logger.getName();
}
private boolean isEnabled(Level level) {
if (level == Level.OFF) return false;
int severity = level.getSeverity();
return severity >= errLevel.getSeverity()
|| severity >= outLevel.getSeverity()
|| logger.isLoggable(level);
}
@Override
public final boolean on() {
return debugOn;
}
@Override
public boolean isLoggable(Level level) {
// fast path, we assume these guys never change.
// support only static configuration.
if (level == Level.DEBUG) return debugOn;
if (level == Level.TRACE) return traceOn;
return isEnabled(level);
}
@Override
public void log(Level level, ResourceBundle unused,
String format, Object... params) {
// fast path, we assume these guys never change.
// support only static configuration.
if (level == Level.DEBUG && !debugOn) return;
if (level == Level.TRACE && !traceOn) return;
int severity = level.getSeverity();
if (errLevel != Level.OFF
&& errLevel.getSeverity() <= severity) {
print(System.err, level, format, params, null);
}
if (outLevel != Level.OFF
&& outLevel.getSeverity() <= severity) {
print(System.out, level, format, params, null);
}
if (logger.isLoggable(level)) {
logger.log(level, unused,
getFormat(new StringBuilder(), format, params).toString(),
params);
}
}
@Override
public void log(Level level, ResourceBundle unused, String msg,
Throwable thrown) {
// fast path, we assume these guys never change.
if (level == Level.DEBUG && !debugOn) return;
if (level == Level.TRACE && !traceOn) return;
if (errLevel != Level.OFF
&& errLevel.getSeverity() <= level.getSeverity()) {
print(System.err, level, msg, null, thrown);
}
if (outLevel != Level.OFF
&& outLevel.getSeverity() <= level.getSeverity()) {
print(System.out, level, msg, null, thrown);
}
if (logger.isLoggable(level)) {
logger.log(level, unused,
getFormat(new StringBuilder(), msg, null).toString(),
thrown);
}
}
private void print(PrintStream out, Level level, String msg,
Object[] params, Throwable t) {
StringBuilder sb = new StringBuilder();
sb.append(level.name()).append(':').append(' ');
sb = format(sb, msg, params);
if (t != null) sb.append(' ').append(t.toString());
out.println(sb.toString());
if (t != null) {
t.printStackTrace(out);
}
}
private StringBuilder decorate(StringBuilder sb, String msg) {
String tag = dbgTag == null ? null : dbgTag.get();
String res = msg == null ? "" : msg;
long elapsed = System.nanoTime() - START_NANOS;
long millis = elapsed / 1000_000;
long secs = millis / 1000;
sb.append('[').append(Thread.currentThread().getName()).append(']')
.append(' ').append('[');
if (secs > 0) {
sb.append(secs).append('s');
}
millis = millis % 1000;
if (millis > 0) {
if (secs > 0) sb.append(' ');
sb.append(millis).append("ms");
}
sb.append(']').append(' ');
if (tag != null) {
sb.append(tag).append(' ');
}
sb.append(res);
return sb;
}
private StringBuilder getFormat(StringBuilder sb, String format, Object[] params) {
if (format == null || params == null || params.length == 0) {
return decorate(sb, format);
} else if (format.contains("{0}") || format.contains("{1}")) {
return decorate(sb, format);
} else if (format.contains("%s") || format.contains("%d")) {
try {
return decorate(sb, String.format(format, params));
} catch (Throwable t) {
return decorate(sb, format);
}
} else {
return decorate(sb, format);
}
}
private StringBuilder format(StringBuilder sb, String format, Object[] params) {
if (format == null || params == null || params.length == 0) {
return decorate(sb, format);
} else if (format.contains("{0}") || format.contains("{1}")) {
return decorate(sb, java.text.MessageFormat.format(format, params));
} else if (format.contains("%s") || format.contains("%d")) {
try {
return decorate(sb, String.format(format, params));
} catch (Throwable t) {
return decorate(sb, format);
}
} else {
return decorate(sb, format);
}
}
public static DebugLogger createHttpLogger(Supplier<String> dbgTag,
Level outLevel,
Level errLevel) {
return new DebugLogger(HTTP, dbgTag, outLevel, errLevel);
}
public static DebugLogger createWebSocketLogger(Supplier<String> dbgTag,
Level outLevel,
Level errLevel) {
return new DebugLogger(WS, dbgTag, outLevel, errLevel);
}
public static DebugLogger createHpackLogger(Supplier<String> dbgTag,
Level outLevel,
Level errLevel) {
return new DebugLogger(HPACK, dbgTag, outLevel, errLevel);
}
}

View file

@ -0,0 +1,124 @@
/*
* Copyright (c) 2017, 2018, 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.common;
import java.util.concurrent.atomic.AtomicLong;
/**
* Encapsulates operations with demand (Reactive Streams).
*
* <p> Demand is the aggregated number of elements requested by a Subscriber
* which is yet to be delivered (fulfilled) by the Publisher.
*/
public final class Demand {
private final AtomicLong val = new AtomicLong();
/**
* Increases this demand by the specified positive value.
*
* @param n
* increment {@code > 0}
*
* @return {@code true} iff prior to this operation this demand was fulfilled
*/
public boolean increase(long n) {
if (n <= 0) {
throw new IllegalArgumentException(String.valueOf(n));
}
long prev = val.getAndAccumulate(n, (p, i) -> p + i < 0 ? Long.MAX_VALUE : p + i);
return prev == 0;
}
/**
* Increases this demand by 1 but only if it is fulfilled.
* @return true if the demand was increased, false otherwise.
*/
public boolean increaseIfFulfilled() {
return val.compareAndSet(0, 1);
}
/**
* Tries to decrease this demand by the specified positive value.
*
* <p> The actual value this demand has been decreased by might be less than
* {@code n}, including {@code 0} (no decrease at all).
*
* @param n
* decrement {@code > 0}
*
* @return a value {@code m} ({@code 0 <= m <= n}) this demand has been
* actually decreased by
*/
public long decreaseAndGet(long n) {
if (n <= 0) {
throw new IllegalArgumentException(String.valueOf(n));
}
long p, d;
do {
d = val.get();
p = Math.min(d, n);
} while (!val.compareAndSet(d, d - p));
return p;
}
/**
* Tries to decrease this demand by {@code 1}.
*
* @return {@code true} iff this demand has been decreased by {@code 1}
*/
public boolean tryDecrement() {
return decreaseAndGet(1) == 1;
}
/**
* @return {@code true} iff there is no unfulfilled demand
*/
public boolean isFulfilled() {
return val.get() == 0;
}
/**
* Resets this object to its initial state.
*/
public void reset() {
val.set(0);
}
/**
* Returns the current value of this demand.
*
* @return the current value of this demand
*/
public long get() {
return val.get();
}
@Override
public String toString() {
return String.valueOf(val.get());
}
}

View file

@ -0,0 +1,185 @@
/*
* Copyright (c) 2017, 2018, 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.common;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.Flow;
/**
* FlowTube is an I/O abstraction that allows reading from and writing to a
* destination asynchronously.
* This is not a {@link Flow.Processor
* Flow.Processor&lt;List&lt;ByteBuffer&gt;, List&lt;ByteBuffer&gt;&gt;},
* but rather models a publisher source and a subscriber sink in a bidirectional flow.
* <p>
* The {@code connectFlows} method should be called to connect the bidirectional
* flow. A FlowTube supports handing over the same read subscription to different
* sequential read subscribers over time. When {@code connectFlows(writePublisher,
* readSubscriber} is called, the FlowTube will call {@code dropSubscription} on
* its former readSubscriber, and {@code onSubscribe} on its new readSubscriber.
*/
public interface FlowTube extends
Flow.Publisher<List<ByteBuffer>>,
Flow.Subscriber<List<ByteBuffer>> {
/**
* A subscriber for reading from the bidirectional flow.
* A TubeSubscriber is a {@code Flow.Subscriber} that can be canceled
* by calling {@code dropSubscription()}.
* Once {@code dropSubscription()} is called, the {@code TubeSubscriber}
* should stop calling any method on its subscription.
*/
static interface TubeSubscriber extends Flow.Subscriber<List<ByteBuffer>> {
/**
* Called when the flow is connected again, and the subscription
* is handed over to a new subscriber.
* Once {@code dropSubscription()} is called, the {@code TubeSubscriber}
* should stop calling any method on its subscription.
*/
default void dropSubscription() { }
}
/**
* A publisher for writing to the bidirectional flow.
*/
static interface TubePublisher extends Flow.Publisher<List<ByteBuffer>> {
}
/**
* Connects the bidirectional flows to a write {@code Publisher} and a
* read {@code Subscriber}. This method can be called sequentially
* several times to switch existing publishers and subscribers to a new
* pair of write subscriber and read publisher.
* @param writePublisher A new publisher for writing to the bidirectional flow.
* @param readSubscriber A new subscriber for reading from the bidirectional
* flow.
*/
default void connectFlows(TubePublisher writePublisher,
TubeSubscriber readSubscriber) {
this.subscribe(readSubscriber);
writePublisher.subscribe(this);
}
/**
* Returns true if this flow was completed, either exceptionally
* or normally (EOF reached).
* @return true if the flow is finished
*/
boolean isFinished();
/**
* Returns {@code s} if {@code s} is a {@code TubeSubscriber}, otherwise
* wraps it in a {@code TubeSubscriber}.
* Using the wrapper is only appropriate in the case where
* {@code dropSubscription} doesn't need to be implemented, and the
* {@code TubeSubscriber} is subscribed only once.
*
* @param s a subscriber for reading.
* @return a {@code TubeSubscriber}: either {@code s} if {@code s} is a
* {@code TubeSubscriber}, otherwise a {@code TubeSubscriber}
* wrapper that delegates to {@code s}
*/
static TubeSubscriber asTubeSubscriber(Flow.Subscriber<? super List<ByteBuffer>> s) {
if (s instanceof TubeSubscriber) {
return (TubeSubscriber) s;
}
return new AbstractTubeSubscriber.TubeSubscriberWrapper(s);
}
/**
* Returns {@code s} if {@code s} is a {@code TubePublisher}, otherwise
* wraps it in a {@code TubePublisher}.
*
* @param p a publisher for writing.
* @return a {@code TubePublisher}: either {@code s} if {@code s} is a
* {@code TubePublisher}, otherwise a {@code TubePublisher}
* wrapper that delegates to {@code s}
*/
static TubePublisher asTubePublisher(Flow.Publisher<List<ByteBuffer>> p) {
if (p instanceof TubePublisher) {
return (TubePublisher) p;
}
return new AbstractTubePublisher.TubePublisherWrapper(p);
}
/**
* Convenience abstract class for {@code TubePublisher} implementations.
* It is not required that a {@code TubePublisher} implementation extends
* this class.
*/
static abstract class AbstractTubePublisher implements TubePublisher {
static final class TubePublisherWrapper extends AbstractTubePublisher {
final Flow.Publisher<List<ByteBuffer>> delegate;
public TubePublisherWrapper(Flow.Publisher<List<ByteBuffer>> delegate) {
this.delegate = delegate;
}
@Override
public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
delegate.subscribe(subscriber);
}
}
}
/**
* Convenience abstract class for {@code TubeSubscriber} implementations.
* It is not required that a {@code TubeSubscriber} implementation extends
* this class.
*/
static abstract class AbstractTubeSubscriber implements TubeSubscriber {
static final class TubeSubscriberWrapper extends AbstractTubeSubscriber {
final Flow.Subscriber<? super List<ByteBuffer>> delegate;
TubeSubscriberWrapper(Flow.Subscriber<? super List<ByteBuffer>> delegate) {
this.delegate = delegate;
}
@Override
public void dropSubscription() {}
@Override
public void onSubscribe(Flow.Subscription subscription) {
delegate.onSubscribe(subscription);
}
@Override
public void onNext(List<ByteBuffer> item) {
delegate.onNext(item);
}
@Override
public void onError(Throwable throwable) {
delegate.onError(throwable);
}
@Override
public void onComplete() {
delegate.onComplete();
}
}
}
}

View file

@ -0,0 +1,89 @@
/*
* Copyright (c) 2015, 2018, 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.common;
import java.net.http.HttpHeaders;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* Implementation of HttpHeaders.
*
* The public HttpHeaders API provides a read-only view, while the
* non-HttpHeaders members allow for implementation specific mutation, e.g.
* during creation, etc.
*/
public class HttpHeadersImpl extends HttpHeaders {
private final TreeMap<String, List<String>> headers;
public HttpHeadersImpl() {
headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
}
@Override
public Map<String, List<String>> map() {
return Collections.unmodifiableMap(headersMap());
}
// non-HttpHeaders private mutators
public HttpHeadersImpl deepCopy() {
HttpHeadersImpl h1 = newDeepCopy();
for (Map.Entry<String, List<String>> entry : headersMap().entrySet()) {
List<String> valuesCopy = new ArrayList<>(entry.getValue());
h1.headersMap().put(entry.getKey(), valuesCopy);
}
return h1;
}
public void addHeader(String name, String value) {
headersMap().computeIfAbsent(name, k -> new ArrayList<>(1))
.add(value);
}
public void setHeader(String name, String value) {
// headers typically have one value
List<String> values = new ArrayList<>(1);
values.add(value);
headersMap().put(name, values);
}
public void clear() {
headersMap().clear();
}
protected HttpHeadersImpl newDeepCopy() {
return new HttpHeadersImpl();
}
protected Map<String, List<String>> headersMap() {
return headers;
}
}

View file

@ -0,0 +1,149 @@
/*
* Copyright (c) 2018, 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.common;
import java.security.Principal;
import java.util.List;
import javax.net.ssl.SSLSession;
import javax.net.ssl.ExtendedSSLSession;
import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SNIServerName;
/**
* All mutating methods throw UnsupportedOperationException
*/
public class ImmutableExtendedSSLSession extends ExtendedSSLSession {
private final ExtendedSSLSession delegate;
ImmutableExtendedSSLSession(ExtendedSSLSession session) {
this.delegate = session;
}
public byte[] getId() {
return delegate.getId();
}
public SSLSessionContext getSessionContext() {
return delegate.getSessionContext();
}
public long getCreationTime() {
return delegate.getCreationTime();
}
public long getLastAccessedTime() {
return delegate.getLastAccessedTime();
}
public void invalidate() {
throw new UnsupportedOperationException("session is not mutable");
}
public boolean isValid() {
return delegate.isValid();
}
public void putValue(String name, Object value) {
throw new UnsupportedOperationException("session is not mutable");
}
public Object getValue(String name) {
return delegate.getValue(name);
}
public void removeValue(String name) {
throw new UnsupportedOperationException("session is not mutable");
}
public String [] getValueNames() {
return delegate.getValueNames();
}
public java.security.cert.Certificate [] getPeerCertificates()
throws SSLPeerUnverifiedException {
return delegate.getPeerCertificates();
}
public java.security.cert.Certificate [] getLocalCertificates() {
return delegate.getLocalCertificates();
}
@Deprecated
public javax.security.cert.X509Certificate [] getPeerCertificateChain()
throws SSLPeerUnverifiedException {
return delegate.getPeerCertificateChain();
}
public Principal getPeerPrincipal()
throws SSLPeerUnverifiedException {
return delegate.getPeerPrincipal();
}
public Principal getLocalPrincipal() {
return delegate.getLocalPrincipal();
}
public String getCipherSuite() {
return delegate.getCipherSuite();
}
public String getProtocol() {
return delegate.getProtocol();
}
public String getPeerHost() {
return delegate.getPeerHost();
}
public int getPeerPort() {
return delegate.getPeerPort();
}
public int getPacketBufferSize() {
return delegate.getPacketBufferSize();
}
public int getApplicationBufferSize() {
return delegate.getApplicationBufferSize();
}
public String[] getLocalSupportedSignatureAlgorithms() {
return delegate.getLocalSupportedSignatureAlgorithms();
}
public String[] getPeerSupportedSignatureAlgorithms() {
return delegate.getPeerSupportedSignatureAlgorithms();
}
public List<SNIServerName> getRequestedServerNames() {
return delegate.getRequestedServerNames();
}
public List<byte[]> getStatusResponses() {
return delegate.getStatusResponses();
}
}

View file

@ -0,0 +1,132 @@
/*
* Copyright (c) 2018, 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.common;
import java.security.Principal;
import java.util.List;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SNIServerName;
/**
* All mutating methods throw UnsupportedOperationException
*/
public class ImmutableSSLSession implements SSLSession {
private final SSLSession delegate;
ImmutableSSLSession(SSLSession session) {
this.delegate = session;
}
public byte[] getId() {
return delegate.getId();
}
public SSLSessionContext getSessionContext() {
return delegate.getSessionContext();
}
public long getCreationTime() {
return delegate.getCreationTime();
}
public long getLastAccessedTime() {
return delegate.getLastAccessedTime();
}
public void invalidate() {
throw new UnsupportedOperationException("session is not mutable");
}
public boolean isValid() {
return delegate.isValid();
}
public void putValue(String name, Object value) {
throw new UnsupportedOperationException("session is not mutable");
}
public Object getValue(String name) {
return delegate.getValue(name);
}
public void removeValue(String name) {
throw new UnsupportedOperationException("session is not mutable");
}
public String [] getValueNames() {
return delegate.getValueNames();
}
public java.security.cert.Certificate [] getPeerCertificates()
throws SSLPeerUnverifiedException {
return delegate.getPeerCertificates();
}
public java.security.cert.Certificate [] getLocalCertificates() {
return delegate.getLocalCertificates();
}
@Deprecated
public javax.security.cert.X509Certificate [] getPeerCertificateChain()
throws SSLPeerUnverifiedException {
return delegate.getPeerCertificateChain();
}
public Principal getPeerPrincipal()
throws SSLPeerUnverifiedException {
return delegate.getPeerPrincipal();
}
public Principal getLocalPrincipal() {
return delegate.getLocalPrincipal();
}
public String getCipherSuite() {
return delegate.getCipherSuite();
}
public String getProtocol() {
return delegate.getProtocol();
}
public String getPeerHost() {
return delegate.getPeerHost();
}
public int getPeerPort() {
return delegate.getPeerPort();
}
public int getPacketBufferSize() {
return delegate.getPacketBufferSize();
}
public int getApplicationBufferSize() {
return delegate.getApplicationBufferSize();
}
}

View file

@ -0,0 +1,322 @@
/*
* Copyright (c) 2015, 2018, 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.common;
import java.net.http.HttpHeaders;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import jdk.internal.net.http.frame.DataFrame;
import jdk.internal.net.http.frame.Http2Frame;
import jdk.internal.net.http.frame.WindowUpdateFrame;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLParameters;
/**
* -Djava.net.HttpClient.log=
* errors,requests,headers,
* frames[:control:data:window:all..],content,ssl,trace
*
* Any of errors, requests, headers or content are optional.
*
* Other handlers may be added. All logging is at level INFO
*
* Logger name is "jdk.httpclient.HttpClient"
*/
// implements System.Logger in order to be skipped when printing the caller's
// information
public abstract class Log implements System.Logger {
static final String logProp = "jdk.httpclient.HttpClient.log";
public static final int OFF = 0;
public static final int ERRORS = 0x1;
public static final int REQUESTS = 0x2;
public static final int HEADERS = 0x4;
public static final int CONTENT = 0x8;
public static final int FRAMES = 0x10;
public static final int SSL = 0x20;
public static final int TRACE = 0x40;
static int logging;
// Frame types: "control", "data", "window", "all"
public static final int CONTROL = 1; // all except DATA and WINDOW_UPDATES
public static final int DATA = 2;
public static final int WINDOW_UPDATES = 4;
public static final int ALL = CONTROL| DATA | WINDOW_UPDATES;
static int frametypes;
static final System.Logger logger;
static {
String s = Utils.getNetProperty(logProp);
if (s == null) {
logging = OFF;
} else {
String[] vals = s.split(",");
for (String val : vals) {
switch (val.toLowerCase(Locale.US)) {
case "errors":
logging |= ERRORS;
break;
case "requests":
logging |= REQUESTS;
break;
case "headers":
logging |= HEADERS;
break;
case "content":
logging |= CONTENT;
break;
case "ssl":
logging |= SSL;
break;
case "trace":
logging |= TRACE;
break;
case "all":
logging |= CONTENT|HEADERS|REQUESTS|FRAMES|ERRORS|TRACE|SSL;
break;
default:
// ignore bad values
}
if (val.startsWith("frames")) {
logging |= FRAMES;
String[] types = val.split(":");
if (types.length == 1) {
frametypes = CONTROL | DATA | WINDOW_UPDATES;
} else {
for (String type : types) {
switch (type.toLowerCase(Locale.US)) {
case "control":
frametypes |= CONTROL;
break;
case "data":
frametypes |= DATA;
break;
case "window":
frametypes |= WINDOW_UPDATES;
break;
case "all":
frametypes = ALL;
break;
default:
// ignore bad values
}
}
}
}
}
}
if (logging != OFF) {
logger = System.getLogger("jdk.httpclient.HttpClient");
} else {
logger = null;
}
}
public static boolean errors() {
return (logging & ERRORS) != 0;
}
public static boolean requests() {
return (logging & REQUESTS) != 0;
}
public static boolean headers() {
return (logging & HEADERS) != 0;
}
public static boolean trace() {
return (logging & TRACE) != 0;
}
public static boolean ssl() {
return (logging & SSL) != 0;
}
public static boolean frames() {
return (logging & FRAMES) != 0;
}
public static void logError(String s, Object... s1) {
if (errors()) {
logger.log(Level.INFO, "ERROR: " + s, s1);
}
}
public static void logError(Throwable t) {
if (errors()) {
String s = Utils.stackTrace(t);
logger.log(Level.INFO, "ERROR: " + s);
}
}
public static void logSSL(String s, Object... s1) {
if (ssl()) {
logger.log(Level.INFO, "SSL: " + s, s1);
}
}
public static void logSSL(Supplier<String> msgSupplier) {
if (ssl()) {
logger.log(Level.INFO, "SSL: " + msgSupplier.get());
}
}
public static void logTrace(String s, Object... s1) {
if (trace()) {
String format = "TRACE: " + s;
logger.log(Level.INFO, format, s1);
}
}
public static void logRequest(String s, Object... s1) {
if (requests()) {
logger.log(Level.INFO, "REQUEST: " + s, s1);
}
}
public static void logResponse(Supplier<String> supplier) {
if (requests()) {
logger.log(Level.INFO, "RESPONSE: " + supplier.get());
}
}
public static void logHeaders(String s, Object... s1) {
if (headers()) {
logger.log(Level.INFO, "HEADERS: " + s, s1);
}
}
public static boolean loggingFrame(Class<? extends Http2Frame> clazz) {
if (frametypes == ALL) {
return true;
}
if (clazz == DataFrame.class) {
return (frametypes & DATA) != 0;
} else if (clazz == WindowUpdateFrame.class) {
return (frametypes & WINDOW_UPDATES) != 0;
} else {
return (frametypes & CONTROL) != 0;
}
}
public static void logFrames(Http2Frame f, String direction) {
if (frames() && loggingFrame(f.getClass())) {
logger.log(Level.INFO, "FRAME: " + direction + ": " + f.toString());
}
}
public static void logParams(SSLParameters p) {
if (!Log.ssl()) {
return;
}
if (p == null) {
Log.logSSL("SSLParameters: Null params");
return;
}
final StringBuilder sb = new StringBuilder("SSLParameters:");
final List<Object> params = new ArrayList<>();
if (p.getCipherSuites() != null) {
for (String cipher : p.getCipherSuites()) {
sb.append("\n cipher: {")
.append(params.size()).append("}");
params.add(cipher);
}
}
// SSLParameters.getApplicationProtocols() can't return null
// JDK 8 EXCL START
for (String approto : p.getApplicationProtocols()) {
sb.append("\n application protocol: {")
.append(params.size()).append("}");
params.add(approto);
}
// JDK 8 EXCL END
if (p.getProtocols() != null) {
for (String protocol : p.getProtocols()) {
sb.append("\n protocol: {")
.append(params.size()).append("}");
params.add(protocol);
}
}
if (p.getEndpointIdentificationAlgorithm() != null) {
sb.append("\n endpointIdAlg: {")
.append(params.size()).append("}");
params.add(p.getEndpointIdentificationAlgorithm());
}
if (p.getServerNames() != null) {
for (SNIServerName sname : p.getServerNames()) {
sb.append("\n server name: {")
.append(params.size()).append("}");
params.add(sname.toString());
}
}
sb.append('\n');
Log.logSSL(sb.toString(), params.toArray());
}
public static void dumpHeaders(StringBuilder sb, String prefix, HttpHeaders headers) {
if (headers != null) {
Map<String,List<String>> h = headers.map();
Set<Map.Entry<String,List<String>>> entries = h.entrySet();
String sep = "";
for (Map.Entry<String,List<String>> entry : entries) {
String key = entry.getKey();
List<String> values = entry.getValue();
if (values == null || values.isEmpty()) {
// should not happen
sb.append(sep);
sb.append(prefix).append(key).append(':');
sep = "\n";
continue;
}
for (String value : values) {
sb.append(sep);
sb.append(prefix).append(key).append(':');
sb.append(' ').append(value);
sep = "\n";
}
}
sb.append('\n');
}
}
// not instantiable
private Log() {}
}

View file

@ -0,0 +1,156 @@
/*
* Copyright (c) 2018, 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.common;
import java.util.function.Supplier;
/**
* An internal {@code System.Logger} that is used for internal
* debugging purposes in the {@link java.net.http} module.
* <p>
* Though not enforced, this interface is designed for emitting
* debug messages with Level.DEBUG.
* <p>
* It defines {@code log} methods that default to {@code Level.DEBUG},
* so that they can be called in statements like:
* <pre>{@code debug.log("some %s with %d %s", message(), one(), params());}</pre>
*
* @implSpec
* This interface is implemented by loggers returned by
* {@link Utils#getDebugLogger(Supplier, boolean)},
* {@link Utils#getWebSocketLogger(Supplier, boolean)}and
* {@link Utils#getHpackLogger(Supplier, boolean)}.
* It is not designed to be implemented by any other
* loggers. Do not use outside of this module.
*/
public interface Logger extends System.Logger {
/**
* Tells whether this logger is on.
* @implSpec The default implementation for this method calls
* {@code this.isLoggable(Level.DEBUG);}
*/
public default boolean on() {
return isLoggable(Level.DEBUG);
}
/**
* Logs a message.
*
* @implSpec The default implementation for this method calls
* {@code this.log(Level.DEBUG, msg);}
*
* @param msg the string message (or a key in the message catalog, if
* this logger is a {@link
* System.LoggerFinder#getLocalizedLogger(java.lang.String,
* java.util.ResourceBundle, java.lang.Module) localized logger});
* can be {@code null}.
*/
public default void log(String msg) {
log(Level.DEBUG, msg);
}
/**
* Logs a lazily supplied message.
*
* @implSpec The default implementation for this method calls
* {@code this.log(Level.DEBUG, msgSupplier);}
*
* @param msgSupplier a supplier function that produces a message.
*
* @throws NullPointerException if {@code msgSupplier} is {@code null}.
*/
public default void log(Supplier<String> msgSupplier) {
log(Level.DEBUG, msgSupplier);
}
/**
* Logs a message produced from the given object.
*
* @implSpec The default implementation for this method calls
* {@code this.log(Level.DEBUG, obj);}
*
* @param obj the object to log.
*
* @throws NullPointerException if {@code obj} is {@code null}.
*/
public default void log(Object obj) {
log(Level.DEBUG, obj);
}
/**
* Logs a message associated with a given throwable.
*
* @implSpec The default implementation for this method calls
* {@code this.log(Level.DEBUG, msg, thrown);}
*
* @param msg the string message (or a key in the message catalog, if
* this logger is a {@link
* System.LoggerFinder#getLocalizedLogger(java.lang.String,
* java.util.ResourceBundle, java.lang.Module) localized logger});
* can be {@code null}.
* @param thrown a {@code Throwable} associated with the log message;
* can be {@code null}.
*/
public default void log(String msg, Throwable thrown) {
this.log(Level.DEBUG, msg, thrown);
}
/**
* Logs a lazily supplied message associated with a given throwable.
*
* @implSpec The default implementation for this method calls
* {@code this.log(Level.DEBUG, msgSupplier, thrown);}
*
* @param msgSupplier a supplier function that produces a message.
* @param thrown a {@code Throwable} associated with log message;
* can be {@code null}.
*
* @throws NullPointerException if {@code msgSupplier} is {@code null}.
*/
public default void log(Supplier<String> msgSupplier, Throwable thrown) {
log(Level.DEBUG, msgSupplier, thrown);
}
/**
* Logs a message with an optional list of parameters.
*
* @implSpec The default implementation for this method calls
* {@code this.log(Level.DEBUG, format, params);}
*
* @param format the string message format in
* {@link String#format(String, Object...)} or {@link
* java.text.MessageFormat} format, (or a key in the message
* catalog, if this logger is a {@link
* System.LoggerFinder#getLocalizedLogger(java.lang.String,
* java.util.ResourceBundle, java.lang.Module) localized logger});
* can be {@code null}.
* @param params an optional list of parameters to the message (may be
* none).
*/
public default void log(String format, Object... params) {
log(Level.DEBUG, format, params);
}
}

View file

@ -0,0 +1,110 @@
/*
* Copyright (c) 2016, 2018, 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.common;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicLong;
import static java.util.Objects.requireNonNull;
/*
* A CompletableFuture which does not allow any obtrusion logic.
* All methods of CompletionStage return instances of this class.
*/
public final class MinimalFuture<T> extends CompletableFuture<T> {
@FunctionalInterface
public interface ExceptionalSupplier<U> {
U get() throws Throwable;
}
private final static AtomicLong TOKENS = new AtomicLong();
private final long id;
public static <U> MinimalFuture<U> completedFuture(U value) {
MinimalFuture<U> f = new MinimalFuture<>();
f.complete(value);
return f;
}
public static <U> CompletableFuture<U> failedFuture(Throwable ex) {
requireNonNull(ex);
MinimalFuture<U> f = new MinimalFuture<>();
f.completeExceptionally(ex);
return f;
}
public static <U> CompletableFuture<U> supply(ExceptionalSupplier<U> supplier) {
CompletableFuture<U> cf = new MinimalFuture<>();
try {
U value = supplier.get();
cf.complete(value);
} catch (Throwable t) {
cf.completeExceptionally(t);
}
return cf;
}
public MinimalFuture() {
super();
this.id = TOKENS.incrementAndGet();
}
@Override
public <U> MinimalFuture<U> newIncompleteFuture() {
return new MinimalFuture<>();
}
@Override
public void obtrudeValue(T value) {
throw new UnsupportedOperationException();
}
@Override
public void obtrudeException(Throwable ex) {
throw new UnsupportedOperationException();
}
@Override
public String toString() {
return super.toString() + " (id=" + id +")";
}
public static <U> MinimalFuture<U> of(CompletionStage<U> stage) {
MinimalFuture<U> cf = new MinimalFuture<>();
stage.whenComplete((r,t) -> complete(cf, r, t));
return cf;
}
private static <U> void complete(CompletableFuture<U> cf, U result, Throwable t) {
if (t == null) {
cf.complete(result);
} else {
cf.completeExceptionally(t);
}
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (c) 2018, 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.common;
import java.net.http.HttpClient;
/**
* A small class allowing to track how many operations are
* left outstanding on an instance of HttpClient.
*/
public final class OperationTrackers {
private OperationTrackers() {
throw new InternalError("not instantiable");
}
/**
* A tracker can return the current value of
* operation counters maintained by an instance
* of {@link Trackable}, such as an HttpClientImpl.
*/
public interface Tracker {
// The total number of outstanding operations
long getOutstandingOperations();
// The number of outstanding HTTP/1.1 operations.
// A single HTTP/1.1 request may increment this counter
// multiple times, so the value returned will be >= to
// the number of active HTTP/1.1 connections, but will
// still be 0 if there are no active connections.
long getOutstandingHttpOperations();
// The number of active HTTP/2 streams
long getOutstandingHttp2Streams();
// The number of active WebSockets
long getOutstandingWebSocketOperations();
// Whether the facade returned to the
// user is still referenced
boolean isFacadeReferenced();
// The name of the object being tracked.
String getName();
}
/**
* Implemented by objects that maintain operation counters.
*/
public interface Trackable {
Tracker getOperationsTracker();
}
/**
* Returns a tracker to track pending operations started on
* an HttpClient instance. May return null if this isn't
* an HttpClientImpl or HttpClientFacade.
* @param client the HttpClient instance to track.
* @return A tracker or null.
*/
public static Tracker getTracker(HttpClient client) {
if (client instanceof Trackable) {
return ((Trackable)client).getOperationsTracker();
} else {
return null;
}
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2015, 2018, 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.common;
/**
* A simple paired value class
*/
public final class Pair<T, U> {
public Pair(T first, U second) {
this.second = second;
this.first = first;
}
public final T first;
public final U second;
// Because 'pair()' is shorter than 'new Pair<>()'.
// Sometimes this difference might be very significant (especially in a
// 80-ish characters boundary). Sorry diamond operator.
public static <T, U> Pair<T, U> pair(T first, U second) {
return new Pair<>(first, second);
}
@Override
public String toString() {
return "(" + first + ", " + second + ")";
}
}

View file

@ -0,0 +1,928 @@
/*
* Copyright (c) 2017, 2018, 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.common;
import jdk.internal.net.http.common.SubscriberWrapper.SchedulingAction;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.lang.System.Logger.Level;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Implements SSL using two SubscriberWrappers.
*
* <p> Constructor takes two Flow.Subscribers: one that receives the network
* data (after it has been encrypted by SSLFlowDelegate) data, and one that
* receives the application data (before it has been encrypted by SSLFlowDelegate).
*
* <p> Methods upstreamReader() and upstreamWriter() return the corresponding
* Flow.Subscribers containing Flows for the encrypted/decrypted upstream data.
* See diagram below.
*
* <p> How Flow.Subscribers are used in this class, and where they come from:
* <pre>
* {@code
*
*
*
* ---------> data flow direction
*
*
* +------------------+
* upstreamWriter | | downWriter
* ---------------> | | ------------>
* obtained from this | | supplied to constructor
* | SSLFlowDelegate |
* downReader | | upstreamReader
* <--------------- | | <--------------
* supplied to constructor | | obtained from this
* +------------------+
*
* Errors are reported to the downReader Flow.Subscriber
*
* }
* </pre>
*/
public class SSLFlowDelegate {
final Logger debug =
Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
final Executor exec;
final Reader reader;
final Writer writer;
final SSLEngine engine;
final String tubeName; // hack
final CompletableFuture<String> alpnCF; // completes on initial handshake
final static ByteBuffer SENTINEL = Utils.EMPTY_BYTEBUFFER;
volatile boolean close_notify_received;
final CompletableFuture<Void> readerCF;
final CompletableFuture<Void> writerCF;
static AtomicInteger scount = new AtomicInteger(1);
final int id;
/**
* Creates an SSLFlowDelegate fed from two Flow.Subscribers. Each
* Flow.Subscriber requires an associated {@link CompletableFuture}
* for errors that need to be signaled from downstream to upstream.
*/
public SSLFlowDelegate(SSLEngine engine,
Executor exec,
Subscriber<? super List<ByteBuffer>> downReader,
Subscriber<? super List<ByteBuffer>> downWriter)
{
this.id = scount.getAndIncrement();
this.tubeName = String.valueOf(downWriter);
this.reader = new Reader();
this.writer = new Writer();
this.engine = engine;
this.exec = exec;
this.handshakeState = new AtomicInteger(NOT_HANDSHAKING);
this.readerCF = reader.completion();
this.writerCF = reader.completion();
readerCF.exceptionally(this::stopOnError);
writerCF.exceptionally(this::stopOnError);
CompletableFuture.allOf(reader.completion(), writer.completion())
.thenRun(this::normalStop);
this.alpnCF = new MinimalFuture<>();
// connect the Reader to the downReader and the
// Writer to the downWriter.
connect(downReader, downWriter);
//Monitor.add(this::monitor);
}
/**
* Returns true if the SSLFlowDelegate has detected a TLS
* close_notify from the server.
* @return true, if a close_notify was detected.
*/
public boolean closeNotifyReceived() {
return close_notify_received;
}
/**
* Connects the read sink (downReader) to the SSLFlowDelegate Reader,
* and the write sink (downWriter) to the SSLFlowDelegate Writer.
* Called from within the constructor. Overwritten by SSLTube.
*
* @param downReader The left hand side read sink (typically, the
* HttpConnection read subscriber).
* @param downWriter The right hand side write sink (typically
* the SocketTube write subscriber).
*/
void connect(Subscriber<? super List<ByteBuffer>> downReader,
Subscriber<? super List<ByteBuffer>> downWriter) {
this.reader.subscribe(downReader);
this.writer.subscribe(downWriter);
}
/**
* Returns a CompletableFuture<String> which completes after
* the initial handshake completes, and which contains the negotiated
* alpn.
*/
public CompletableFuture<String> alpn() {
return alpnCF;
}
private void setALPN() {
// Handshake is finished. So, can retrieve the ALPN now
if (alpnCF.isDone())
return;
String alpn = engine.getApplicationProtocol();
if (debug.on()) debug.log("setALPN = %s", alpn);
alpnCF.complete(alpn);
}
public String monitor() {
StringBuilder sb = new StringBuilder();
sb.append("SSL: id ").append(id);
sb.append(" HS state: " + states(handshakeState));
sb.append(" Engine state: " + engine.getHandshakeStatus().toString());
sb.append(" LL : ");
for (String s: stateList) {
sb.append(s).append(" ");
}
sb.append("\r\n");
sb.append("Reader:: ").append(reader.toString());
sb.append("\r\n");
sb.append("Writer:: ").append(writer.toString());
sb.append("\r\n===================================");
return sb.toString();
}
protected SchedulingAction enterReadScheduling() {
return SchedulingAction.CONTINUE;
}
/**
* Processing function for incoming data. Pass it thru SSLEngine.unwrap().
* Any decrypted buffers returned to be passed downstream.
* Status codes:
* NEED_UNWRAP: do nothing. Following incoming data will contain
* any required handshake data
* NEED_WRAP: call writer.addData() with empty buffer
* NEED_TASK: delegate task to executor
* BUFFER_OVERFLOW: allocate larger output buffer. Repeat unwrap
* BUFFER_UNDERFLOW: keep buffer and wait for more data
* OK: return generated buffers.
*
* Upstream subscription strategy is to try and keep no more than
* TARGET_BUFSIZE bytes in readBuf
*/
class Reader extends SubscriberWrapper {
final SequentialScheduler scheduler;
static final int TARGET_BUFSIZE = 16 * 1024;
volatile ByteBuffer readBuf;
volatile boolean completing;
final Object readBufferLock = new Object();
final Logger debugr = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
class ReaderDownstreamPusher implements Runnable {
@Override public void run() { processData(); }
}
Reader() {
super();
scheduler = SequentialScheduler.synchronizedScheduler(
new ReaderDownstreamPusher());
this.readBuf = ByteBuffer.allocate(1024);
readBuf.limit(0); // keep in read mode
}
protected SchedulingAction enterScheduling() {
return enterReadScheduling();
}
public final String dbgString() {
return "SSL Reader(" + tubeName + ")";
}
/**
* entry point for buffers delivered from upstream Subscriber
*/
@Override
public void incoming(List<ByteBuffer> buffers, boolean complete) {
if (debugr.on())
debugr.log("Adding %d bytes to read buffer",
Utils.remaining(buffers));
addToReadBuf(buffers, complete);
scheduler.runOrSchedule();
}
@Override
public String toString() {
return "READER: " + super.toString() + " readBuf: " + readBuf.toString()
+ " count: " + count.toString();
}
private void reallocReadBuf() {
int sz = readBuf.capacity();
ByteBuffer newb = ByteBuffer.allocate(sz*2);
readBuf.flip();
Utils.copy(readBuf, newb);
readBuf = newb;
}
@Override
protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) {
if (readBuf.remaining() > TARGET_BUFSIZE) {
return 0;
} else {
return super.upstreamWindowUpdate(currentWindow, downstreamQsize);
}
}
// readBuf is kept ready for reading outside of this method
private void addToReadBuf(List<ByteBuffer> buffers, boolean complete) {
synchronized (readBufferLock) {
for (ByteBuffer buf : buffers) {
readBuf.compact();
while (readBuf.remaining() < buf.remaining())
reallocReadBuf();
readBuf.put(buf);
readBuf.flip();
}
if (complete) {
this.completing = complete;
}
}
}
void schedule() {
scheduler.runOrSchedule();
}
void stop() {
if (debugr.on()) debugr.log("stop");
scheduler.stop();
}
AtomicInteger count = new AtomicInteger(0);
// work function where it all happens
void processData() {
try {
if (debugr.on())
debugr.log("processData:"
+ " readBuf remaining:" + readBuf.remaining()
+ ", state:" + states(handshakeState)
+ ", engine handshake status:" + engine.getHandshakeStatus());
int len;
boolean complete = false;
while ((len = readBuf.remaining()) > 0) {
boolean handshaking = false;
try {
EngineResult result;
synchronized (readBufferLock) {
complete = this.completing;
result = unwrapBuffer(readBuf);
if (debugr.on())
debugr.log("Unwrapped: %s", result.result);
}
if (result.bytesProduced() > 0) {
if (debugr.on())
debugr.log("sending %d", result.bytesProduced());
count.addAndGet(result.bytesProduced());
outgoing(result.destBuffer, false);
}
if (result.status() == Status.BUFFER_UNDERFLOW) {
if (debugr.on()) debugr.log("BUFFER_UNDERFLOW");
// not enough data in the read buffer...
requestMore();
synchronized (readBufferLock) {
// check if we have received some data
if (readBuf.remaining() > len) continue;
return;
}
}
if (complete && result.status() == Status.CLOSED) {
if (debugr.on()) debugr.log("Closed: completing");
outgoing(Utils.EMPTY_BB_LIST, true);
return;
}
if (result.handshaking() && !complete) {
if (debugr.on()) debugr.log("handshaking");
if (doHandshake(result, READER)) {
resumeActivity();
}
handshaking = true;
} else {
if ((handshakeState.getAndSet(NOT_HANDSHAKING)& ~DOING_TASKS) == HANDSHAKING) {
setALPN();
handshaking = false;
resumeActivity();
}
}
} catch (IOException ex) {
errorCommon(ex);
handleError(ex);
}
if (handshaking && !complete)
return;
}
if (!complete) {
synchronized (readBufferLock) {
complete = this.completing && !readBuf.hasRemaining();
}
}
if (complete) {
if (debugr.on()) debugr.log("completing");
// Complete the alpnCF, if not already complete, regardless of
// whether or not the ALPN is available, there will be no more
// activity.
setALPN();
outgoing(Utils.EMPTY_BB_LIST, true);
}
} catch (Throwable ex) {
errorCommon(ex);
handleError(ex);
}
}
EngineResult unwrapBuffer(ByteBuffer src) throws IOException {
ByteBuffer dst = getAppBuffer();
while (true) {
SSLEngineResult sslResult = engine.unwrap(src, dst);
switch (sslResult.getStatus()) {
case BUFFER_OVERFLOW:
// may happen only if app size buffer was changed.
// get it again if app buffer size changed
int appSize = engine.getSession().getApplicationBufferSize();
ByteBuffer b = ByteBuffer.allocate(appSize + dst.position());
dst.flip();
b.put(dst);
dst = b;
break;
case CLOSED:
return doClosure(new EngineResult(sslResult));
case BUFFER_UNDERFLOW:
// handled implicitly by compaction/reallocation of readBuf
return new EngineResult(sslResult);
case OK:
dst.flip();
return new EngineResult(sslResult, dst);
}
}
}
}
public interface Monitorable {
public String getInfo();
}
public static class Monitor extends Thread {
final List<Monitorable> list;
static Monitor themon;
static {
themon = new Monitor();
themon.start(); // uncomment to enable Monitor
}
Monitor() {
super("Monitor");
setDaemon(true);
list = Collections.synchronizedList(new LinkedList<>());
}
void addTarget(Monitorable o) {
list.add(o);
}
public static void add(Monitorable o) {
themon.addTarget(o);
}
@Override
public void run() {
System.out.println("Monitor starting");
try {
while (true) {
Thread.sleep(20 * 1000);
synchronized (list) {
for (Monitorable o : list) {
System.out.println(o.getInfo());
System.out.println("-------------------------");
}
}
System.out.println("--o-o-o-o-o-o-o-o-o-o-o-o-o-o-");
}
} catch (InterruptedException e) {
System.out.println("Monitor exiting with " + e);
}
}
}
/**
* Processing function for outgoing data. Pass it thru SSLEngine.wrap()
* Any encrypted buffers generated are passed downstream to be written.
* Status codes:
* NEED_UNWRAP: call reader.addData() with empty buffer
* NEED_WRAP: call addData() with empty buffer
* NEED_TASK: delegate task to executor
* BUFFER_OVERFLOW: allocate larger output buffer. Repeat wrap
* BUFFER_UNDERFLOW: shouldn't happen on writing side
* OK: return generated buffers
*/
class Writer extends SubscriberWrapper {
final SequentialScheduler scheduler;
// queues of buffers received from upstream waiting
// to be processed by the SSLEngine
final List<ByteBuffer> writeList;
final Logger debugw = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
volatile boolean completing;
boolean completed; // only accessed in processData
class WriterDownstreamPusher extends SequentialScheduler.CompleteRestartableTask {
@Override public void run() { processData(); }
}
Writer() {
super();
writeList = Collections.synchronizedList(new LinkedList<>());
scheduler = new SequentialScheduler(new WriterDownstreamPusher());
}
@Override
protected void incoming(List<ByteBuffer> buffers, boolean complete) {
assert complete ? buffers == Utils.EMPTY_BB_LIST : true;
assert buffers != Utils.EMPTY_BB_LIST ? complete == false : true;
if (complete) {
if (debugw.on()) debugw.log("adding SENTINEL");
completing = true;
writeList.add(SENTINEL);
} else {
writeList.addAll(buffers);
}
if (debugw.on())
debugw.log("added " + buffers.size()
+ " (" + Utils.remaining(buffers)
+ " bytes) to the writeList");
scheduler.runOrSchedule();
}
public final String dbgString() {
return "SSL Writer(" + tubeName + ")";
}
protected void onSubscribe() {
if (debugw.on()) debugw.log("onSubscribe initiating handshaking");
addData(HS_TRIGGER); // initiates handshaking
}
void schedule() {
scheduler.runOrSchedule();
}
void stop() {
if (debugw.on()) debugw.log("stop");
scheduler.stop();
}
@Override
public boolean closing() {
return closeNotifyReceived();
}
private boolean isCompleting() {
return completing;
}
@Override
protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) {
if (writeList.size() > 10)
return 0;
else
return super.upstreamWindowUpdate(currentWindow, downstreamQsize);
}
private boolean hsTriggered() {
synchronized(writeList) {
for (ByteBuffer b : writeList)
if (b == HS_TRIGGER)
return true;
return false;
}
}
private void processData() {
boolean completing = isCompleting();
try {
if (debugw.on())
debugw.log("processData, writeList remaining:"
+ Utils.remaining(writeList) + ", hsTriggered:"
+ hsTriggered() + ", needWrap:" + needWrap());
while (Utils.remaining(writeList) > 0 || hsTriggered() || needWrap()) {
ByteBuffer[] outbufs = writeList.toArray(Utils.EMPTY_BB_ARRAY);
EngineResult result = wrapBuffers(outbufs);
if (debugw.on())
debugw.log("wrapBuffer returned %s", result.result);
if (result.status() == Status.CLOSED) {
if (!upstreamCompleted) {
upstreamCompleted = true;
upstreamSubscription.cancel();
}
if (result.bytesProduced() <= 0)
return;
if (!completing && !completed) {
completing = this.completing = true;
// There could still be some outgoing data in outbufs.
writeList.add(SENTINEL);
}
}
boolean handshaking = false;
if (result.handshaking()) {
if (debugw.on()) debugw.log("handshaking");
doHandshake(result, WRITER); // ok to ignore return
handshaking = true;
} else {
if ((handshakeState.getAndSet(NOT_HANDSHAKING) & ~DOING_TASKS) == HANDSHAKING) {
setALPN();
resumeActivity();
}
}
cleanList(writeList); // tidy up the source list
sendResultBytes(result);
if (handshaking && !completing) {
if (needWrap()) {
continue;
} else {
return;
}
}
}
if (completing && Utils.remaining(writeList) == 0) {
if (!completed) {
completed = true;
writeList.clear();
outgoing(Utils.EMPTY_BB_LIST, true);
}
return;
}
if (writeList.isEmpty() && needWrap()) {
writer.addData(HS_TRIGGER);
}
} catch (Throwable ex) {
errorCommon(ex);
handleError(ex);
}
}
@SuppressWarnings("fallthrough")
EngineResult wrapBuffers(ByteBuffer[] src) throws SSLException {
if (debugw.on())
debugw.log("wrapping " + Utils.remaining(src) + " bytes");
ByteBuffer dst = getNetBuffer();
while (true) {
SSLEngineResult sslResult = engine.wrap(src, dst);
if (debugw.on()) debugw.log("SSLResult: " + sslResult);
switch (sslResult.getStatus()) {
case BUFFER_OVERFLOW:
// Shouldn't happen. We allocated buffer with packet size
// get it again if net buffer size was changed
if (debugw.on()) debugw.log("BUFFER_OVERFLOW");
int appSize = engine.getSession().getApplicationBufferSize();
ByteBuffer b = ByteBuffer.allocate(appSize + dst.position());
dst.flip();
b.put(dst);
dst = b;
break; // try again
case CLOSED:
if (debugw.on()) debugw.log("CLOSED");
// fallthrough. There could be some remaining data in dst.
// CLOSED will be handled by the caller.
case OK:
dst.flip();
final ByteBuffer dest = dst;
if (debugw.on())
debugw.log("OK => produced: %d, not wrapped: %d",
dest.remaining(), Utils.remaining(src));
return new EngineResult(sslResult, dest);
case BUFFER_UNDERFLOW:
// Shouldn't happen. Doesn't returns when wrap()
// underflow handled externally
// assert false : "Buffer Underflow";
if (debug.on()) debug.log("BUFFER_UNDERFLOW");
return new EngineResult(sslResult);
default:
if (debugw.on())
debugw.log("result: %s", sslResult.getStatus());
assert false : "result:" + sslResult.getStatus();
}
}
}
private boolean needWrap() {
return engine.getHandshakeStatus() == HandshakeStatus.NEED_WRAP;
}
private void sendResultBytes(EngineResult result) {
if (result.bytesProduced() > 0) {
if (debugw.on())
debugw.log("Sending %d bytes downstream",
result.bytesProduced());
outgoing(result.destBuffer, false);
}
}
@Override
public String toString() {
return "WRITER: " + super.toString() +
" writeList size " + Integer.toString(writeList.size());
//" writeList: " + writeList.toString();
}
}
private void handleError(Throwable t) {
if (debug.on()) debug.log("handleError", t);
readerCF.completeExceptionally(t);
writerCF.completeExceptionally(t);
// no-op if already completed
alpnCF.completeExceptionally(t);
reader.stop();
writer.stop();
}
boolean stopped;
private synchronized void normalStop() {
if (stopped)
return;
stopped = true;
reader.stop();
writer.stop();
}
private Void stopOnError(Throwable currentlyUnused) {
// maybe log, etc
normalStop();
return null;
}
private void cleanList(List<ByteBuffer> l) {
synchronized (l) {
Iterator<ByteBuffer> iter = l.iterator();
while (iter.hasNext()) {
ByteBuffer b = iter.next();
if (!b.hasRemaining() && b != SENTINEL) {
iter.remove();
}
}
}
}
/**
* States for handshake. We avoid races when accessing/updating the AtomicInt
* because updates always schedule an additional call to both the read()
* and write() functions.
*/
private static final int NOT_HANDSHAKING = 0;
private static final int HANDSHAKING = 1;
private static final int DOING_TASKS = 4; // bit added to above state
private static final ByteBuffer HS_TRIGGER = ByteBuffer.allocate(0);
private static final int READER = 1;
private static final int WRITER = 2;
private static String states(AtomicInteger state) {
int s = state.get();
StringBuilder sb = new StringBuilder();
int x = s & ~DOING_TASKS;
switch (x) {
case NOT_HANDSHAKING:
sb.append(" NOT_HANDSHAKING ");
break;
case HANDSHAKING:
sb.append(" HANDSHAKING ");
break;
default:
throw new InternalError();
}
if ((s & DOING_TASKS) > 0)
sb.append("|DOING_TASKS");
return sb.toString();
}
private void resumeActivity() {
reader.schedule();
writer.schedule();
}
final AtomicInteger handshakeState;
final ConcurrentLinkedQueue<String> stateList = new ConcurrentLinkedQueue<>();
private boolean doHandshake(EngineResult r, int caller) {
// unconditionally sets the HANDSHAKING bit, while preserving DOING_TASKS
handshakeState.getAndAccumulate(HANDSHAKING, (current, update) -> update | (current & DOING_TASKS));
stateList.add(r.handshakeStatus().toString());
stateList.add(Integer.toString(caller));
switch (r.handshakeStatus()) {
case NEED_TASK:
int s = handshakeState.getAndUpdate((current) -> current | DOING_TASKS);
if ((s & DOING_TASKS) > 0) // someone else was doing tasks
return false;
if (debug.on()) debug.log("obtaining and initiating task execution");
List<Runnable> tasks = obtainTasks();
executeTasks(tasks);
return false; // executeTasks will resume activity
case NEED_WRAP:
if (caller == READER) {
writer.addData(HS_TRIGGER);
return false;
}
break;
case NEED_UNWRAP:
case NEED_UNWRAP_AGAIN:
// do nothing else
// receiving-side data will trigger unwrap
break;
default:
throw new InternalError("Unexpected handshake status:"
+ r.handshakeStatus());
}
return true;
}
private List<Runnable> obtainTasks() {
List<Runnable> l = new ArrayList<>();
Runnable r;
while ((r = engine.getDelegatedTask()) != null) {
l.add(r);
}
return l;
}
private void executeTasks(List<Runnable> tasks) {
if (tasks.isEmpty())
return;
exec.execute(() -> {
try {
List<Runnable> nextTasks = tasks;
do {
nextTasks.forEach(Runnable::run);
if (engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
nextTasks = obtainTasks();
} else {
break;
}
} while (true);
handshakeState.getAndUpdate((current) -> current & ~DOING_TASKS);
//writer.addData(HS_TRIGGER);
resumeActivity();
} catch (Throwable t) {
handleError(t);
}
});
}
// FIXME: acknowledge a received CLOSE request from peer
EngineResult doClosure(EngineResult r) throws IOException {
if (debug.on())
debug.log("doClosure(%s): %s [isOutboundDone: %s, isInboundDone: %s]",
r.result, engine.getHandshakeStatus(),
engine.isOutboundDone(), engine.isInboundDone());
if (engine.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) {
// we have received TLS close_notify and need to send
// an acknowledgement back. We're calling doHandshake
// to finish the close handshake.
if (engine.isInboundDone() && !engine.isOutboundDone()) {
if (debug.on()) debug.log("doClosure: close_notify received");
close_notify_received = true;
doHandshake(r, READER);
}
}
return r;
}
/**
* Returns the upstream Flow.Subscriber of the reading (incoming) side.
* This flow must be given the encrypted data read from upstream (eg socket)
* before it is decrypted.
*/
public Flow.Subscriber<List<ByteBuffer>> upstreamReader() {
return reader;
}
/**
* Returns the upstream Flow.Subscriber of the writing (outgoing) side.
* This flow contains the plaintext data before it is encrypted.
*/
public Flow.Subscriber<List<ByteBuffer>> upstreamWriter() {
return writer;
}
public boolean resumeReader() {
return reader.signalScheduling();
}
public void resetReaderDemand() {
reader.resetDownstreamDemand();
}
static class EngineResult {
final SSLEngineResult result;
final ByteBuffer destBuffer;
// normal result
EngineResult(SSLEngineResult result) {
this(result, null);
}
EngineResult(SSLEngineResult result, ByteBuffer destBuffer) {
this.result = result;
this.destBuffer = destBuffer;
}
boolean handshaking() {
HandshakeStatus s = result.getHandshakeStatus();
return s != HandshakeStatus.FINISHED
&& s != HandshakeStatus.NOT_HANDSHAKING
&& result.getStatus() != Status.CLOSED;
}
boolean needUnwrap() {
HandshakeStatus s = result.getHandshakeStatus();
return s == HandshakeStatus.NEED_UNWRAP;
}
int bytesConsumed() {
return result.bytesConsumed();
}
int bytesProduced() {
return result.bytesProduced();
}
SSLEngineResult.HandshakeStatus handshakeStatus() {
return result.getHandshakeStatus();
}
SSLEngineResult.Status status() {
return result.getStatus();
}
}
public ByteBuffer getNetBuffer() {
return ByteBuffer.allocate(engine.getSession().getPacketBufferSize());
}
private ByteBuffer getAppBuffer() {
return ByteBuffer.allocate(engine.getSession().getApplicationBufferSize());
}
final String dbgString() {
return "SSLFlowDelegate(" + tubeName + ")";
}
}

View file

@ -0,0 +1,588 @@
/*
* Copyright (c) 2017, 2018, 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.common;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import jdk.internal.net.http.common.SubscriberWrapper.SchedulingAction;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.FINISHED;
/**
* An implementation of FlowTube that wraps another FlowTube in an
* SSL flow.
* <p>
* The following diagram shows a typical usage of the SSLTube, where
* the SSLTube wraps a SocketTube on the right hand side, and is connected
* to an HttpConnection on the left hand side.
*
* <preformatted>{@code
* +---------- SSLTube -------------------------+
* | |
* | +---SSLFlowDelegate---+ |
* HttpConnection | | | | SocketTube
* read sink <- SSLSubscriberW. <- Reader <- upstreamR.() <---- read source
* (a subscriber) | | \ / | | (a publisher)
* | | SSLEngine | |
* HttpConnection | | / \ | | SocketTube
* write source -> SSLSubscriptionW. -> upstreamW.() -> Writer ----> write sink
* (a publisher) | | | | (a subscriber)
* | +---------------------+ |
* | |
* +---------------------------------------------+
* }</preformatted>
*/
public class SSLTube implements FlowTube {
final Logger debug =
Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
private final FlowTube tube;
private final SSLSubscriberWrapper readSubscriber;
private final SSLSubscriptionWrapper writeSubscription;
private final SSLFlowDelegate sslDelegate;
private final SSLEngine engine;
private volatile boolean finished;
public SSLTube(SSLEngine engine, Executor executor, FlowTube tube) {
Objects.requireNonNull(engine);
Objects.requireNonNull(executor);
this.tube = Objects.requireNonNull(tube);
writeSubscription = new SSLSubscriptionWrapper();
readSubscriber = new SSLSubscriberWrapper();
this.engine = engine;
sslDelegate = new SSLTubeFlowDelegate(engine,
executor,
readSubscriber,
tube);
}
final class SSLTubeFlowDelegate extends SSLFlowDelegate {
SSLTubeFlowDelegate(SSLEngine engine, Executor executor,
SSLSubscriberWrapper readSubscriber,
FlowTube tube) {
super(engine, executor, readSubscriber, tube);
}
protected SchedulingAction enterReadScheduling() {
readSubscriber.processPendingSubscriber();
return SchedulingAction.CONTINUE;
}
void connect(Flow.Subscriber<? super List<ByteBuffer>> downReader,
Flow.Subscriber<? super List<ByteBuffer>> downWriter) {
assert downWriter == tube;
assert downReader == readSubscriber;
// Connect the read sink first. That's the left-hand side
// downstream subscriber from the HttpConnection (or more
// accurately, the SSLSubscriberWrapper that will wrap it
// when SSLTube::connectFlows is called.
reader.subscribe(downReader);
// Connect the right hand side tube (the socket tube).
//
// The SSLFlowDelegate.writer publishes ByteBuffer to
// the SocketTube for writing on the socket, and the
// SSLFlowDelegate::upstreamReader subscribes to the
// SocketTube to receive ByteBuffers read from the socket.
//
// Basically this method is equivalent to:
// // connect the read source:
// // subscribe the SSLFlowDelegate upstream reader
// // to the socket tube publisher.
// tube.subscribe(upstreamReader());
// // connect the write sink:
// // subscribe the socket tube write subscriber
// // with the SSLFlowDelegate downstream writer.
// writer.subscribe(tube);
tube.connectFlows(FlowTube.asTubePublisher(writer),
FlowTube.asTubeSubscriber(upstreamReader()));
// Finally connect the write source. That's the left
// hand side publisher which will push ByteBuffer for
// writing and encryption to the SSLFlowDelegate.
// The writeSubscription is in fact the SSLSubscriptionWrapper
// that will wrap the subscription provided by the
// HttpConnection publisher when SSLTube::connectFlows
// is called.
upstreamWriter().onSubscribe(writeSubscription);
}
}
public CompletableFuture<String> getALPN() {
return sslDelegate.alpn();
}
@Override
public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
readSubscriber.dropSubscription();
readSubscriber.setDelegate(s);
s.onSubscribe(readSubscription);
}
/**
* Tells whether, or not, this FlowTube has finished receiving data.
*
* @return true when one of this FlowTube Subscriber's OnError or onComplete
* methods have been invoked
*/
@Override
public boolean isFinished() {
return finished;
}
private volatile Flow.Subscription readSubscription;
// The DelegateWrapper wraps a subscribed {@code Flow.Subscriber} and
// tracks the subscriber's state. In particular it makes sure that
// onComplete/onError are not called before onSubscribed.
final static class DelegateWrapper implements FlowTube.TubeSubscriber {
private final FlowTube.TubeSubscriber delegate;
private final Logger debug;
volatile boolean subscribedCalled;
volatile boolean subscribedDone;
volatile boolean completed;
volatile Throwable error;
DelegateWrapper(Flow.Subscriber<? super List<ByteBuffer>> delegate,
Logger debug) {
this.delegate = FlowTube.asTubeSubscriber(delegate);
this.debug = debug;
}
@Override
public void dropSubscription() {
if (subscribedCalled && !completed) {
delegate.dropSubscription();
}
}
@Override
public void onNext(List<ByteBuffer> item) {
assert subscribedCalled;
delegate.onNext(item);
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
onSubscribe(delegate::onSubscribe, subscription);
}
private void onSubscribe(Consumer<Flow.Subscription> method,
Flow.Subscription subscription) {
subscribedCalled = true;
method.accept(subscription);
Throwable x;
boolean finished;
synchronized (this) {
subscribedDone = true;
x = error;
finished = completed;
}
if (x != null) {
if (debug.on())
debug.log("Subscriber completed before subscribe: forwarding %s",
(Object)x);
delegate.onError(x);
} else if (finished) {
if (debug.on())
debug.log("Subscriber completed before subscribe: calling onComplete()");
delegate.onComplete();
}
}
@Override
public void onError(Throwable t) {
if (completed) {
if (debug.on())
debug.log("Subscriber already completed: ignoring %s",
(Object)t);
return;
}
boolean subscribed;
synchronized (this) {
if (completed) return;
error = t;
completed = true;
subscribed = subscribedDone;
}
if (subscribed) {
delegate.onError(t);
} else {
if (debug.on())
debug.log("Subscriber not yet subscribed: stored %s",
(Object)t);
}
}
@Override
public void onComplete() {
if (completed) return;
boolean subscribed;
synchronized (this) {
if (completed) return;
completed = true;
subscribed = subscribedDone;
}
if (subscribed) {
if (debug.on()) debug.log("DelegateWrapper: completing subscriber");
delegate.onComplete();
} else {
if (debug.on())
debug.log("Subscriber not yet subscribed: stored completed=true");
}
}
@Override
public String toString() {
return "DelegateWrapper:" + delegate.toString();
}
}
// Used to read data from the SSLTube.
final class SSLSubscriberWrapper implements FlowTube.TubeSubscriber {
private AtomicReference<DelegateWrapper> pendingDelegate =
new AtomicReference<>();
private volatile DelegateWrapper subscribed;
private volatile boolean onCompleteReceived;
private final AtomicReference<Throwable> errorRef
= new AtomicReference<>();
// setDelegate can be called asynchronously when the SSLTube flow
// is connected. At this time the permanent subscriber (this class)
// may already be subscribed (readSubscription != null) or not.
// 1. If it's already subscribed (readSubscription != null), we
// are going to signal the SSLFlowDelegate reader, and make sure
// onSubscribed is called within the reader flow
// 2. If it's not yet subscribed (readSubscription == null), then
// we're going to wait for onSubscribe to be called.
//
void setDelegate(Flow.Subscriber<? super List<ByteBuffer>> delegate) {
if (debug.on())
debug.log("SSLSubscriberWrapper (reader) got delegate: %s",
delegate);
assert delegate != null;
DelegateWrapper delegateWrapper = new DelegateWrapper(delegate, debug);
DelegateWrapper previous;
Flow.Subscription subscription;
boolean handleNow;
synchronized (this) {
previous = pendingDelegate.getAndSet(delegateWrapper);
subscription = readSubscription;
handleNow = this.errorRef.get() != null || finished;
}
if (previous != null) {
previous.dropSubscription();
}
if (subscription == null) {
if (debug.on())
debug.log("SSLSubscriberWrapper (reader) no subscription yet");
return;
}
if (handleNow || !sslDelegate.resumeReader()) {
processPendingSubscriber();
}
}
// Can be called outside of the flow if an error has already been
// raise. Otherwise, must be called within the SSLFlowDelegate
// downstream reader flow.
// If there is a subscription, and if there is a pending delegate,
// calls dropSubscription() on the previous delegate (if any),
// then subscribe the pending delegate.
void processPendingSubscriber() {
Flow.Subscription subscription;
DelegateWrapper delegateWrapper, previous;
synchronized (this) {
delegateWrapper = pendingDelegate.get();
if (delegateWrapper == null) return;
subscription = readSubscription;
previous = subscribed;
}
if (subscription == null) {
if (debug.on())
debug.log("SSLSubscriberWrapper (reader) " +
"processPendingSubscriber: no subscription yet");
return;
}
delegateWrapper = pendingDelegate.getAndSet(null);
if (delegateWrapper == null) return;
if (previous != null) {
previous.dropSubscription();
}
onNewSubscription(delegateWrapper, subscription);
}
@Override
public void dropSubscription() {
DelegateWrapper subscriberImpl = subscribed;
if (subscriberImpl != null) {
subscriberImpl.dropSubscription();
}
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
if (debug.on())
debug.log("SSLSubscriberWrapper (reader) onSubscribe(%s)",
subscription);
onSubscribeImpl(subscription);
}
// called in the reader flow, from onSubscribe.
private void onSubscribeImpl(Flow.Subscription subscription) {
assert subscription != null;
DelegateWrapper subscriberImpl, pending;
synchronized (this) {
readSubscription = subscription;
subscriberImpl = subscribed;
pending = pendingDelegate.get();
}
if (subscriberImpl == null && pending == null) {
if (debug.on())
debug.log("SSLSubscriberWrapper (reader) onSubscribeImpl: "
+ "no delegate yet");
return;
}
if (pending == null) {
// There is no pending delegate, but we have a previously
// subscribed delegate. This is obviously a re-subscribe.
// We are in the downstream reader flow, so we should call
// onSubscribe directly.
if (debug.on())
debug.log("SSLSubscriberWrapper (reader) onSubscribeImpl: "
+ "resubscribing");
onNewSubscription(subscriberImpl, subscription);
} else {
// We have some pending subscriber: subscribe it now that we have
// a subscription. If we already had a previous delegate then
// it will get a dropSubscription().
if (debug.on())
debug.log("SSLSubscriberWrapper (reader) onSubscribeImpl: "
+ "subscribing pending");
processPendingSubscriber();
}
}
private void onNewSubscription(DelegateWrapper subscriberImpl,
Flow.Subscription subscription) {
assert subscriberImpl != null;
assert subscription != null;
Throwable failed;
boolean completed;
// reset any demand that may have been made by the previous
// subscriber
sslDelegate.resetReaderDemand();
// send the subscription to the subscriber.
subscriberImpl.onSubscribe(subscription);
// The following twisted logic is just here that we don't invoke
// onError before onSubscribe. It also prevents race conditions
// if onError is invoked concurrently with setDelegate.
synchronized (this) {
failed = this.errorRef.get();
completed = finished;
subscribed = subscriberImpl;
}
if (failed != null) {
subscriberImpl.onError(failed);
} else if (completed) {
subscriberImpl.onComplete();
}
}
@Override
public void onNext(List<ByteBuffer> item) {
subscribed.onNext(item);
}
public void onErrorImpl(Throwable throwable) {
// The following twisted logic is just here that we don't invoke
// onError before onSubscribe. It also prevents race conditions
// if onError is invoked concurrently with setDelegate.
// See setDelegate.
errorRef.compareAndSet(null, throwable);
Throwable failed = errorRef.get();
finished = true;
if (debug.on())
debug.log("%s: onErrorImpl: %s", this, throwable);
DelegateWrapper subscriberImpl;
synchronized (this) {
subscriberImpl = subscribed;
}
if (subscriberImpl != null) {
subscriberImpl.onError(failed);
} else {
if (debug.on())
debug.log("%s: delegate null, stored %s", this, failed);
}
// now if we have any pending subscriber, we should forward
// the error to them immediately as the read scheduler will
// already be stopped.
processPendingSubscriber();
}
@Override
public void onError(Throwable throwable) {
assert !finished && !onCompleteReceived;
onErrorImpl(throwable);
}
private boolean handshaking() {
HandshakeStatus hs = engine.getHandshakeStatus();
return !(hs == NOT_HANDSHAKING || hs == FINISHED);
}
private boolean handshakeFailed() {
// sslDelegate can be null if we reach here
// during the initial handshake, as that happens
// within the SSLFlowDelegate constructor.
// In that case we will want to raise an exception.
return handshaking()
&& (sslDelegate == null
|| !sslDelegate.closeNotifyReceived());
}
@Override
public void onComplete() {
assert !finished && !onCompleteReceived;
onCompleteReceived = true;
DelegateWrapper subscriberImpl;
synchronized(this) {
subscriberImpl = subscribed;
}
if (handshakeFailed()) {
if (debug.on())
debug.log("handshake: %s, inbound done: %s outbound done: %s",
engine.getHandshakeStatus(),
engine.isInboundDone(),
engine.isOutboundDone());
onErrorImpl(new SSLHandshakeException(
"Remote host terminated the handshake"));
} else if (subscriberImpl != null) {
finished = true;
subscriberImpl.onComplete();
}
// now if we have any pending subscriber, we should complete
// them immediately as the read scheduler will already be stopped.
processPendingSubscriber();
}
}
@Override
public void connectFlows(TubePublisher writePub,
TubeSubscriber readSub) {
if (debug.on()) debug.log("connecting flows");
readSubscriber.setDelegate(readSub);
writePub.subscribe(this);
}
/** Outstanding write demand from the SSL Flow Delegate. */
private final Demand writeDemand = new Demand();
final class SSLSubscriptionWrapper implements Flow.Subscription {
volatile Flow.Subscription delegate;
void setSubscription(Flow.Subscription sub) {
long demand = writeDemand.get(); // FIXME: isn't it a racy way of passing the demand?
delegate = sub;
if (debug.on()) debug.log("setSubscription: demand=%d", demand);
if (demand > 0)
sub.request(demand);
}
@Override
public void request(long n) {
writeDemand.increase(n);
if (debug.on()) debug.log("request: n=%d", n);
Flow.Subscription sub = delegate;
if (sub != null && n > 0) {
sub.request(n);
}
}
@Override
public void cancel() {
// TODO: no-op or error?
}
}
/* Subscriber - writing side */
@Override
public void onSubscribe(Flow.Subscription subscription) {
Objects.requireNonNull(subscription);
Flow.Subscription x = writeSubscription.delegate;
if (x != null)
x.cancel();
writeSubscription.setSubscription(subscription);
}
@Override
public void onNext(List<ByteBuffer> item) {
Objects.requireNonNull(item);
boolean decremented = writeDemand.tryDecrement();
assert decremented : "Unexpected writeDemand: ";
if (debug.on())
debug.log("sending %d buffers to SSL flow delegate", item.size());
sslDelegate.upstreamWriter().onNext(item);
}
@Override
public void onError(Throwable throwable) {
Objects.requireNonNull(throwable);
sslDelegate.upstreamWriter().onError(throwable);
}
@Override
public void onComplete() {
sslDelegate.upstreamWriter().onComplete();
}
@Override
public String toString() {
return dbgString();
}
final String dbgString() {
return "SSLTube(" + tube + ")";
}
}

View file

@ -0,0 +1,362 @@
/*
* Copyright (c) 2016, 2018, 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.common;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import static java.util.Objects.requireNonNull;
/**
* A scheduler of ( repeatable ) tasks that MUST be run sequentially.
*
* <p> This class can be used as a synchronization aid that assists a number of
* parties in running a task in a mutually exclusive fashion.
*
* <p> To run the task, a party invokes {@code runOrSchedule}. To permanently
* prevent the task from subsequent runs, the party invokes {@code stop}.
*
* <p> The parties can, but do not have to, operate in different threads.
*
* <p> The task can be either synchronous ( completes when its {@code run}
* method returns ), or asynchronous ( completed when its
* {@code DeferredCompleter} is explicitly completed ).
*
* <p> The next run of the task will not begin until the previous run has
* finished.
*
* <p> The task may invoke {@code runOrSchedule} itself, which may be a normal
* situation.
*/
public final class SequentialScheduler {
/*
Since the task is fixed and known beforehand, no blocking synchronization
(locks, queues, etc.) is required. The job can be done solely using
nonblocking primitives.
The machinery below addresses two problems:
1. Running the task in a sequential order (no concurrent runs):
begin, end, begin, end...
2. Avoiding indefinite recursion:
begin
end
begin
end
...
Problem #1 is solved with a finite state machine with 4 states:
BEGIN, AGAIN, END, and STOP.
Problem #2 is solved with a "state modifier" OFFLOAD.
Parties invoke `runOrSchedule()` to signal the task must run. A party
that has invoked `runOrSchedule()` either begins the task or exploits the
party that is either beginning the task or ending it.
The party that is trying to end the task either ends it or begins it
again.
To avoid indefinite recursion, before re-running the task the
TryEndDeferredCompleter sets the OFFLOAD bit, signalling to its "child"
TryEndDeferredCompleter that this ("parent") TryEndDeferredCompleter is
available and the "child" must offload the task on to the "parent". Then
a race begins. Whichever invocation of TryEndDeferredCompleter.complete
manages to unset OFFLOAD bit first does not do the work.
There is at most 1 thread that is beginning the task and at most 2
threads that are trying to end it: "parent" and "child". In case of a
synchronous task "parent" and "child" are the same thread.
*/
/**
* An interface to signal the completion of a {@link RestartableTask}.
*
* <p> The invocation of {@code complete} completes the task. The invocation
* of {@code complete} may restart the task, if an attempt has previously
* been made to run the task while it was already running.
*
* @apiNote {@code DeferredCompleter} is useful when a task is not necessary
* complete when its {@code run} method returns, but will complete at a
* later time, and maybe in different thread. This type exists for
* readability purposes at use-sites only.
*/
public static abstract class DeferredCompleter {
/** Extensible from this (outer) class ONLY. */
private DeferredCompleter() { }
/** Completes the task. Must be called once, and once only. */
public abstract void complete();
}
/**
* A restartable task.
*/
@FunctionalInterface
public interface RestartableTask {
/**
* The body of the task.
*
* @param taskCompleter
* A completer that must be invoked once, and only once,
* when this task is logically finished
*/
void run(DeferredCompleter taskCompleter);
}
/**
* A simple and self-contained task that completes once its {@code run}
* method returns.
*/
public static abstract class CompleteRestartableTask
implements RestartableTask
{
@Override
public final void run(DeferredCompleter taskCompleter) {
try {
run();
} finally {
taskCompleter.complete();
}
}
/** The body of the task. */
protected abstract void run();
}
/**
* A task that runs its main loop within a synchronized block to provide
* memory visibility between runs. Since the main loop can't run concurrently,
* the lock shouldn't be contended and no deadlock should ever be possible.
*/
public static final class SynchronizedRestartableTask
extends CompleteRestartableTask {
private final Runnable mainLoop;
private final Object lock = new Object();
public SynchronizedRestartableTask(Runnable mainLoop) {
this.mainLoop = mainLoop;
}
@Override
protected void run() {
synchronized(lock) {
mainLoop.run();
}
}
}
private static final int OFFLOAD = 1;
private static final int AGAIN = 2;
private static final int BEGIN = 4;
private static final int STOP = 8;
private static final int END = 16;
private final AtomicInteger state = new AtomicInteger(END);
private final RestartableTask restartableTask;
private final DeferredCompleter completer;
private final SchedulableTask schedulableTask;
/**
* An auxiliary task that starts the restartable task:
* {@code restartableTask.run(completer)}.
*/
private final class SchedulableTask implements Runnable {
@Override
public void run() {
restartableTask.run(completer);
}
}
public SequentialScheduler(RestartableTask restartableTask) {
this.restartableTask = requireNonNull(restartableTask);
this.completer = new TryEndDeferredCompleter();
this.schedulableTask = new SchedulableTask();
}
/**
* Runs or schedules the task to be run.
*
* @implSpec The recursion which is possible here must be bounded:
*
* <pre>{@code
* this.runOrSchedule()
* completer.complete()
* this.runOrSchedule()
* ...
* }</pre>
*
* @implNote The recursion in this implementation has the maximum
* depth of 1.
*/
public void runOrSchedule() {
runOrSchedule(schedulableTask, null);
}
/**
* Executes or schedules the task to be executed in the provided executor.
*
* <p> This method can be used when potential executing from a calling
* thread is not desirable.
*
* @param executor
* An executor in which to execute the task, if the task needs
* to be executed.
*
* @apiNote The given executor can be {@code null} in which case calling
* {@code runOrSchedule(null)} is strictly equivalent to calling
* {@code runOrSchedule()}.
*/
public void runOrSchedule(Executor executor) {
runOrSchedule(schedulableTask, executor);
}
private void runOrSchedule(SchedulableTask task, Executor executor) {
while (true) {
int s = state.get();
if (s == END) {
if (state.compareAndSet(END, BEGIN)) {
break;
}
} else if ((s & BEGIN) != 0) {
// Tries to change the state to AGAIN, preserving OFFLOAD bit
if (state.compareAndSet(s, AGAIN | (s & OFFLOAD))) {
return;
}
} else if ((s & AGAIN) != 0 || s == STOP) {
/* In the case of AGAIN the scheduler does not provide
happens-before relationship between actions prior to
runOrSchedule() and actions that happen in task.run().
The reason is that no volatile write is done in this case,
and the call piggybacks on the call that has actually set
AGAIN state. */
return;
} else {
// Non-existent state, or the one that cannot be offloaded
throw new InternalError(String.valueOf(s));
}
}
if (executor == null) {
task.run();
} else {
executor.execute(task);
}
}
/** The only concrete {@code DeferredCompleter} implementation. */
private class TryEndDeferredCompleter extends DeferredCompleter {
@Override
public void complete() {
while (true) {
int s;
while (((s = state.get()) & OFFLOAD) != 0) {
// Tries to offload ending of the task to the parent
if (state.compareAndSet(s, s & ~OFFLOAD)) {
return;
}
}
while (true) {
if ((s & OFFLOAD) != 0) {
/* OFFLOAD bit can never be observed here. Otherwise
it would mean there is another invocation of
"complete" that can run the task. */
throw new InternalError(String.valueOf(s));
}
if (s == BEGIN) {
if (state.compareAndSet(BEGIN, END)) {
return;
}
} else if (s == AGAIN) {
if (state.compareAndSet(AGAIN, BEGIN | OFFLOAD)) {
break;
}
} else if (s == STOP) {
return;
} else if (s == END) {
throw new IllegalStateException("Duplicate completion");
} else {
// Non-existent state
throw new InternalError(String.valueOf(s));
}
s = state.get();
}
restartableTask.run(completer);
}
}
}
/**
* Tells whether, or not, this scheduler has been permanently stopped.
*
* <p> Should be used from inside the task to poll the status of the
* scheduler, pretty much the same way as it is done for threads:
* <pre>{@code
* if (!Thread.currentThread().isInterrupted()) {
* ...
* }
* }</pre>
*/
public boolean isStopped() {
return state.get() == STOP;
}
/**
* Stops this scheduler. Subsequent invocations of {@code runOrSchedule}
* are effectively no-ops.
*
* <p> If the task has already begun, this invocation will not affect it,
* unless the task itself uses {@code isStopped()} method to check the state
* of the handler.
*/
public void stop() {
state.set(STOP);
}
/**
* Returns a new {@code SequentialScheduler} that executes the provided
* {@code mainLoop} from within a {@link SynchronizedRestartableTask}.
*
* @apiNote This is equivalent to calling
* {@code new SequentialScheduler(new SynchronizedRestartableTask(mainLoop))}
* The main loop must not perform any blocking operation.
*
* @param mainLoop The main loop of the new sequential scheduler
* @return a new {@code SequentialScheduler} that executes the provided
* {@code mainLoop} from within a {@link SynchronizedRestartableTask}.
*/
public static SequentialScheduler synchronizedScheduler(Runnable mainLoop) {
return new SequentialScheduler(new SynchronizedRestartableTask(mainLoop));
}
}

View file

@ -0,0 +1,475 @@
/*
* Copyright (c) 2017, 2018, 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.common;
import java.io.Closeable;
import java.lang.System.Logger.Level;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Flow;
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
/**
* A wrapper for a Flow.Subscriber. This wrapper delivers data to the wrapped
* Subscriber which is supplied to the constructor. This class takes care of
* downstream flow control automatically and upstream flow control automatically
* by default.
* <p>
* Processing is done by implementing the {@link #incoming(List, boolean)} method
* which supplies buffers from upstream. This method (or any other method)
* can then call the outgoing() method to deliver processed buffers downstream.
* <p>
* Upstream error signals are delivered downstream directly. Cancellation from
* downstream is also propagated upstream immediately.
* <p>
* Each SubscriberWrapper has a {@link java.util.concurrent.CompletableFuture}{@code <Void>}
* which propagates completion/errors from downstream to upstream. Normal completion
* can only occur after onComplete() is called, but errors can be propagated upwards
* at any time.
*/
public abstract class SubscriberWrapper
implements FlowTube.TubeSubscriber, Closeable, Flow.Processor<List<ByteBuffer>,List<ByteBuffer>>
// TODO: SSLTube Subscriber will never change? Does this really need to be a TS?
{
final Logger debug =
Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
public enum SchedulingAction { CONTINUE, RETURN, RESCHEDULE }
volatile Flow.Subscription upstreamSubscription;
final SubscriptionBase downstreamSubscription;
volatile boolean upstreamCompleted;
volatile boolean downstreamCompleted;
volatile boolean completionAcknowledged;
private volatile Subscriber<? super List<ByteBuffer>> downstreamSubscriber;
// processed byte to send to the downstream subscriber.
private final ConcurrentLinkedQueue<List<ByteBuffer>> outputQ;
private final CompletableFuture<Void> cf;
private final SequentialScheduler pushScheduler;
private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
final AtomicLong upstreamWindow = new AtomicLong(0);
/**
* Wraps the given downstream subscriber. For each call to {@link
* #onNext(List<ByteBuffer>) } the given filter function is invoked
* and the list (if not empty) returned is passed downstream.
*
* A {@code CompletableFuture} is supplied which can be used to signal an
* error from downstream and which terminates the wrapper or which signals
* completion of downstream activity which can be propagated upstream. Error
* completion can be signaled at any time, but normal completion must not be
* signaled before onComplete() is called.
*/
public SubscriberWrapper()
{
this.outputQ = new ConcurrentLinkedQueue<>();
this.cf = new MinimalFuture<Void>();
cf.whenComplete((v,t) -> {
if (t != null)
errorCommon(t);
});
this.pushScheduler =
SequentialScheduler.synchronizedScheduler(new DownstreamPusher());
this.downstreamSubscription = new SubscriptionBase(pushScheduler,
this::downstreamCompletion);
}
@Override
public final void subscribe(Subscriber<? super List<ByteBuffer>> downstreamSubscriber) {
Objects.requireNonNull(downstreamSubscriber);
this.downstreamSubscriber = downstreamSubscriber;
}
/**
* Wraps the given downstream wrapper in this. For each call to
* {@link #onNext(List<ByteBuffer>) } the incoming() method is called.
*
* The {@code downstreamCF} from the downstream wrapper is linked to this
* wrappers notifier.
*
* @param downstreamWrapper downstream destination
*/
public SubscriberWrapper(Subscriber<? super List<ByteBuffer>> downstreamWrapper)
{
this();
subscribe(downstreamWrapper);
}
/**
* Delivers data to be processed by this wrapper. Generated data to be sent
* downstream, must be provided to the {@link #outgoing(List, boolean)}}
* method.
*
* @param buffers a List of ByteBuffers.
* @param complete if true then no more data will be added to the list
*/
protected abstract void incoming(List<ByteBuffer> buffers, boolean complete);
/**
* This method is called to determine the window size to use at any time. The
* current window is supplied together with the current downstream queue size.
* {@code 0} should be returned if no change is
* required or a positive integer which will be added to the current window.
* The default implementation maintains a downstream queue size of no greater
* than 5. The method can be overridden if required.
*
* @param currentWindow the current upstream subscription window
* @param downstreamQsize the current number of buffers waiting to be sent
* downstream
*
* @return value to add to currentWindow
*/
protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) {
if (downstreamQsize > 5) {
return 0;
}
if (currentWindow == 0) {
return 1;
} else {
return 0;
}
}
/**
* Override this if anything needs to be done after the upstream subscriber
* has subscribed
*/
protected void onSubscribe() {
}
/**
* Override this if anything needs to be done before checking for error
* and processing the input queue.
* @return
*/
protected SchedulingAction enterScheduling() {
return SchedulingAction.CONTINUE;
}
protected boolean signalScheduling() {
if (downstreamCompleted || pushScheduler.isStopped()) {
return false;
}
pushScheduler.runOrSchedule();
return true;
}
/**
* Delivers buffers of data downstream. After incoming()
* has been called complete == true signifying completion of the upstream
* subscription, data may continue to be delivered, up to when outgoing() is
* called complete == true, after which, the downstream subscription is
* completed.
*
* It's an error to call outgoing() with complete = true if incoming() has
* not previously been called with it.
*/
public void outgoing(ByteBuffer buffer, boolean complete) {
Objects.requireNonNull(buffer);
assert !complete || !buffer.hasRemaining();
outgoing(List.of(buffer), complete);
}
/**
* Sometime it might be necessary to complete the downstream subscriber
* before the upstream completes. For instance, when an SSL server
* sends a notify_close. In that case we should let the outgoing
* complete before upstream is completed.
* @return true, may be overridden by subclasses.
*/
public boolean closing() {
return false;
}
public void outgoing(List<ByteBuffer> buffers, boolean complete) {
Objects.requireNonNull(buffers);
if (complete) {
assert Utils.remaining(buffers) == 0;
boolean closing = closing();
if (debug.on())
debug.log("completionAcknowledged upstreamCompleted:%s,"
+ " downstreamCompleted:%s, closing:%s",
upstreamCompleted, downstreamCompleted, closing);
if (!upstreamCompleted && !closing) {
throw new IllegalStateException("upstream not completed");
}
completionAcknowledged = true;
} else {
if (debug.on())
debug.log("Adding %d to outputQ queue", Utils.remaining(buffers));
outputQ.add(buffers);
}
if (debug.on())
debug.log("pushScheduler" +(pushScheduler.isStopped() ? " is stopped!" : " is alive"));
pushScheduler.runOrSchedule();
}
/**
* Returns a CompletableFuture which completes when this wrapper completes.
* Normal completion happens with the following steps (in order):
* 1. onComplete() is called
* 2. incoming() called with complete = true
* 3. outgoing() may continue to be called normally
* 4. outgoing called with complete = true
* 5. downstream subscriber is called onComplete()
*
* If the subscription is canceled or onComplete() is invoked the
* CompletableFuture completes exceptionally. Exceptional completion
* also occurs if downstreamCF completes exceptionally.
*/
public CompletableFuture<Void> completion() {
return cf;
}
/**
* Invoked whenever it 'may' be possible to push buffers downstream.
*/
class DownstreamPusher implements Runnable {
@Override
public void run() {
try {
run1();
} catch (Throwable t) {
errorCommon(t);
}
}
private void run1() {
if (downstreamCompleted) {
if (debug.on())
debug.log("DownstreamPusher: downstream is already completed");
return;
}
switch (enterScheduling()) {
case CONTINUE: break;
case RESCHEDULE: pushScheduler.runOrSchedule(); return;
case RETURN: return;
default:
errorRef.compareAndSet(null,
new InternalError("unknown scheduling command"));
break;
}
// If there was an error, send it downstream.
Throwable error = errorRef.get();
if (error != null && outputQ.isEmpty()) {
synchronized(this) {
if (downstreamCompleted)
return;
downstreamCompleted = true;
}
if (debug.on())
debug.log("DownstreamPusher: forwarding error downstream: " + error);
pushScheduler.stop();
outputQ.clear();
downstreamSubscriber.onError(error);
return;
}
// OK - no error, let's proceed
if (!outputQ.isEmpty()) {
if (debug.on())
debug.log("DownstreamPusher: queue not empty, downstreamSubscription: %s",
downstreamSubscription);
} else {
if (debug.on())
debug.log("DownstreamPusher: queue empty, downstreamSubscription: %s",
downstreamSubscription);
}
while (!outputQ.isEmpty() && downstreamSubscription.tryDecrement()) {
List<ByteBuffer> b = outputQ.poll();
if (debug.on())
debug.log("DownstreamPusher: Pushing %d bytes downstream",
Utils.remaining(b));
downstreamSubscriber.onNext(b);
}
upstreamWindowUpdate();
checkCompletion();
}
}
void upstreamWindowUpdate() {
long downstreamQueueSize = outputQ.size();
long upstreamWindowSize = upstreamWindow.get();
long n = upstreamWindowUpdate(upstreamWindowSize, downstreamQueueSize);
if (debug.on())
debug.log("upstreamWindowUpdate, "
+ "downstreamQueueSize:%d, upstreamWindow:%d",
downstreamQueueSize, upstreamWindowSize);
if (n > 0)
upstreamRequest(n);
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
if (upstreamSubscription != null) {
throw new IllegalStateException("Single shot publisher");
}
this.upstreamSubscription = subscription;
upstreamRequest(upstreamWindowUpdate(0, 0));
if (debug.on())
debug.log("calling downstreamSubscriber::onSubscribe on %s",
downstreamSubscriber);
downstreamSubscriber.onSubscribe(downstreamSubscription);
onSubscribe();
}
@Override
public void onNext(List<ByteBuffer> item) {
if (debug.on()) debug.log("onNext");
long prev = upstreamWindow.getAndDecrement();
if (prev <= 0)
throw new IllegalStateException("invalid onNext call");
incomingCaller(item, false);
upstreamWindowUpdate();
}
private void upstreamRequest(long n) {
if (debug.on()) debug.log("requesting %d", n);
upstreamWindow.getAndAdd(n);
upstreamSubscription.request(n);
}
protected void requestMore() {
if (upstreamWindow.get() == 0) {
upstreamRequest(1);
}
}
public long upstreamWindow() {
return upstreamWindow.get();
}
@Override
public void onError(Throwable throwable) {
if (debug.on()) debug.log("onError: " + throwable);
errorCommon(Objects.requireNonNull(throwable));
}
protected boolean errorCommon(Throwable throwable) {
assert throwable != null ||
(throwable = new AssertionError("null throwable")) != null;
if (errorRef.compareAndSet(null, throwable)) {
if (debug.on()) debug.log("error", throwable);
pushScheduler.runOrSchedule();
upstreamCompleted = true;
cf.completeExceptionally(throwable);
return true;
}
return false;
}
@Override
public void close() {
errorCommon(new RuntimeException("wrapper closed"));
}
public void close(Throwable t) {
errorCommon(t);
}
private void incomingCaller(List<ByteBuffer> l, boolean complete) {
try {
incoming(l, complete);
} catch(Throwable t) {
errorCommon(t);
}
}
@Override
public void onComplete() {
if (debug.on()) debug.log("upstream completed: " + toString());
upstreamCompleted = true;
incomingCaller(Utils.EMPTY_BB_LIST, true);
// pushScheduler will call checkCompletion()
pushScheduler.runOrSchedule();
}
/** Adds the given data to the input queue. */
public void addData(ByteBuffer l) {
if (upstreamSubscription == null) {
throw new IllegalStateException("can't add data before upstream subscriber subscribes");
}
incomingCaller(List.of(l), false);
}
void checkCompletion() {
if (downstreamCompleted || !upstreamCompleted) {
return;
}
if (!outputQ.isEmpty()) {
return;
}
if (errorRef.get() != null) {
pushScheduler.runOrSchedule();
return;
}
if (completionAcknowledged) {
if (debug.on()) debug.log("calling downstreamSubscriber.onComplete()");
downstreamSubscriber.onComplete();
// Fix me subscriber.onComplete.run();
downstreamCompleted = true;
cf.complete(null);
}
}
// called from the downstream Subscription.cancel()
void downstreamCompletion() {
upstreamSubscription.cancel();
cf.complete(null);
}
public void resetDownstreamDemand() {
downstreamSubscription.demand.reset();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("SubscriberWrapper:")
.append(" upstreamCompleted: ").append(Boolean.toString(upstreamCompleted))
.append(" upstreamWindow: ").append(upstreamWindow.toString())
.append(" downstreamCompleted: ").append(Boolean.toString(downstreamCompleted))
.append(" completionAcknowledged: ").append(Boolean.toString(completionAcknowledged))
.append(" outputQ size: ").append(Integer.toString(outputQ.size()))
//.append(" outputQ: ").append(outputQ.toString())
.append(" cf: ").append(cf.toString())
.append(" downstreamSubscription: ").append(downstreamSubscription.toString());
return sb.toString();
}
public String dbgString() {
return "SubscriberWrapper";
}
}

View file

@ -0,0 +1,101 @@
/*
* Copyright (c) 2017, 2018, 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.common;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
/**
* Maintains subscription counter and provides primitives for:
* - accessing window
* - reducing window when delivering items externally
* - resume delivery when window was zero previously
*/
public class SubscriptionBase implements Flow.Subscription {
final Demand demand = new Demand();
final SequentialScheduler scheduler; // when window was zero and is opened, run this
final Runnable cancelAction; // when subscription cancelled, run this
final AtomicBoolean cancelled;
final Consumer<Throwable> onError;
public SubscriptionBase(SequentialScheduler scheduler, Runnable cancelAction) {
this(scheduler, cancelAction, null);
}
public SubscriptionBase(SequentialScheduler scheduler,
Runnable cancelAction,
Consumer<Throwable> onError) {
this.scheduler = scheduler;
this.cancelAction = cancelAction;
this.cancelled = new AtomicBoolean(false);
this.onError = onError;
}
@Override
public void request(long n) {
try {
if (demand.increase(n))
scheduler.runOrSchedule();
} catch(Throwable t) {
if (onError != null) {
if (cancelled.getAndSet(true))
return;
onError.accept(t);
} else throw t;
}
}
@Override
public synchronized String toString() {
return "SubscriptionBase: window = " + demand.get() +
" cancelled = " + cancelled.toString();
}
/**
* Returns true if the window was reduced by 1. In that case
* items must be supplied to subscribers and the scheduler run
* externally. If the window could not be reduced by 1, then false
* is returned and the scheduler will run later when the window is updated.
*/
public boolean tryDecrement() {
return demand.tryDecrement();
}
public long window() {
return demand.get();
}
@Override
public void cancel() {
if (cancelled.getAndSet(true))
return;
scheduler.stop();
cancelAction.run();
}
}

View file

@ -0,0 +1,962 @@
/*
* Copyright (c) 2015, 2018, 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.common;
import sun.net.NetProperties;
import sun.net.util.IPAddressUtil;
import sun.net.www.HeaderParser;
import javax.net.ssl.ExtendedSSLSession;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSession;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.io.UnsupportedEncodingException;
import java.lang.System.Logger.Level;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLPermission;
import java.net.http.HttpHeaders;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
/**
* Miscellaneous utilities
*/
public final class Utils {
public static final boolean ASSERTIONSENABLED;
static {
boolean enabled = false;
assert enabled = true;
ASSERTIONSENABLED = enabled;
}
// public static final boolean TESTING;
// static {
// if (ASSERTIONSENABLED) {
// PrivilegedAction<String> action = () -> System.getProperty("test.src");
// TESTING = AccessController.doPrivileged(action) != null;
// } else TESTING = false;
// }
public static final boolean DEBUG = // Revisit: temporary dev flag.
getBooleanProperty(DebugLogger.HTTP_NAME, false);
public static final boolean DEBUG_WS = // Revisit: temporary dev flag.
getBooleanProperty(DebugLogger.WS_NAME, false);
public static final boolean DEBUG_HPACK = // Revisit: temporary dev flag.
getBooleanProperty(DebugLogger.HPACK_NAME, false);
public static final boolean TESTING = DEBUG;
public static final boolean isHostnameVerificationDisabled = // enabled by default
hostnameVerificationDisabledValue();
private static boolean hostnameVerificationDisabledValue() {
String prop = getProperty("jdk.internal.httpclient.disableHostnameVerification");
if (prop == null)
return false;
return prop.isEmpty() ? true : Boolean.parseBoolean(prop);
}
/**
* Allocated buffer size. Must never be higher than 16K. But can be lower
* if smaller allocation units preferred. HTTP/2 mandates that all
* implementations support frame payloads of at least 16K.
*/
private static final int DEFAULT_BUFSIZE = 16 * 1024;
public static final int BUFSIZE = getIntegerNetProperty(
"jdk.httpclient.bufsize", DEFAULT_BUFSIZE
);
private static final Set<String> DISALLOWED_HEADERS_SET;
static {
// A case insensitive TreeSet of strings.
TreeSet<String> treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
treeSet.addAll(Set.of("connection", "content-length",
"date", "expect", "from", "host", "origin",
"referer", "upgrade",
"via", "warning"));
DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet);
}
public static final Predicate<String>
ALLOWED_HEADERS = header -> !DISALLOWED_HEADERS_SET.contains(header);
public static final BiPredicate<String, List<String>> VALIDATE_USER_HEADER =
(name, lv) -> {
requireNonNull(name, "header name");
requireNonNull(lv, "header values");
if (!isValidName(name)) {
throw newIAE("invalid header name: \"%s\"", name);
}
if (!Utils.ALLOWED_HEADERS.test(name)) {
throw newIAE("restricted header name: \"%s\"", name);
}
for (String value : lv) {
requireNonNull(value, "header value");
if (!isValidValue(value)) {
throw newIAE("invalid header value for %s: \"%s\"", name, value);
}
}
return true;
};
private static final Predicate<String> IS_PROXY_HEADER = (k) ->
k != null && k.length() > 6 && "proxy-".equalsIgnoreCase(k.substring(0,6));
private static final Predicate<String> NO_PROXY_HEADER =
IS_PROXY_HEADER.negate();
private static final Predicate<String> ALL_HEADERS = (s) -> true;
private static final Set<String> PROXY_AUTH_DISABLED_SCHEMES;
private static final Set<String> PROXY_AUTH_TUNNEL_DISABLED_SCHEMES;
static {
String proxyAuthDisabled =
getNetProperty("jdk.http.auth.proxying.disabledSchemes");
String proxyAuthTunnelDisabled =
getNetProperty("jdk.http.auth.tunneling.disabledSchemes");
PROXY_AUTH_DISABLED_SCHEMES =
proxyAuthDisabled == null ? Set.of() :
Stream.of(proxyAuthDisabled.split(","))
.map(String::trim)
.filter((s) -> !s.isEmpty())
.collect(Collectors.toUnmodifiableSet());
PROXY_AUTH_TUNNEL_DISABLED_SCHEMES =
proxyAuthTunnelDisabled == null ? Set.of() :
Stream.of(proxyAuthTunnelDisabled.split(","))
.map(String::trim)
.filter((s) -> !s.isEmpty())
.collect(Collectors.toUnmodifiableSet());
}
private static final String WSPACES = " \t\r\n";
private static final boolean isAllowedForProxy(String name,
List<String> value,
Set<String> disabledSchemes,
Predicate<String> allowedKeys) {
if (!allowedKeys.test(name)) return false;
if (disabledSchemes.isEmpty()) return true;
if (name.equalsIgnoreCase("proxy-authorization")) {
if (value.isEmpty()) return false;
for (String scheme : disabledSchemes) {
int slen = scheme.length();
for (String v : value) {
int vlen = v.length();
if (vlen == slen) {
if (v.equalsIgnoreCase(scheme)) {
return false;
}
} else if (vlen > slen) {
if (v.substring(0,slen).equalsIgnoreCase(scheme)) {
int c = v.codePointAt(slen);
if (WSPACES.indexOf(c) > -1
|| Character.isSpaceChar(c)
|| Character.isWhitespace(c)) {
return false;
}
}
}
}
}
}
return true;
}
public static final BiPredicate<String, List<String>> PROXY_TUNNEL_FILTER =
(s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_TUNNEL_DISABLED_SCHEMES,
IS_PROXY_HEADER);
public static final BiPredicate<String, List<String>> PROXY_FILTER =
(s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_DISABLED_SCHEMES,
ALL_HEADERS);
public static final BiPredicate<String, List<String>> NO_PROXY_HEADERS_FILTER =
(n,v) -> Utils.NO_PROXY_HEADER.test(n);
public static boolean proxyHasDisabledSchemes(boolean tunnel) {
return tunnel ? ! PROXY_AUTH_TUNNEL_DISABLED_SCHEMES.isEmpty()
: ! PROXY_AUTH_DISABLED_SCHEMES.isEmpty();
}
public static IllegalArgumentException newIAE(String message, Object... args) {
return new IllegalArgumentException(format(message, args));
}
public static ByteBuffer getBuffer() {
return ByteBuffer.allocate(BUFSIZE);
}
public static Throwable getCompletionCause(Throwable x) {
if (!(x instanceof CompletionException)
&& !(x instanceof ExecutionException)) return x;
final Throwable cause = x.getCause();
if (cause == null) {
throw new InternalError("Unexpected null cause", x);
}
return cause;
}
public static IOException getIOException(Throwable t) {
if (t instanceof IOException) {
return (IOException) t;
}
Throwable cause = t.getCause();
if (cause != null) {
return getIOException(cause);
}
return new IOException(t);
}
private Utils() { }
/**
* Returns the security permissions required to connect to the proxy, or
* {@code null} if none is required or applicable.
*/
public static URLPermission permissionForProxy(InetSocketAddress proxyAddress) {
if (proxyAddress == null)
return null;
StringBuilder sb = new StringBuilder();
sb.append("socket://")
.append(proxyAddress.getHostString()).append(":")
.append(proxyAddress.getPort());
String urlString = sb.toString();
return new URLPermission(urlString, "CONNECT");
}
/**
* Returns the security permission required for the given details.
*/
public static URLPermission permissionForServer(URI uri,
String method,
Stream<String> headers) {
String urlString = new StringBuilder()
.append(uri.getScheme()).append("://")
.append(uri.getAuthority())
.append(uri.getPath()).toString();
StringBuilder actionStringBuilder = new StringBuilder(method);
String collected = headers.collect(joining(","));
if (!collected.isEmpty()) {
actionStringBuilder.append(":").append(collected);
}
return new URLPermission(urlString, actionStringBuilder.toString());
}
// ABNF primitives defined in RFC 7230
private static final boolean[] tchar = new boolean[256];
private static final boolean[] fieldvchar = new boolean[256];
static {
char[] allowedTokenChars =
("!#$%&'*+-.^_`|~0123456789" +
"abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray();
for (char c : allowedTokenChars) {
tchar[c] = true;
}
for (char c = 0x21; c < 0xFF; c++) {
fieldvchar[c] = true;
}
fieldvchar[0x7F] = false; // a little hole (DEL) in the range
}
/*
* Validates a RFC 7230 field-name.
*/
public static boolean isValidName(String token) {
for (int i = 0; i < token.length(); i++) {
char c = token.charAt(i);
if (c > 255 || !tchar[c]) {
return false;
}
}
return !token.isEmpty();
}
public static class ServerName {
ServerName(String name, boolean isLiteral) {
this.name = name;
this.isLiteral = isLiteral;
}
final String name;
final boolean isLiteral;
public String getName() {
return name;
}
public boolean isLiteral() {
return isLiteral;
}
}
/**
* Analyse the given address and determine if it is literal or not,
* returning the address in String form.
*/
public static ServerName getServerName(InetSocketAddress addr) {
String host = addr.getHostString();
byte[] literal = IPAddressUtil.textToNumericFormatV4(host);
if (literal == null) {
// not IPv4 literal. Check IPv6
literal = IPAddressUtil.textToNumericFormatV6(host);
return new ServerName(host, literal != null);
} else {
return new ServerName(host, true);
}
}
private static boolean isLoopbackLiteral(byte[] bytes) {
if (bytes.length == 4) {
return bytes[0] == 127;
} else if (bytes.length == 16) {
for (int i=0; i<14; i++)
if (bytes[i] != 0)
return false;
if (bytes[15] != 1)
return false;
return true;
} else
throw new InternalError();
}
/*
* Validates a RFC 7230 field-value.
*
* "Obsolete line folding" rule
*
* obs-fold = CRLF 1*( SP / HTAB )
*
* is not permitted!
*/
public static boolean isValidValue(String token) {
for (int i = 0; i < token.length(); i++) {
char c = token.charAt(i);
if (c > 255) {
return false;
}
if (c == ' ' || c == '\t') {
continue;
} else if (!fieldvchar[c]) {
return false; // forbidden byte
}
}
return true;
}
public static int getIntegerNetProperty(String name, int defaultValue) {
return AccessController.doPrivileged((PrivilegedAction<Integer>) () ->
NetProperties.getInteger(name, defaultValue));
}
public static String getNetProperty(String name) {
return AccessController.doPrivileged((PrivilegedAction<String>) () ->
NetProperties.get(name));
}
public static boolean getBooleanProperty(String name, boolean def) {
return AccessController.doPrivileged((PrivilegedAction<Boolean>) () ->
Boolean.parseBoolean(System.getProperty(name, String.valueOf(def))));
}
public static String getProperty(String name) {
return AccessController.doPrivileged((PrivilegedAction<String>) () ->
System.getProperty(name));
}
public static int getIntegerProperty(String name, int defaultValue) {
return AccessController.doPrivileged((PrivilegedAction<Integer>) () ->
Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue))));
}
public static SSLParameters copySSLParameters(SSLParameters p) {
SSLParameters p1 = new SSLParameters();
p1.setAlgorithmConstraints(p.getAlgorithmConstraints());
p1.setCipherSuites(p.getCipherSuites());
// JDK 8 EXCL START
p1.setEnableRetransmissions(p.getEnableRetransmissions());
p1.setMaximumPacketSize(p.getMaximumPacketSize());
// JDK 8 EXCL END
p1.setEndpointIdentificationAlgorithm(p.getEndpointIdentificationAlgorithm());
p1.setNeedClientAuth(p.getNeedClientAuth());
String[] protocols = p.getProtocols();
if (protocols != null) {
p1.setProtocols(protocols.clone());
}
p1.setSNIMatchers(p.getSNIMatchers());
p1.setServerNames(p.getServerNames());
p1.setUseCipherSuitesOrder(p.getUseCipherSuitesOrder());
p1.setWantClientAuth(p.getWantClientAuth());
return p1;
}
/**
* Set limit to position, and position to mark.
*/
public static void flipToMark(ByteBuffer buffer, int mark) {
buffer.limit(buffer.position());
buffer.position(mark);
}
public static String stackTrace(Throwable t) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
String s = null;
try {
PrintStream p = new PrintStream(bos, true, "US-ASCII");
t.printStackTrace(p);
s = bos.toString("US-ASCII");
} catch (UnsupportedEncodingException ex) {
throw new InternalError(ex); // Can't happen
}
return s;
}
/**
* Copies as much of src to dst as possible.
* Return number of bytes copied
*/
public static int copy(ByteBuffer src, ByteBuffer dst) {
int srcLen = src.remaining();
int dstLen = dst.remaining();
if (srcLen > dstLen) {
int diff = srcLen - dstLen;
int limit = src.limit();
src.limit(limit - diff);
dst.put(src);
src.limit(limit);
} else {
dst.put(src);
}
return srcLen - src.remaining();
}
/** Threshold beyond which data is no longer copied into the current
* buffer, if that buffer has enough unused space. */
private static final int COPY_THRESHOLD = 8192;
/**
* Adds the data from buffersToAdd to currentList. Either 1) appends the
* data from a particular buffer to the last buffer in the list ( if
* there is enough unused space ), or 2) adds it to the list.
*
* @return the number of bytes added
*/
public static long accumulateBuffers(List<ByteBuffer> currentList,
List<ByteBuffer> buffersToAdd) {
long accumulatedBytes = 0;
for (ByteBuffer bufferToAdd : buffersToAdd) {
int remaining = bufferToAdd.remaining();
if (remaining <= 0)
continue;
int listSize = currentList.size();
if (listSize == 0) {
currentList.add(bufferToAdd);
accumulatedBytes = remaining;
continue;
}
ByteBuffer lastBuffer = currentList.get(listSize - 1);
int freeSpace = lastBuffer.capacity() - lastBuffer.limit();
if (remaining <= COPY_THRESHOLD && freeSpace >= remaining) {
// append the new data to the unused space in the last buffer
int position = lastBuffer.position();
int limit = lastBuffer.limit();
lastBuffer.position(limit);
lastBuffer.limit(limit + remaining);
lastBuffer.put(bufferToAdd);
lastBuffer.position(position);
} else {
currentList.add(bufferToAdd);
}
accumulatedBytes += remaining;
}
return accumulatedBytes;
}
public static ByteBuffer copy(ByteBuffer src) {
ByteBuffer dst = ByteBuffer.allocate(src.remaining());
dst.put(src);
dst.flip();
return dst;
}
public static String dump(Object... objects) {
return Arrays.toString(objects);
}
public static String stringOf(Collection<?> source) {
// We don't know anything about toString implementation of this
// collection, so let's create an array
return Arrays.toString(source.toArray());
}
public static long remaining(ByteBuffer[] bufs) {
long remain = 0;
for (ByteBuffer buf : bufs) {
remain += buf.remaining();
}
return remain;
}
public static boolean hasRemaining(List<ByteBuffer> bufs) {
synchronized (bufs) {
for (ByteBuffer buf : bufs) {
if (buf.hasRemaining())
return true;
}
}
return false;
}
public static long remaining(List<ByteBuffer> bufs) {
long remain = 0;
synchronized (bufs) {
for (ByteBuffer buf : bufs) {
remain += buf.remaining();
}
}
return remain;
}
public static int remaining(List<ByteBuffer> bufs, int max) {
long remain = 0;
synchronized (bufs) {
for (ByteBuffer buf : bufs) {
remain += buf.remaining();
if (remain > max) {
throw new IllegalArgumentException("too many bytes");
}
}
}
return (int) remain;
}
public static long remaining(ByteBufferReference[] refs) {
long remain = 0;
for (ByteBufferReference ref : refs) {
remain += ref.get().remaining();
}
return remain;
}
public static int remaining(ByteBufferReference[] refs, int max) {
long remain = 0;
for (ByteBufferReference ref : refs) {
remain += ref.get().remaining();
if (remain > max) {
throw new IllegalArgumentException("too many bytes");
}
}
return (int) remain;
}
public static int remaining(ByteBuffer[] refs, int max) {
long remain = 0;
for (ByteBuffer b : refs) {
remain += b.remaining();
if (remain > max) {
throw new IllegalArgumentException("too many bytes");
}
}
return (int) remain;
}
public static void close(Closeable... closeables) {
for (Closeable c : closeables) {
try {
c.close();
} catch (IOException ignored) { }
}
}
// Put all these static 'empty' singletons here
public static final ByteBuffer EMPTY_BYTEBUFFER = ByteBuffer.allocate(0);
public static final ByteBuffer[] EMPTY_BB_ARRAY = new ByteBuffer[0];
public static final List<ByteBuffer> EMPTY_BB_LIST = List.of();
public static final ByteBufferReference[] EMPTY_BBR_ARRAY = new ByteBufferReference[0];
/**
* Returns a slice of size {@code amount} from the given buffer. If the
* buffer contains more data than {@code amount}, then the slice's capacity
* ( and, but not just, its limit ) is set to {@code amount}. If the buffer
* does not contain more data than {@code amount}, then the slice's capacity
* will be the same as the given buffer's capacity.
*/
public static ByteBuffer sliceWithLimitedCapacity(ByteBuffer buffer, int amount) {
final int index = buffer.position() + amount;
final int limit = buffer.limit();
if (index != limit) {
// additional data in the buffer
buffer.limit(index); // ensures that the slice does not go beyond
} else {
// no additional data in the buffer
buffer.limit(buffer.capacity()); // allows the slice full capacity
}
ByteBuffer newb = buffer.slice();
buffer.position(index);
buffer.limit(limit); // restore the original buffer's limit
newb.limit(amount); // slices limit to amount (capacity may be greater)
return newb;
}
/**
* Get the Charset from the Content-encoding header. Defaults to
* UTF_8
*/
public static Charset charsetFrom(HttpHeaders headers) {
String type = headers.firstValue("Content-type")
.orElse("text/html; charset=utf-8");
int i = type.indexOf(";");
if (i >= 0) type = type.substring(i+1);
try {
HeaderParser parser = new HeaderParser(type);
String value = parser.findValue("charset");
if (value == null) return StandardCharsets.UTF_8;
return Charset.forName(value);
} catch (Throwable x) {
Log.logTrace("Can't find charset in \"{0}\" ({1})", type, x);
return StandardCharsets.UTF_8;
}
}
public static UncheckedIOException unchecked(IOException e) {
return new UncheckedIOException(e);
}
/**
* Get a logger for debug HTTP traces.
*
* The logger should only be used with levels whose severity is
* {@code <= DEBUG}. By default, this logger will forward all messages
* logged to an internal logger named "jdk.internal.httpclient.debug".
* In addition, if the property -Djdk.internal.httpclient.debug=true is set,
* it will print the messages on stderr.
* The logger will add some decoration to the printed message, in the form of
* {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
*
* @param dbgTag A lambda that returns a string that identifies the caller
* (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
*
* @return A logger for HTTP internal debug traces
*/
public static Logger getDebugLogger(Supplier<String> dbgTag) {
return getDebugLogger(dbgTag, DEBUG);
}
/**
* Get a logger for debug HTTP traces.The logger should only be used
* with levels whose severity is {@code <= DEBUG}.
*
* By default, this logger will forward all messages logged to an internal
* logger named "jdk.internal.httpclient.debug".
* In addition, if the message severity level is >= to
* the provided {@code errLevel} it will print the messages on stderr.
* The logger will add some decoration to the printed message, in the form of
* {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
*
* @apiNote To obtain a logger that will always print things on stderr in
* addition to forwarding to the internal logger, use
* {@code getDebugLogger(this::dbgTag, Level.ALL);}.
* This is also equivalent to calling
* {@code getDebugLogger(this::dbgTag, true);}.
* To obtain a logger that will only forward to the internal logger,
* use {@code getDebugLogger(this::dbgTag, Level.OFF);}.
* This is also equivalent to calling
* {@code getDebugLogger(this::dbgTag, false);}.
*
* @param dbgTag A lambda that returns a string that identifies the caller
* (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
* @param errLevel The level above which messages will be also printed on
* stderr (in addition to be forwarded to the internal logger).
*
* @return A logger for HTTP internal debug traces
*/
static Logger getDebugLogger(Supplier<String> dbgTag, Level errLevel) {
return DebugLogger.createHttpLogger(dbgTag, Level.OFF, errLevel);
}
/**
* Get a logger for debug HTTP traces.The logger should only be used
* with levels whose severity is {@code <= DEBUG}.
*
* By default, this logger will forward all messages logged to an internal
* logger named "jdk.internal.httpclient.debug".
* In addition, the provided boolean {@code on==true}, it will print the
* messages on stderr.
* The logger will add some decoration to the printed message, in the form of
* {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
*
* @apiNote To obtain a logger that will always print things on stderr in
* addition to forwarding to the internal logger, use
* {@code getDebugLogger(this::dbgTag, true);}.
* This is also equivalent to calling
* {@code getDebugLogger(this::dbgTag, Level.ALL);}.
* To obtain a logger that will only forward to the internal logger,
* use {@code getDebugLogger(this::dbgTag, false);}.
* This is also equivalent to calling
* {@code getDebugLogger(this::dbgTag, Level.OFF);}.
*
* @param dbgTag A lambda that returns a string that identifies the caller
* (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
* @param on Whether messages should also be printed on
* stderr (in addition to be forwarded to the internal logger).
*
* @return A logger for HTTP internal debug traces
*/
public static Logger getDebugLogger(Supplier<String> dbgTag, boolean on) {
Level errLevel = on ? Level.ALL : Level.OFF;
return getDebugLogger(dbgTag, errLevel);
}
/**
* Get a logger for debug HPACK traces.The logger should only be used
* with levels whose severity is {@code <= DEBUG}.
*
* By default, this logger will forward all messages logged to an internal
* logger named "jdk.internal.httpclient.hpack.debug".
* In addition, if the message severity level is >= to
* the provided {@code errLevel} it will print the messages on stderr.
* The logger will add some decoration to the printed message, in the form of
* {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
*
* @apiNote To obtain a logger that will always print things on stderr in
* addition to forwarding to the internal logger, use
* {@code getHpackLogger(this::dbgTag, Level.ALL);}.
* This is also equivalent to calling
* {@code getHpackLogger(this::dbgTag, true);}.
* To obtain a logger that will only forward to the internal logger,
* use {@code getHpackLogger(this::dbgTag, Level.OFF);}.
* This is also equivalent to calling
* {@code getHpackLogger(this::dbgTag, false);}.
*
* @param dbgTag A lambda that returns a string that identifies the caller
* (e.g: "Http2Connection(SocketTube(3))/hpack.Decoder(3)")
* @param errLevel The level above which messages will be also printed on
* stderr (in addition to be forwarded to the internal logger).
*
* @return A logger for HPACK internal debug traces
*/
public static Logger getHpackLogger(Supplier<String> dbgTag, Level errLevel) {
Level outLevel = Level.OFF;
return DebugLogger.createHpackLogger(dbgTag, outLevel, errLevel);
}
/**
* Get a logger for debug HPACK traces.The logger should only be used
* with levels whose severity is {@code <= DEBUG}.
*
* By default, this logger will forward all messages logged to an internal
* logger named "jdk.internal.httpclient.hpack.debug".
* In addition, the provided boolean {@code on==true}, it will print the
* messages on stderr.
* The logger will add some decoration to the printed message, in the form of
* {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
*
* @apiNote To obtain a logger that will always print things on stderr in
* addition to forwarding to the internal logger, use
* {@code getHpackLogger(this::dbgTag, true);}.
* This is also equivalent to calling
* {@code getHpackLogger(this::dbgTag, Level.ALL);}.
* To obtain a logger that will only forward to the internal logger,
* use {@code getHpackLogger(this::dbgTag, false);}.
* This is also equivalent to calling
* {@code getHpackLogger(this::dbgTag, Level.OFF);}.
*
* @param dbgTag A lambda that returns a string that identifies the caller
* (e.g: "Http2Connection(SocketTube(3))/hpack.Decoder(3)")
* @param on Whether messages should also be printed on
* stderr (in addition to be forwarded to the internal logger).
*
* @return A logger for HPACK internal debug traces
*/
public static Logger getHpackLogger(Supplier<String> dbgTag, boolean on) {
Level errLevel = on ? Level.ALL : Level.OFF;
return getHpackLogger(dbgTag, errLevel);
}
/**
* Get a logger for debug WebSocket traces.The logger should only be used
* with levels whose severity is {@code <= DEBUG}.
*
* By default, this logger will forward all messages logged to an internal
* logger named "jdk.internal.httpclient.websocket.debug".
* In addition, if the message severity level is >= to
* the provided {@code errLevel} it will print the messages on stderr.
* The logger will add some decoration to the printed message, in the form of
* {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
*
* @apiNote To obtain a logger that will always print things on stderr in
* addition to forwarding to the internal logger, use
* {@code getWebSocketLogger(this::dbgTag, Level.ALL);}.
* This is also equivalent to calling
* {@code getWSLogger(this::dbgTag, true);}.
* To obtain a logger that will only forward to the internal logger,
* use {@code getWebSocketLogger(this::dbgTag, Level.OFF);}.
* This is also equivalent to calling
* {@code getWSLogger(this::dbgTag, false);}.
*
* @param dbgTag A lambda that returns a string that identifies the caller
* (e.g: "WebSocket(3)")
* @param errLevel The level above which messages will be also printed on
* stderr (in addition to be forwarded to the internal logger).
*
* @return A logger for HPACK internal debug traces
*/
public static Logger getWebSocketLogger(Supplier<String> dbgTag, Level errLevel) {
Level outLevel = Level.OFF;
return DebugLogger.createWebSocketLogger(dbgTag, outLevel, errLevel);
}
/**
* Get a logger for debug WebSocket traces.The logger should only be used
* with levels whose severity is {@code <= DEBUG}.
*
* By default, this logger will forward all messages logged to an internal
* logger named "jdk.internal.httpclient.websocket.debug".
* In addition, the provided boolean {@code on==true}, it will print the
* messages on stderr.
* The logger will add some decoration to the printed message, in the form of
* {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
*
* @apiNote To obtain a logger that will always print things on stderr in
* addition to forwarding to the internal logger, use
* {@code getWebSocketLogger(this::dbgTag, true);}.
* This is also equivalent to calling
* {@code getWebSocketLogger(this::dbgTag, Level.ALL);}.
* To obtain a logger that will only forward to the internal logger,
* use {@code getWebSocketLogger(this::dbgTag, false);}.
* This is also equivalent to calling
* {@code getHpackLogger(this::dbgTag, Level.OFF);}.
*
* @param dbgTag A lambda that returns a string that identifies the caller
* (e.g: "WebSocket(3)")
* @param on Whether messages should also be printed on
* stderr (in addition to be forwarded to the internal logger).
*
* @return A logger for WebSocket internal debug traces
*/
public static Logger getWebSocketLogger(Supplier<String> dbgTag, boolean on) {
Level errLevel = on ? Level.ALL : Level.OFF;
return getWebSocketLogger(dbgTag, errLevel);
}
/**
* SSLSessions returned to user are wrapped in an immutable object
*/
public static SSLSession immutableSession(SSLSession session) {
if (session instanceof ExtendedSSLSession)
return new ImmutableExtendedSSLSession((ExtendedSSLSession)session);
else
return new ImmutableSSLSession(session);
}
/**
* Enabled by default. May be disabled for testing. Use with care
*/
public static boolean isHostnameVerificationDisabled() {
return isHostnameVerificationDisabled;
}
public static InetSocketAddress resolveAddress(InetSocketAddress address) {
if (address != null && address.isUnresolved()) {
// The default proxy selector may select a proxy whose address is
// unresolved. We must resolve the address before connecting to it.
address = new InetSocketAddress(address.getHostString(), address.getPort());
}
return address;
}
/**
* Returns the smallest (closest to zero) positive number {@code m} (which
* is also a power of 2) such that {@code n <= m}.
* <pre>{@code
* n pow2Size(n)
* -----------------------
* 0 1
* 1 1
* 2 2
* 3 4
* 4 4
* 5 8
* 6 8
* 7 8
* 8 8
* 9 16
* 10 16
* ... ...
* 2147483647 1073741824
* } </pre>
*
* The result is capped at {@code 1 << 30} as beyond that int wraps.
*
* @param n
* capacity
*
* @return the size of the array
* @apiNote Used to size arrays in circular buffers (rings), usually in
* order to squeeze extra performance substituting {@code %} operation for
* {@code &}, which is up to 2 times faster.
*/
public static int pow2Size(int n) {
if (n < 0) {
throw new IllegalArgumentException();
} else if (n == 0) {
return 1;
} else if (n >= (1 << 30)) { // 2^31 is a negative int
return 1 << 30;
} else {
return 1 << (32 - Integer.numberOfLeadingZeros(n - 1));
}
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
import java.nio.ByteBuffer;
import java.util.List;
public class ContinuationFrame extends HeaderFrame {
public static final int TYPE = 0x9;
public ContinuationFrame(int streamid, int flags, List<ByteBuffer> headerBlocks) {
super(streamid, flags, headerBlocks);
}
public ContinuationFrame(int streamid, ByteBuffer headersBlock) {
this(streamid, 0, List.of(headersBlock));
}
@Override
public int type() {
return TYPE;
}
@Override
int length() {
return headerLength;
}
}

View file

@ -0,0 +1,112 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
import jdk.internal.net.http.common.Utils;
import java.nio.ByteBuffer;
import java.util.List;
public class DataFrame extends Http2Frame {
public static final int TYPE = 0x0;
// Flags
public static final int END_STREAM = 0x1;
public static final int PADDED = 0x8;
private int padLength;
private final List<ByteBuffer> data;
private final int dataLength;
public DataFrame(int streamid, int flags, ByteBuffer data) {
this(streamid, flags, List.of(data));
}
public DataFrame(int streamid, int flags, List<ByteBuffer> data) {
super(streamid, flags);
this.data = data;
this.dataLength = Utils.remaining(data, Integer.MAX_VALUE);
}
public DataFrame(int streamid, int flags, List<ByteBuffer> data, int padLength) {
this(streamid, flags, data);
if (padLength > 0) {
setPadLength(padLength);
}
}
@Override
public int type() {
return TYPE;
}
@Override
int length() {
return dataLength + (((flags & PADDED) != 0) ? (padLength + 1) : 0);
}
@Override
public String flagAsString(int flag) {
switch (flag) {
case END_STREAM:
return "END_STREAM";
case PADDED:
return "PADDED";
}
return super.flagAsString(flag);
}
public List<ByteBuffer> getData() {
return data;
}
public int getDataLength() {
return dataLength;
}
int getPadLength() {
return padLength;
}
public void setPadLength(int padLength) {
this.padLength = padLength;
flags |= PADDED;
}
public int payloadLength() {
// RFC 7540 6.1:
// The entire DATA frame payload is included in flow control,
// including the Pad Length and Padding fields if present
if ((flags & PADDED) != 0) {
return dataLength + (1 + padLength);
} else {
return dataLength;
}
}
}

View file

@ -0,0 +1,91 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
public abstract class ErrorFrame extends Http2Frame {
// error codes
public static final int NO_ERROR = 0x0;
public static final int PROTOCOL_ERROR = 0x1;
public static final int INTERNAL_ERROR = 0x2;
public static final int FLOW_CONTROL_ERROR = 0x3;
public static final int SETTINGS_TIMEOUT = 0x4;
public static final int STREAM_CLOSED = 0x5;
public static final int FRAME_SIZE_ERROR = 0x6;
public static final int REFUSED_STREAM = 0x7;
public static final int CANCEL = 0x8;
public static final int COMPRESSION_ERROR = 0x9;
public static final int CONNECT_ERROR = 0xa;
public static final int ENHANCE_YOUR_CALM = 0xb;
public static final int INADEQUATE_SECURITY = 0xc;
public static final int HTTP_1_1_REQUIRED = 0xd;
static final int LAST_ERROR = 0xd;
static final String[] errorStrings = {
"Not an error",
"Protocol error",
"Internal error",
"Flow control error",
"Settings timeout",
"Stream is closed",
"Frame size error",
"Stream not processed",
"Stream cancelled",
"Compression state not updated",
"TCP Connection error on CONNECT",
"Processing capacity exceeded",
"Negotiated TLS parameters not acceptable",
"Use HTTP/1.1 for request"
};
public static String stringForCode(int code) {
if (code < 0) {
throw new IllegalArgumentException();
}
if (code > LAST_ERROR) {
return "Error: " + Integer.toString(code);
} else {
return errorStrings[code];
}
}
int errorCode;
public ErrorFrame(int streamid, int flags, int errorCode) {
super(streamid, flags);
this.errorCode = errorCode;
}
@Override
public String toString() {
return super.toString() + " Error: " + stringForCode(errorCode);
}
public int getErrorCode() {
return this.errorCode;
}
}

View file

@ -0,0 +1,545 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
import java.io.IOException;
import java.lang.System.Logger.Level;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.Utils;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Frames Decoder
* <p>
* collect buffers until frame decoding is possible,
* all decoded frames are passed to the FrameProcessor callback in order of decoding.
*
* It's a stateful class due to the fact that FramesDecoder stores buffers inside.
* Should be allocated only the single instance per connection.
*/
public class FramesDecoder {
static final Logger debug =
Utils.getDebugLogger("FramesDecoder"::toString, Utils.DEBUG);
@FunctionalInterface
public interface FrameProcessor {
void processFrame(Http2Frame frame) throws IOException;
}
private final FrameProcessor frameProcessor;
private final int maxFrameSize;
private ByteBuffer currentBuffer; // current buffer either null or hasRemaining
private final ArrayDeque<ByteBuffer> tailBuffers = new ArrayDeque<>();
private int tailSize = 0;
private boolean slicedToDataFrame = false;
private final List<ByteBuffer> prepareToRelease = new ArrayList<>();
// if true - Frame Header was parsed (9 bytes consumed) and subsequent fields have meaning
// otherwise - stopped at frames boundary
private boolean frameHeaderParsed = false;
private int frameLength;
private int frameType;
private int frameFlags;
private int frameStreamid;
private boolean closed;
/**
* Creates Frame Decoder
*
* @param frameProcessor - callback for decoded frames
*/
public FramesDecoder(FrameProcessor frameProcessor) {
this(frameProcessor, 16 * 1024);
}
/**
* Creates Frame Decoder
* @param frameProcessor - callback for decoded frames
* @param maxFrameSize - maxFrameSize accepted by this decoder
*/
public FramesDecoder(FrameProcessor frameProcessor, int maxFrameSize) {
this.frameProcessor = frameProcessor;
this.maxFrameSize = Math.min(Math.max(16 * 1024, maxFrameSize), 16 * 1024 * 1024 - 1);
}
/** Threshold beyond which data is no longer copied into the current buffer,
* if that buffer has enough unused space. */
private static final int COPY_THRESHOLD = 8192;
/**
* Adds the data from the given buffer, and performs frame decoding if
* possible. Either 1) appends the data from the given buffer to the
* current buffer ( if there is enough unused space ), or 2) adds it to the
* next buffer in the queue.
*
* If there is enough data to perform frame decoding then, all buffers are
* decoded and the FrameProcessor is invoked.
*/
public void decode(ByteBuffer inBoundBuffer) throws IOException {
if (closed) {
if (debug.on())
debug.log("closed: ignoring buffer (%s bytes)",
inBoundBuffer.remaining());
inBoundBuffer.position(inBoundBuffer.limit());
return;
}
int remaining = inBoundBuffer.remaining();
if (debug.on()) debug.log("decodes: %d", remaining);
if (remaining > 0) {
if (currentBuffer == null) {
currentBuffer = inBoundBuffer;
} else {
ByteBuffer b = currentBuffer;
if (!tailBuffers.isEmpty()) {
b = tailBuffers.getLast();
}
int limit = b.limit();
int freeSpace = b.capacity() - limit;
if (remaining <= COPY_THRESHOLD && freeSpace >= remaining) {
// append the new data to the unused space in the current buffer
int position = b.position();
b.position(limit);
b.limit(limit + inBoundBuffer.remaining());
b.put(inBoundBuffer);
b.position(position);
if (b != currentBuffer)
tailSize += remaining;
if (debug.on()) debug.log("copied: %d", remaining);
} else {
if (debug.on()) debug.log("added: %d", remaining);
tailBuffers.add(inBoundBuffer);
tailSize += remaining;
}
}
}
if (debug.on())
debug.log("Tail size is now: %d, current=", tailSize,
(currentBuffer == null ? 0 : currentBuffer.remaining()));
Http2Frame frame;
while ((frame = nextFrame()) != null) {
if (debug.on()) debug.log("Got frame: %s", frame);
frameProcessor.processFrame(frame);
frameProcessed();
}
}
private Http2Frame nextFrame() throws IOException {
while (true) {
if (currentBuffer == null) {
return null; // no data at all
}
long available = currentBuffer.remaining() + tailSize;
if (!frameHeaderParsed) {
if (available >= Http2Frame.FRAME_HEADER_SIZE) {
parseFrameHeader();
if (frameLength > maxFrameSize) {
// connection error
return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
"Frame type("+frameType+") "
+"length("+frameLength
+") exceeds MAX_FRAME_SIZE("
+ maxFrameSize+")");
}
frameHeaderParsed = true;
} else {
if (debug.on())
debug.log("Not enough data to parse header, needs: %d, has: %d",
Http2Frame.FRAME_HEADER_SIZE, available);
return null;
}
}
available = currentBuffer == null ? 0 : currentBuffer.remaining() + tailSize;
if ((frameLength == 0) ||
(currentBuffer != null && available >= frameLength)) {
Http2Frame frame = parseFrameBody();
frameHeaderParsed = false;
// frame == null means we have to skip this frame and try parse next
if (frame != null) {
return frame;
}
} else {
if (debug.on())
debug.log("Not enough data to parse frame body, needs: %d, has: %d",
frameLength, available);
return null; // no data for the whole frame header
}
}
}
private void frameProcessed() {
prepareToRelease.clear();
}
private void parseFrameHeader() throws IOException {
int x = getInt();
this.frameLength = (x >>> 8) & 0x00ffffff;
this.frameType = x & 0xff;
this.frameFlags = getByte();
this.frameStreamid = getInt() & 0x7fffffff;
// R: A reserved 1-bit field. The semantics of this bit are undefined,
// MUST be ignored when receiving.
}
// move next buffer from tailBuffers to currentBuffer if required
private void nextBuffer() {
if (!currentBuffer.hasRemaining()) {
if (!slicedToDataFrame) {
prepareToRelease.add(currentBuffer);
}
slicedToDataFrame = false;
currentBuffer = tailBuffers.poll();
if (currentBuffer != null) {
tailSize -= currentBuffer.remaining();
}
}
}
public int getByte() {
int res = currentBuffer.get() & 0xff;
nextBuffer();
return res;
}
public int getShort() {
if (currentBuffer.remaining() >= 2) {
int res = currentBuffer.getShort() & 0xffff;
nextBuffer();
return res;
}
int val = getByte();
val = (val << 8) + getByte();
return val;
}
public int getInt() {
if (currentBuffer.remaining() >= 4) {
int res = currentBuffer.getInt();
nextBuffer();
return res;
}
int val = getByte();
val = (val << 8) + getByte();
val = (val << 8) + getByte();
val = (val << 8) + getByte();
return val;
}
public byte[] getBytes(int n) {
byte[] bytes = new byte[n];
int offset = 0;
while (n > 0) {
int length = Math.min(n, currentBuffer.remaining());
currentBuffer.get(bytes, offset, length);
offset += length;
n -= length;
nextBuffer();
}
return bytes;
}
private List<ByteBuffer> getBuffers(boolean isDataFrame, int bytecount) {
List<ByteBuffer> res = new ArrayList<>();
while (bytecount > 0) {
int remaining = currentBuffer.remaining();
int extract = Math.min(remaining, bytecount);
ByteBuffer extractedBuf;
if (isDataFrame) {
extractedBuf = Utils.sliceWithLimitedCapacity(currentBuffer, extract)
.asReadOnlyBuffer();
slicedToDataFrame = true;
} else {
// Header frames here
// HPACK decoding should performed under lock and immediately after frame decoding.
// in that case it is safe to release original buffer,
// because of sliced buffer has a very short life
extractedBuf = Utils.sliceWithLimitedCapacity(currentBuffer, extract);
}
res.add(extractedBuf);
bytecount -= extract;
nextBuffer();
}
return res;
}
public void close(String msg) {
closed = true;
tailBuffers.clear();
int bytes = tailSize;
ByteBuffer b = currentBuffer;
if (b != null) {
bytes += b.remaining();
b.position(b.limit());
}
tailSize = 0;
currentBuffer = null;
if (debug.on()) debug.log("closed %s, ignoring %d bytes", msg, bytes);
}
public void skipBytes(int bytecount) {
while (bytecount > 0) {
int remaining = currentBuffer.remaining();
int extract = Math.min(remaining, bytecount);
currentBuffer.position(currentBuffer.position() + extract);
bytecount -= remaining;
nextBuffer();
}
}
private Http2Frame parseFrameBody() throws IOException {
assert frameHeaderParsed;
switch (frameType) {
case DataFrame.TYPE:
return parseDataFrame(frameLength, frameStreamid, frameFlags);
case HeadersFrame.TYPE:
return parseHeadersFrame(frameLength, frameStreamid, frameFlags);
case PriorityFrame.TYPE:
return parsePriorityFrame(frameLength, frameStreamid, frameFlags);
case ResetFrame.TYPE:
return parseResetFrame(frameLength, frameStreamid, frameFlags);
case SettingsFrame.TYPE:
return parseSettingsFrame(frameLength, frameStreamid, frameFlags);
case PushPromiseFrame.TYPE:
return parsePushPromiseFrame(frameLength, frameStreamid, frameFlags);
case PingFrame.TYPE:
return parsePingFrame(frameLength, frameStreamid, frameFlags);
case GoAwayFrame.TYPE:
return parseGoAwayFrame(frameLength, frameStreamid, frameFlags);
case WindowUpdateFrame.TYPE:
return parseWindowUpdateFrame(frameLength, frameStreamid, frameFlags);
case ContinuationFrame.TYPE:
return parseContinuationFrame(frameLength, frameStreamid, frameFlags);
default:
// RFC 7540 4.1
// Implementations MUST ignore and discard any frame that has a type that is unknown.
Log.logTrace("Unknown incoming frame type: {0}", frameType);
skipBytes(frameLength);
return null;
}
}
private Http2Frame parseDataFrame(int frameLength, int streamid, int flags) {
// non-zero stream
if (streamid == 0) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
"zero streamId for DataFrame");
}
int padLength = 0;
if ((flags & DataFrame.PADDED) != 0) {
padLength = getByte();
if (padLength >= frameLength) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
"the length of the padding is the length of the frame payload or greater");
}
frameLength--;
}
DataFrame df = new DataFrame(streamid, flags,
getBuffers(true, frameLength - padLength), padLength);
skipBytes(padLength);
return df;
}
private Http2Frame parseHeadersFrame(int frameLength, int streamid, int flags) {
// non-zero stream
if (streamid == 0) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
"zero streamId for HeadersFrame");
}
int padLength = 0;
if ((flags & HeadersFrame.PADDED) != 0) {
padLength = getByte();
frameLength--;
}
boolean hasPriority = (flags & HeadersFrame.PRIORITY) != 0;
boolean exclusive = false;
int streamDependency = 0;
int weight = 0;
if (hasPriority) {
int x = getInt();
exclusive = (x & 0x80000000) != 0;
streamDependency = x & 0x7fffffff;
weight = getByte();
frameLength -= 5;
}
if(frameLength < padLength) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
"Padding exceeds the size remaining for the header block");
}
HeadersFrame hf = new HeadersFrame(streamid, flags,
getBuffers(false, frameLength - padLength), padLength);
skipBytes(padLength);
if (hasPriority) {
hf.setPriority(streamDependency, exclusive, weight);
}
return hf;
}
private Http2Frame parsePriorityFrame(int frameLength, int streamid, int flags) {
// non-zero stream; no flags
if (streamid == 0) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
"zero streamId for PriorityFrame");
}
if(frameLength != 5) {
skipBytes(frameLength);
return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, streamid,
"PriorityFrame length is "+ frameLength+", expected 5");
}
int x = getInt();
int weight = getByte();
return new PriorityFrame(streamid, x & 0x7fffffff, (x & 0x80000000) != 0, weight);
}
private Http2Frame parseResetFrame(int frameLength, int streamid, int flags) {
// non-zero stream; no flags
if (streamid == 0) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
"zero streamId for ResetFrame");
}
if(frameLength != 4) {
return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
"ResetFrame length is "+ frameLength+", expected 4");
}
return new ResetFrame(streamid, getInt());
}
private Http2Frame parseSettingsFrame(int frameLength, int streamid, int flags) {
// only zero stream
if (streamid != 0) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
"non-zero streamId for SettingsFrame");
}
if ((SettingsFrame.ACK & flags) != 0 && frameLength > 0) {
// RFC 7540 6.5
// Receipt of a SETTINGS frame with the ACK flag set and a length
// field value other than 0 MUST be treated as a connection error
return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
"ACK SettingsFrame is not empty");
}
if (frameLength % 6 != 0) {
return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
"invalid SettingsFrame size: "+frameLength);
}
SettingsFrame sf = new SettingsFrame(flags);
int n = frameLength / 6;
for (int i=0; i<n; i++) {
int id = getShort();
int val = getInt();
if (id > 0 && id <= SettingsFrame.MAX_PARAM) {
// a known parameter. Ignore otherwise
sf.setParameter(id, val); // TODO parameters validation
}
}
return sf;
}
private Http2Frame parsePushPromiseFrame(int frameLength, int streamid, int flags) {
// non-zero stream
if (streamid == 0) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
"zero streamId for PushPromiseFrame");
}
int padLength = 0;
if ((flags & PushPromiseFrame.PADDED) != 0) {
padLength = getByte();
frameLength--;
}
int promisedStream = getInt() & 0x7fffffff;
frameLength -= 4;
if(frameLength < padLength) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
"Padding exceeds the size remaining for the PushPromiseFrame");
}
PushPromiseFrame ppf = new PushPromiseFrame(streamid, flags, promisedStream,
getBuffers(false, frameLength - padLength), padLength);
skipBytes(padLength);
return ppf;
}
private Http2Frame parsePingFrame(int frameLength, int streamid, int flags) {
// only zero stream
if (streamid != 0) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
"non-zero streamId for PingFrame");
}
if(frameLength != 8) {
return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
"PingFrame length is "+ frameLength+", expected 8");
}
return new PingFrame(flags, getBytes(8));
}
private Http2Frame parseGoAwayFrame(int frameLength, int streamid, int flags) {
// only zero stream; no flags
if (streamid != 0) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
"non-zero streamId for GoAwayFrame");
}
if (frameLength < 8) {
return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
"Invalid GoAway frame size");
}
int lastStream = getInt() & 0x7fffffff;
int errorCode = getInt();
byte[] debugData = getBytes(frameLength - 8);
if (debugData.length > 0) {
Log.logError("GoAway debugData " + new String(debugData, UTF_8));
}
return new GoAwayFrame(lastStream, errorCode, debugData);
}
private Http2Frame parseWindowUpdateFrame(int frameLength, int streamid, int flags) {
// any stream; no flags
if(frameLength != 4) {
return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
"WindowUpdateFrame length is "+ frameLength+", expected 4");
}
return new WindowUpdateFrame(streamid, getInt() & 0x7fffffff);
}
private Http2Frame parseContinuationFrame(int frameLength, int streamid, int flags) {
// non-zero stream;
if (streamid == 0) {
return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
"zero streamId for ContinuationFrame");
}
return new ContinuationFrame(streamid, flags, getBuffers(false, frameLength));
}
}

View file

@ -0,0 +1,293 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Frames Encoder
*
* Encode framed into ByteBuffers.
* The class is stateless.
*/
public class FramesEncoder {
public FramesEncoder() {
}
public List<ByteBuffer> encodeFrames(List<HeaderFrame> frames) {
List<ByteBuffer> bufs = new ArrayList<>(frames.size() * 2);
for (HeaderFrame f : frames) {
bufs.addAll(encodeFrame(f));
}
return bufs;
}
public ByteBuffer encodeConnectionPreface(byte[] preface, SettingsFrame frame) {
final int length = frame.length();
ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length + preface.length);
buf.put(preface);
putSettingsFrame(buf, frame, length);
buf.flip();
return buf;
}
public List<ByteBuffer> encodeFrame(Http2Frame frame) {
switch (frame.type()) {
case DataFrame.TYPE:
return encodeDataFrame((DataFrame) frame);
case HeadersFrame.TYPE:
return encodeHeadersFrame((HeadersFrame) frame);
case PriorityFrame.TYPE:
return encodePriorityFrame((PriorityFrame) frame);
case ResetFrame.TYPE:
return encodeResetFrame((ResetFrame) frame);
case SettingsFrame.TYPE:
return encodeSettingsFrame((SettingsFrame) frame);
case PushPromiseFrame.TYPE:
return encodePushPromiseFrame((PushPromiseFrame) frame);
case PingFrame.TYPE:
return encodePingFrame((PingFrame) frame);
case GoAwayFrame.TYPE:
return encodeGoAwayFrame((GoAwayFrame) frame);
case WindowUpdateFrame.TYPE:
return encodeWindowUpdateFrame((WindowUpdateFrame) frame);
case ContinuationFrame.TYPE:
return encodeContinuationFrame((ContinuationFrame) frame);
default:
throw new UnsupportedOperationException("Not supported frame "+frame.type()+" ("+frame.getClass().getName()+")");
}
}
private static final int NO_FLAGS = 0;
private static final int ZERO_STREAM = 0;
private List<ByteBuffer> encodeDataFrame(DataFrame frame) {
// non-zero stream
assert frame.streamid() != 0;
ByteBuffer buf = encodeDataFrameStart(frame);
if (frame.getFlag(DataFrame.PADDED)) {
return joinWithPadding(buf, frame.getData(), frame.getPadLength());
} else {
return join(buf, frame.getData());
}
}
private ByteBuffer encodeDataFrameStart(DataFrame frame) {
boolean isPadded = frame.getFlag(DataFrame.PADDED);
final int length = frame.getDataLength() + (isPadded ? (frame.getPadLength() + 1) : 0);
ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 1 : 0));
putHeader(buf, length, DataFrame.TYPE, frame.getFlags(), frame.streamid());
if (isPadded) {
buf.put((byte) frame.getPadLength());
}
buf.flip();
return buf;
}
private List<ByteBuffer> encodeHeadersFrame(HeadersFrame frame) {
// non-zero stream
assert frame.streamid() != 0;
ByteBuffer buf = encodeHeadersFrameStart(frame);
if (frame.getFlag(HeadersFrame.PADDED)) {
return joinWithPadding(buf, frame.getHeaderBlock(), frame.getPadLength());
} else {
return join(buf, frame.getHeaderBlock());
}
}
private ByteBuffer encodeHeadersFrameStart(HeadersFrame frame) {
boolean isPadded = frame.getFlag(HeadersFrame.PADDED);
boolean hasPriority = frame.getFlag(HeadersFrame.PRIORITY);
final int length = frame.getHeaderLength() + (isPadded ? (frame.getPadLength() + 1) : 0) + (hasPriority ? 5 : 0);
ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 1 : 0) + (hasPriority ? 5 : 0));
putHeader(buf, length, HeadersFrame.TYPE, frame.getFlags(), frame.streamid());
if (isPadded) {
buf.put((byte) frame.getPadLength());
}
if (hasPriority) {
putPriority(buf, frame.getExclusive(), frame.getStreamDependency(), frame.getWeight());
}
buf.flip();
return buf;
}
private List<ByteBuffer> encodePriorityFrame(PriorityFrame frame) {
// non-zero stream; no flags
assert frame.streamid() != 0;
final int length = 5;
ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
putHeader(buf, length, PriorityFrame.TYPE, NO_FLAGS, frame.streamid());
putPriority(buf, frame.exclusive(), frame.streamDependency(), frame.weight());
buf.flip();
return List.of(buf);
}
private List<ByteBuffer> encodeResetFrame(ResetFrame frame) {
// non-zero stream; no flags
assert frame.streamid() != 0;
final int length = 4;
ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
putHeader(buf, length, ResetFrame.TYPE, NO_FLAGS, frame.streamid());
buf.putInt(frame.getErrorCode());
buf.flip();
return List.of(buf);
}
private List<ByteBuffer> encodeSettingsFrame(SettingsFrame frame) {
// only zero stream
assert frame.streamid() == 0;
final int length = frame.length();
ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
putSettingsFrame(buf, frame, length);
buf.flip();
return List.of(buf);
}
private List<ByteBuffer> encodePushPromiseFrame(PushPromiseFrame frame) {
// non-zero stream
assert frame.streamid() != 0;
boolean isPadded = frame.getFlag(PushPromiseFrame.PADDED);
final int length = frame.getHeaderLength() + (isPadded ? 5 : 4);
ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 5 : 4));
putHeader(buf, length, PushPromiseFrame.TYPE, frame.getFlags(), frame.streamid());
if (isPadded) {
buf.put((byte) frame.getPadLength());
}
buf.putInt(frame.getPromisedStream());
buf.flip();
if (frame.getFlag(PushPromiseFrame.PADDED)) {
return joinWithPadding(buf, frame.getHeaderBlock(), frame.getPadLength());
} else {
return join(buf, frame.getHeaderBlock());
}
}
private List<ByteBuffer> encodePingFrame(PingFrame frame) {
// only zero stream
assert frame.streamid() == 0;
final int length = 8;
ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
putHeader(buf, length, PingFrame.TYPE, frame.getFlags(), ZERO_STREAM);
buf.put(frame.getData());
buf.flip();
return List.of(buf);
}
private List<ByteBuffer> encodeGoAwayFrame(GoAwayFrame frame) {
// only zero stream; no flags
assert frame.streamid() == 0;
byte[] debugData = frame.getDebugData();
final int length = 8 + debugData.length;
ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
putHeader(buf, length, GoAwayFrame.TYPE, NO_FLAGS, ZERO_STREAM);
buf.putInt(frame.getLastStream());
buf.putInt(frame.getErrorCode());
if (debugData.length > 0) {
buf.put(debugData);
}
buf.flip();
return List.of(buf);
}
private List<ByteBuffer> encodeWindowUpdateFrame(WindowUpdateFrame frame) {
// any stream; no flags
final int length = 4;
ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
putHeader(buf, length, WindowUpdateFrame.TYPE, NO_FLAGS, frame.streamid);
buf.putInt(frame.getUpdate());
buf.flip();
return List.of(buf);
}
private List<ByteBuffer> encodeContinuationFrame(ContinuationFrame frame) {
// non-zero stream;
assert frame.streamid() != 0;
final int length = frame.getHeaderLength();
ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE);
putHeader(buf, length, ContinuationFrame.TYPE, frame.getFlags(), frame.streamid());
buf.flip();
return join(buf, frame.getHeaderBlock());
}
private List<ByteBuffer> joinWithPadding(ByteBuffer buf, List<ByteBuffer> data, int padLength) {
int len = data.size();
if (len == 0) return List.of(buf, getPadding(padLength));
else if (len == 1) return List.of(buf, data.get(0), getPadding(padLength));
else if (len == 2) return List.of(buf, data.get(0), data.get(1), getPadding(padLength));
List<ByteBuffer> res = new ArrayList<>(len+2);
res.add(buf);
res.addAll(data);
res.add(getPadding(padLength));
return res;
}
private List<ByteBuffer> join(ByteBuffer buf, List<ByteBuffer> data) {
int len = data.size();
if (len == 0) return List.of(buf);
else if (len == 1) return List.of(buf, data.get(0));
else if (len == 2) return List.of(buf, data.get(0), data.get(1));
List<ByteBuffer> joined = new ArrayList<>(len + 1);
joined.add(buf);
joined.addAll(data);
return joined;
}
private void putSettingsFrame(ByteBuffer buf, SettingsFrame frame, int length) {
// only zero stream;
assert frame.streamid() == 0;
putHeader(buf, length, SettingsFrame.TYPE, frame.getFlags(), ZERO_STREAM);
frame.toByteBuffer(buf);
}
private void putHeader(ByteBuffer buf, int length, int type, int flags, int streamId) {
int x = (length << 8) + type;
buf.putInt(x);
buf.put((byte) flags);
buf.putInt(streamId);
}
private void putPriority(ByteBuffer buf, boolean exclusive, int streamDependency, int weight) {
buf.putInt(exclusive ? (1 << 31) + streamDependency : streamDependency);
buf.put((byte) weight);
}
private ByteBuffer getBuffer(int capacity) {
return ByteBuffer.allocate(capacity);
}
public ByteBuffer getPadding(int length) {
if (length > 255) {
throw new IllegalArgumentException("Padding too big");
}
return ByteBuffer.allocate(length); // zeroed!
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
import static java.nio.charset.StandardCharsets.UTF_8;
public class GoAwayFrame extends ErrorFrame {
private final int lastStream;
private final byte[] debugData;
public static final int TYPE = 0x7;
public GoAwayFrame(int lastStream, int errorCode) {
this(lastStream, errorCode, new byte[0]);
}
public GoAwayFrame(int lastStream, int errorCode, byte[] debugData) {
super(0, 0, errorCode);
this.lastStream = lastStream;
this.debugData = debugData.clone();
}
@Override
public int type() {
return TYPE;
}
@Override
int length() {
return 8 + debugData.length;
}
@Override
public String toString() {
return super.toString() + " Debugdata: " + new String(debugData, UTF_8);
}
public int getLastStream() {
return this.lastStream;
}
public byte[] getDebugData() {
return debugData.clone();
}
}

View file

@ -0,0 +1,76 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
import jdk.internal.net.http.common.Utils;
import java.nio.ByteBuffer;
import java.util.List;
/**
* Either a HeadersFrame or a ContinuationFrame
*/
public abstract class HeaderFrame extends Http2Frame {
final int headerLength;
final List<ByteBuffer> headerBlocks;
public static final int END_STREAM = 0x1;
public static final int END_HEADERS = 0x4;
public HeaderFrame(int streamid, int flags, List<ByteBuffer> headerBlocks) {
super(streamid, flags);
this.headerBlocks = headerBlocks;
this.headerLength = Utils.remaining(headerBlocks, Integer.MAX_VALUE);
}
@Override
public String flagAsString(int flag) {
switch (flag) {
case END_HEADERS:
return "END_HEADERS";
case END_STREAM:
return "END_STREAM";
}
return super.flagAsString(flag);
}
public List<ByteBuffer> getHeaderBlock() {
return headerBlocks;
}
int getHeaderLength() {
return headerLength;
}
/**
* Returns true if this block is the final block of headers.
*/
public boolean endHeaders() {
return getFlag(END_HEADERS);
}
}

View file

@ -0,0 +1,114 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
import java.nio.ByteBuffer;
import java.util.List;
public class HeadersFrame extends HeaderFrame {
public static final int TYPE = 0x1;
// Flags
public static final int END_STREAM = 0x1;
public static final int PADDED = 0x8;
public static final int PRIORITY = 0x20;
private int padLength;
private int streamDependency;
private int weight;
private boolean exclusive;
public HeadersFrame(int streamid, int flags, List<ByteBuffer> headerBlocks, int padLength) {
super(streamid, flags, headerBlocks);
if (padLength > 0) {
setPadLength(padLength);
}
}
public HeadersFrame(int streamid, int flags, List<ByteBuffer> headerBlocks) {
super(streamid, flags, headerBlocks);
}
public HeadersFrame(int streamid, int flags, ByteBuffer headerBlock) {
this(streamid, flags, List.of(headerBlock));
}
@Override
public int type() {
return TYPE;
}
@Override
int length() {
return headerLength
+ ((flags & PADDED) != 0 ? (1 + padLength) : 0)
+ ((flags & PRIORITY) != 0 ? 5 : 0);
}
@Override
public String flagAsString(int flag) {
switch (flag) {
case END_STREAM:
return "END_STREAM";
case PADDED:
return "PADDED";
case PRIORITY:
return "PRIORITY";
}
return super.flagAsString(flag);
}
public void setPadLength(int padLength) {
this.padLength = padLength;
flags |= PADDED;
}
int getPadLength() {
return padLength;
}
public void setPriority(int streamDependency, boolean exclusive, int weight) {
this.streamDependency = streamDependency;
this.exclusive = exclusive;
this.weight = weight;
this.flags |= PRIORITY;
}
public int getStreamDependency() {
return streamDependency;
}
public int getWeight() {
return weight;
}
public boolean getExclusive() {
return exclusive;
}
}

View file

@ -0,0 +1,141 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
/**
* When sending a frame, the length field must be set in sub-class
* by calling computeLength()
*/
public abstract class Http2Frame {
public static final int FRAME_HEADER_SIZE = 9;
protected int streamid;
protected int flags;
public Http2Frame(int streamid, int flags) {
this.streamid = streamid;
this.flags = flags;
}
public int streamid() {
return streamid;
}
public void setFlag(int flag) {
flags |= flag;
}
public int getFlags() {
return flags;
}
public boolean getFlag(int flag) {
return (flags & flag) != 0;
}
// public void clearFlag(int flag) {
// flags &= 0xffffffff ^ flag;
// }
public void streamid(int streamid) {
this.streamid = streamid;
}
private String typeAsString() {
return asString(type());
}
public int type() {
return -1; // Unknown type
}
int length() {
return -1; // Unknown length
}
public static String asString(int type) {
switch (type) {
case DataFrame.TYPE:
return "DATA";
case HeadersFrame.TYPE:
return "HEADERS";
case ContinuationFrame.TYPE:
return "CONTINUATION";
case ResetFrame.TYPE:
return "RESET";
case PriorityFrame.TYPE:
return "PRIORITY";
case SettingsFrame.TYPE:
return "SETTINGS";
case GoAwayFrame.TYPE:
return "GOAWAY";
case PingFrame.TYPE:
return "PING";
case PushPromiseFrame.TYPE:
return "PUSH_PROMISE";
case WindowUpdateFrame.TYPE:
return "WINDOW_UPDATE";
default:
return "UNKNOWN";
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(typeAsString())
.append(": length=")
.append(Integer.toString(length()))
.append(", streamid=")
.append(streamid)
.append(", flags=");
int f = flags;
int i = 0;
if (f == 0) {
sb.append("0 ");
} else {
while (f != 0) {
if ((f & 1) == 1) {
sb.append(flagAsString(1 << i))
.append(' ');
}
f = f >> 1;
i++;
}
}
return sb.toString();
}
// Override
public String flagAsString(int f) {
return "unknown";
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
public class MalformedFrame extends Http2Frame {
private int errorCode;
// if errorStream == 0 means Connection Error; RFC 7540 5.4.1
// if errorStream != 0 means Stream Error; RFC 7540 5.4.2
private int errorStream;
private String msg;
/**
* Creates Connection Error malformed frame
* @param errorCode - error code, as specified by RFC 7540
* @param msg - internal debug message
*/
public MalformedFrame(int errorCode, String msg) {
this(errorCode, 0 , msg);
}
/**
* Creates Stream Error malformed frame
* @param errorCode - error code, as specified by RFC 7540
* @param errorStream - id of error stream (RST_FRAME will be send for this stream)
* @param msg - internal debug message
*/
public MalformedFrame(int errorCode, int errorStream, String msg) {
super(0, 0);
this.errorCode = errorCode;
this.errorStream = errorStream;
this.msg = msg;
}
@Override
public String toString() {
return super.toString() + " MalformedFrame, Error: " + ErrorFrame.stringForCode(errorCode)
+ " streamid: " + streamid + " reason: " + msg;
}
public int getErrorCode() {
return errorCode;
}
public String getMessage() {
return msg;
}
}

View file

@ -0,0 +1,83 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
import java.net.http.HttpHeaders;
/**
* Contains all parameters for outgoing headers. Is converted to
* HeadersFrame and ContinuationFrames by Http2Connection.
*/
public class OutgoingHeaders<T> extends Http2Frame {
int streamDependency;
int weight;
boolean exclusive;
T attachment;
public static final int PRIORITY = 0x20;
HttpHeaders user, system;
public OutgoingHeaders(HttpHeaders hdrs1, HttpHeaders hdrs2, T attachment) {
super(0, 0);
this.user = hdrs2;
this.system = hdrs1;
this.attachment = attachment;
}
public void setPriority(int streamDependency, boolean exclusive, int weight) {
this.streamDependency = streamDependency;
this.exclusive = exclusive;
this.weight = weight;
this.flags |= PRIORITY;
}
public int getStreamDependency() {
return streamDependency;
}
public int getWeight() {
return weight;
}
public boolean getExclusive() {
return exclusive;
}
public T getAttachment() {
return attachment;
}
public HttpHeaders getUserHeaders() {
return user;
}
public HttpHeaders getSystemHeaders() {
return system;
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
public class PingFrame extends Http2Frame {
private final byte[] data;
public static final int TYPE = 0x6;
// Flags
public static final int ACK = 0x1;
public PingFrame(int flags, byte[] data) {
super(0, flags);
assert data.length == 8;
this.data = data.clone();
}
@Override
public int type() {
return TYPE;
}
@Override
int length() {
return 8;
}
@Override
public String flagAsString(int flag) {
switch (flag) {
case ACK:
return "ACK";
}
return super.flagAsString(flag);
}
public byte[] getData() {
return data.clone();
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
public class PriorityFrame extends Http2Frame {
private final int streamDependency;
private final int weight;
private final boolean exclusive;
public static final int TYPE = 0x2;
public PriorityFrame(int streamId, int streamDependency, boolean exclusive, int weight) {
super(streamId, 0);
this.streamDependency = streamDependency;
this.exclusive = exclusive;
this.weight = weight;
}
@Override
public int type() {
return TYPE;
}
@Override
int length() {
return 5;
}
public int streamDependency() {
return streamDependency;
}
public int weight() {
return weight;
}
public boolean exclusive() {
return exclusive;
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
import java.nio.ByteBuffer;
import java.util.List;
public class PushPromiseFrame extends HeaderFrame {
private int padLength;
private final int promisedStream;
public static final int TYPE = 0x5;
// Flags
public static final int END_HEADERS = 0x4;
public static final int PADDED = 0x8;
public PushPromiseFrame(int streamid, int flags, int promisedStream, List<ByteBuffer> buffers, int padLength) {
super(streamid, flags, buffers);
this.promisedStream = promisedStream;
if(padLength > 0 ) {
setPadLength(padLength);
}
}
@Override
public int type() {
return TYPE;
}
@Override
int length() {
return headerLength + ((flags & PADDED) != 0 ? 5 : 4);
}
@Override
public String toString() {
return super.toString() + " promisedStreamid: " + promisedStream
+ " headerLength: " + headerLength;
}
@Override
public String flagAsString(int flag) {
switch (flag) {
case PADDED:
return "PADDED";
case END_HEADERS:
return "END_HEADERS";
}
return super.flagAsString(flag);
}
public void setPadLength(int padLength) {
this.padLength = padLength;
flags |= PADDED;
}
public int getPadLength() {
return padLength;
}
public int getPromisedStream() {
return promisedStream;
}
@Override
public boolean endHeaders() {
return getFlag(END_HEADERS);
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
public class ResetFrame extends ErrorFrame {
public static final int TYPE = 0x3;
// See ErrorFrame for error values
public ResetFrame(int streamid, int errorCode) {
super(streamid, 0, errorCode);
}
@Override
public int type() {
return TYPE;
}
@Override
int length() {
return 4;
}
}

View file

@ -0,0 +1,174 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class SettingsFrame extends Http2Frame {
private final int[] parameters;
public static final int TYPE = 0x4;
// Flags
public static final int ACK = 0x1;
@Override
public String flagAsString(int flag) {
switch (flag) {
case ACK:
return "ACK";
}
return super.flagAsString(flag);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString())
.append(" Settings: ");
for (int i = 0; i < MAX_PARAM; i++) {
if (parameters[i] != -1) {
sb.append(name(i))
.append("=")
.append(Integer.toString(parameters[i]))
.append(' ');
}
}
return sb.toString();
}
// Parameters
public static final int HEADER_TABLE_SIZE = 0x1;
public static final int ENABLE_PUSH = 0x2;
public static final int MAX_CONCURRENT_STREAMS = 0x3;
public static final int INITIAL_WINDOW_SIZE = 0x4;
public static final int MAX_FRAME_SIZE = 0x5;
public static final int MAX_HEADER_LIST_SIZE = 0x6;
private String name(int i) {
switch (i+1) {
case HEADER_TABLE_SIZE:
return "HEADER_TABLE_SIZE";
case ENABLE_PUSH:
return "ENABLE_PUSH";
case MAX_CONCURRENT_STREAMS:
return "MAX_CONCURRENT_STREAMS";
case INITIAL_WINDOW_SIZE:
return "INITIAL_WINDOW_SIZE";
case MAX_FRAME_SIZE:
return "MAX_FRAME_SIZE";
case MAX_HEADER_LIST_SIZE:
return "MAX_HEADER_LIST_SIZE";
}
return "unknown parameter";
}
public static final int MAX_PARAM = 0x6;
public SettingsFrame(int flags) {
super(0, flags);
parameters = new int [MAX_PARAM];
Arrays.fill(parameters, -1);
}
public SettingsFrame() {
this(0);
}
public SettingsFrame(SettingsFrame other) {
super(0, other.flags);
parameters = Arrays.copyOf(other.parameters, MAX_PARAM);
}
@Override
public int type() {
return TYPE;
}
public int getParameter(int paramID) {
if (paramID > MAX_PARAM) {
throw new IllegalArgumentException("illegal parameter");
}
return parameters[paramID - 1];
}
public SettingsFrame setParameter(int paramID, int value) {
if (paramID > MAX_PARAM) {
throw new IllegalArgumentException("illegal parameter");
}
parameters[paramID-1] = value;
return this;
}
int length() {
int len = 0;
for (int i : parameters) {
if (i != -1) {
len += 6;
}
}
return len;
}
void toByteBuffer(ByteBuffer buf) {
for (int i = 0; i < MAX_PARAM; i++) {
if (parameters[i] != -1) {
buf.putShort((short) (i + 1));
buf.putInt(parameters[i]);
}
}
}
public byte[] toByteArray() {
byte[] bytes = new byte[length()];
ByteBuffer buf = ByteBuffer.wrap(bytes);
toByteBuffer(buf);
return bytes;
}
private static final int K = 1024;
public synchronized void update(SettingsFrame updated) {
for (int i = 0; i < MAX_PARAM; i++) {
if (updated.parameters[i] != -1) {
parameters[i] = updated.parameters[i];
}
}
}
public static SettingsFrame getDefaultSettings() {
SettingsFrame f = new SettingsFrame();
// TODO: check these values
f.setParameter(ENABLE_PUSH, 1);
f.setParameter(HEADER_TABLE_SIZE, 4 * K);
f.setParameter(MAX_CONCURRENT_STREAMS, 35);
f.setParameter(INITIAL_WINDOW_SIZE, 64 * K - 1);
f.setParameter(MAX_FRAME_SIZE, 16 * K);
return f;
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2015, 2018, 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.frame;
public class WindowUpdateFrame extends Http2Frame {
private final int windowUpdate;
public static final int TYPE = 0x8;
public WindowUpdateFrame(int streamid, int windowUpdate) {
super(streamid, 0);
this.windowUpdate = windowUpdate;
}
@Override
public int type() {
return TYPE;
}
@Override
int length() {
return 4;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString())
.append(" WindowUpdate: ")
.append(windowUpdate);
return sb.toString();
}
public int getUpdate() {
return this.windowUpdate;
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2015, 2018, 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.hpack;
import java.nio.ByteBuffer;
interface BinaryRepresentationWriter {
boolean write(HeaderTable table, ByteBuffer destination);
BinaryRepresentationWriter reset();
}

View file

@ -0,0 +1,78 @@
/*
* Copyright (c) 2016, 2018, 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.hpack;
import java.nio.ByteBuffer;
import java.util.Iterator;
import static java.util.Objects.requireNonNull;
final class BulkSizeUpdateWriter implements BinaryRepresentationWriter {
private final SizeUpdateWriter writer = new SizeUpdateWriter();
private Iterator<Integer> maxSizes;
private boolean writing;
private boolean configured;
BulkSizeUpdateWriter maxHeaderTableSizes(Iterable<Integer> sizes) {
if (configured) {
throw new IllegalStateException("Already configured");
}
requireNonNull(sizes, "sizes");
maxSizes = sizes.iterator();
configured = true;
return this;
}
@Override
public boolean write(HeaderTable table, ByteBuffer destination) {
if (!configured) {
throw new IllegalStateException("Configure first");
}
while (true) {
if (writing) {
if (!writer.write(table, destination)) {
return false;
}
writing = false;
} else if (maxSizes.hasNext()) {
writing = true;
writer.reset();
writer.maxHeaderTableSize(maxSizes.next());
} else {
configured = false;
return true;
}
}
}
@Override
public BulkSizeUpdateWriter reset() {
maxSizes = null;
writing = false;
configured = false;
return this;
}
}

View file

@ -0,0 +1,592 @@
/*
* Copyright (c) 2014, 2018, 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.hpack;
import jdk.internal.net.http.hpack.HPACK.Logger;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicLong;
import static jdk.internal.net.http.hpack.HPACK.Logger.Level.EXTRA;
import static jdk.internal.net.http.hpack.HPACK.Logger.Level.NORMAL;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
/**
* Decodes headers from their binary representation.
*
* <p> Typical lifecycle looks like this:
*
* <p> {@link #Decoder(int) new Decoder}
* ({@link #setMaxCapacity(int) setMaxCapacity}?
* {@link #decode(ByteBuffer, boolean, DecodingCallback) decode})*
*
* @apiNote
*
* <p> The design intentions behind Decoder were to facilitate flexible and
* incremental style of processing.
*
* <p> {@code Decoder} does not require a complete header block in a single
* {@code ByteBuffer}. The header block can be spread across many buffers of any
* size and decoded one-by-one the way it makes most sense for the user. This
* way also allows not to limit the size of the header block.
*
* <p> Headers are delivered to the {@linkplain DecodingCallback callback} as
* soon as they become decoded. Using the callback also gives the user a freedom
* to decide how headers are processed. The callback does not limit the number
* of headers decoded during single decoding operation.
*
* @since 9
*/
public final class Decoder {
private final Logger logger;
private static final AtomicLong DECODERS_IDS = new AtomicLong();
private static final State[] states = new State[256];
static {
// To be able to do a quick lookup, each of 256 possibilities are mapped
// to corresponding states.
//
// We can safely do this since patterns 1, 01, 001, 0001, 0000 are
// Huffman prefixes and therefore are inherently not ambiguous.
//
// I do it mainly for better debugging (to not go each time step by step
// through if...else tree). As for performance win for the decoding, I
// believe is negligible.
for (int i = 0; i < states.length; i++) {
if ((i & 0b1000_0000) == 0b1000_0000) {
states[i] = State.INDEXED;
} else if ((i & 0b1100_0000) == 0b0100_0000) {
states[i] = State.LITERAL_WITH_INDEXING;
} else if ((i & 0b1110_0000) == 0b0010_0000) {
states[i] = State.SIZE_UPDATE;
} else if ((i & 0b1111_0000) == 0b0001_0000) {
states[i] = State.LITERAL_NEVER_INDEXED;
} else if ((i & 0b1111_0000) == 0b0000_0000) {
states[i] = State.LITERAL;
} else {
throw new InternalError(String.valueOf(i));
}
}
}
private final long id;
private final SimpleHeaderTable table;
private State state = State.READY;
private final IntegerReader integerReader;
private final StringReader stringReader;
private final StringBuilder name;
private final StringBuilder value;
private int intValue;
private boolean firstValueRead;
private boolean firstValueIndex;
private boolean nameHuffmanEncoded;
private boolean valueHuffmanEncoded;
private int capacity;
/**
* Constructs a {@code Decoder} with the specified initial capacity of the
* header table.
*
* <p> The value has to be agreed between decoder and encoder out-of-band,
* e.g. by a protocol that uses HPACK
* (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
*
* @param capacity
* a non-negative integer
*
* @throws IllegalArgumentException
* if capacity is negative
*/
public Decoder(int capacity) {
id = DECODERS_IDS.incrementAndGet();
logger = HPACK.getLogger().subLogger("Decoder#" + id);
if (logger.isLoggable(NORMAL)) {
logger.log(NORMAL, () -> format("new decoder with maximum table size %s",
capacity));
}
if (logger.isLoggable(NORMAL)) {
/* To correlate with logging outside HPACK, knowing
hashCode/toString is important */
logger.log(NORMAL, () -> {
String hashCode = Integer.toHexString(
System.identityHashCode(this));
return format("toString='%s', identityHashCode=%s",
toString(), hashCode);
});
}
setMaxCapacity0(capacity);
table = new SimpleHeaderTable(capacity, logger.subLogger("HeaderTable"));
integerReader = new IntegerReader();
stringReader = new StringReader();
name = new StringBuilder(512);
value = new StringBuilder(1024);
}
/**
* Sets a maximum capacity of the header table.
*
* <p> The value has to be agreed between decoder and encoder out-of-band,
* e.g. by a protocol that uses HPACK
* (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
*
* @param capacity
* a non-negative integer
*
* @throws IllegalArgumentException
* if capacity is negative
*/
public void setMaxCapacity(int capacity) {
if (logger.isLoggable(NORMAL)) {
logger.log(NORMAL, () -> format("setting maximum table size to %s",
capacity));
}
setMaxCapacity0(capacity);
}
private void setMaxCapacity0(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("capacity >= 0: " + capacity);
}
// FIXME: await capacity update if less than what was prior to it
this.capacity = capacity;
}
/**
* Decodes a header block from the given buffer to the given callback.
*
* <p> Suppose a header block is represented by a sequence of
* {@code ByteBuffer}s in the form of {@code Iterator<ByteBuffer>}. And the
* consumer of decoded headers is represented by the callback. Then to
* decode the header block, the following approach might be used:
*
* <pre>{@code
* while (buffers.hasNext()) {
* ByteBuffer input = buffers.next();
* decoder.decode(input, callback, !buffers.hasNext());
* }
* }</pre>
*
* <p> The decoder reads as much as possible of the header block from the
* given buffer, starting at the buffer's position, and increments its
* position to reflect the bytes read. The buffer's mark and limit will not
* be modified.
*
* <p> Once the method is invoked with {@code endOfHeaderBlock == true}, the
* current header block is deemed ended, and inconsistencies, if any, are
* reported immediately by throwing an {@code IOException}.
*
* <p> Each callback method is called only after the implementation has
* processed the corresponding bytes. If the bytes revealed a decoding
* error, the callback method is not called.
*
* <p> In addition to exceptions thrown directly by the method, any
* exceptions thrown from the {@code callback} will bubble up.
*
* @apiNote The method asks for {@code endOfHeaderBlock} flag instead of
* returning it for two reasons. The first one is that the user of the
* decoder always knows which chunk is the last. The second one is to throw
* the most detailed exception possible, which might be useful for
* diagnosing issues.
*
* @implNote This implementation is not atomic in respect to decoding
* errors. In other words, if the decoding operation has thrown a decoding
* error, the decoder is no longer usable.
*
* @param headerBlock
* the chunk of the header block, may be empty
* @param endOfHeaderBlock
* true if the chunk is the final (or the only one) in the sequence
*
* @param consumer
* the callback
* @throws IOException
* in case of a decoding error
* @throws NullPointerException
* if either headerBlock or consumer are null
*/
public void decode(ByteBuffer headerBlock,
boolean endOfHeaderBlock,
DecodingCallback consumer) throws IOException {
requireNonNull(headerBlock, "headerBlock");
requireNonNull(consumer, "consumer");
if (logger.isLoggable(NORMAL)) {
logger.log(NORMAL, () -> format("reading %s, end of header block? %s",
headerBlock, endOfHeaderBlock));
}
while (headerBlock.hasRemaining()) {
proceed(headerBlock, consumer);
}
if (endOfHeaderBlock && state != State.READY) {
logger.log(NORMAL, () -> format("unexpected end of %s representation",
state));
throw new IOException("Unexpected end of header block");
}
}
private void proceed(ByteBuffer input, DecodingCallback action)
throws IOException {
switch (state) {
case READY:
resumeReady(input);
break;
case INDEXED:
resumeIndexed(input, action);
break;
case LITERAL:
resumeLiteral(input, action);
break;
case LITERAL_WITH_INDEXING:
resumeLiteralWithIndexing(input, action);
break;
case LITERAL_NEVER_INDEXED:
resumeLiteralNeverIndexed(input, action);
break;
case SIZE_UPDATE:
resumeSizeUpdate(input, action);
break;
default:
throw new InternalError("Unexpected decoder state: " + state);
}
}
private void resumeReady(ByteBuffer input) {
int b = input.get(input.position()) & 0xff; // absolute read
State s = states[b];
if (logger.isLoggable(EXTRA)) {
logger.log(EXTRA, () -> format("next binary representation %s (first byte 0x%02x)",
s, b));
}
switch (s) {
case INDEXED:
integerReader.configure(7);
state = State.INDEXED;
firstValueIndex = true;
break;
case LITERAL:
state = State.LITERAL;
firstValueIndex = (b & 0b0000_1111) != 0;
if (firstValueIndex) {
integerReader.configure(4);
}
break;
case LITERAL_WITH_INDEXING:
state = State.LITERAL_WITH_INDEXING;
firstValueIndex = (b & 0b0011_1111) != 0;
if (firstValueIndex) {
integerReader.configure(6);
}
break;
case LITERAL_NEVER_INDEXED:
state = State.LITERAL_NEVER_INDEXED;
firstValueIndex = (b & 0b0000_1111) != 0;
if (firstValueIndex) {
integerReader.configure(4);
}
break;
case SIZE_UPDATE:
integerReader.configure(5);
state = State.SIZE_UPDATE;
firstValueIndex = true;
break;
default:
throw new InternalError(String.valueOf(s));
}
if (!firstValueIndex) {
input.get(); // advance, next stop: "String Literal"
}
}
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+
// | 1 | Index (7+) |
// +---+---------------------------+
//
private void resumeIndexed(ByteBuffer input, DecodingCallback action)
throws IOException {
if (!integerReader.read(input)) {
return;
}
intValue = integerReader.get();
integerReader.reset();
if (logger.isLoggable(NORMAL)) {
logger.log(NORMAL, () -> format("indexed %s", intValue));
}
try {
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
action.onIndexed(intValue, f.name, f.value);
} finally {
state = State.READY;
}
}
private SimpleHeaderTable.HeaderField getHeaderFieldAt(int index)
throws IOException
{
SimpleHeaderTable.HeaderField f;
try {
f = table.get(index);
} catch (IndexOutOfBoundsException e) {
throw new IOException("header fields table index", e);
}
return f;
}
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+
// | 0 | 0 | 0 | 0 | Index (4+) |
// +---+---+-----------------------+
// | H | Value Length (7+) |
// +---+---------------------------+
// | Value String (Length octets) |
// +-------------------------------+
//
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+
// | 0 | 0 | 0 | 0 | 0 |
// +---+---+-----------------------+
// | H | Name Length (7+) |
// +---+---------------------------+
// | Name String (Length octets) |
// +---+---------------------------+
// | H | Value Length (7+) |
// +---+---------------------------+
// | Value String (Length octets) |
// +-------------------------------+
//
private void resumeLiteral(ByteBuffer input, DecodingCallback action)
throws IOException {
if (!completeReading(input)) {
return;
}
try {
if (firstValueIndex) {
if (logger.isLoggable(NORMAL)) {
logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')",
intValue, value));
}
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
action.onLiteral(intValue, f.name, value, valueHuffmanEncoded);
} else {
if (logger.isLoggable(NORMAL)) {
logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')",
name, value));
}
action.onLiteral(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
}
} finally {
cleanUpAfterReading();
}
}
//
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+
// | 0 | 1 | Index (6+) |
// +---+---+-----------------------+
// | H | Value Length (7+) |
// +---+---------------------------+
// | Value String (Length octets) |
// +-------------------------------+
//
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+
// | 0 | 1 | 0 |
// +---+---+-----------------------+
// | H | Name Length (7+) |
// +---+---------------------------+
// | Name String (Length octets) |
// +---+---------------------------+
// | H | Value Length (7+) |
// +---+---------------------------+
// | Value String (Length octets) |
// +-------------------------------+
//
private void resumeLiteralWithIndexing(ByteBuffer input,
DecodingCallback action)
throws IOException {
if (!completeReading(input)) {
return;
}
try {
//
// 1. (name, value) will be stored in the table as strings
// 2. Most likely the callback will also create strings from them
// ------------------------------------------------------------------------
// Let's create those string beforehand (and only once!) to benefit everyone
//
String n;
String v = value.toString();
if (firstValueIndex) {
if (logger.isLoggable(NORMAL)) {
logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')",
intValue, value));
}
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
n = f.name;
action.onLiteralWithIndexing(intValue, n, v, valueHuffmanEncoded);
} else {
n = name.toString();
if (logger.isLoggable(NORMAL)) {
logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')",
n, value));
}
action.onLiteralWithIndexing(n, nameHuffmanEncoded, v, valueHuffmanEncoded);
}
table.put(n, v);
} finally {
cleanUpAfterReading();
}
}
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+
// | 0 | 0 | 0 | 1 | Index (4+) |
// +---+---+-----------------------+
// | H | Value Length (7+) |
// +---+---------------------------+
// | Value String (Length octets) |
// +-------------------------------+
//
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+
// | 0 | 0 | 0 | 1 | 0 |
// +---+---+-----------------------+
// | H | Name Length (7+) |
// +---+---------------------------+
// | Name String (Length octets) |
// +---+---------------------------+
// | H | Value Length (7+) |
// +---+---------------------------+
// | Value String (Length octets) |
// +-------------------------------+
//
private void resumeLiteralNeverIndexed(ByteBuffer input,
DecodingCallback action)
throws IOException {
if (!completeReading(input)) {
return;
}
try {
if (firstValueIndex) {
if (logger.isLoggable(NORMAL)) {
logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')",
intValue, value));
}
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
action.onLiteralNeverIndexed(intValue, f.name, value, valueHuffmanEncoded);
} else {
if (logger.isLoggable(NORMAL)) {
logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')",
name, value));
}
action.onLiteralNeverIndexed(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
}
} finally {
cleanUpAfterReading();
}
}
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+
// | 0 | 0 | 1 | Max size (5+) |
// +---+---------------------------+
//
private void resumeSizeUpdate(ByteBuffer input,
DecodingCallback action) throws IOException {
if (!integerReader.read(input)) {
return;
}
intValue = integerReader.get();
if (logger.isLoggable(NORMAL)) {
logger.log(NORMAL, () -> format("dynamic table size update %s",
intValue));
}
assert intValue >= 0;
if (intValue > capacity) {
throw new IOException(
format("Received capacity exceeds expected: capacity=%s, expected=%s",
intValue, capacity));
}
integerReader.reset();
try {
action.onSizeUpdate(intValue);
table.setMaxSize(intValue);
} finally {
state = State.READY;
}
}
private boolean completeReading(ByteBuffer input) throws IOException {
if (!firstValueRead) {
if (firstValueIndex) {
if (!integerReader.read(input)) {
return false;
}
intValue = integerReader.get();
integerReader.reset();
} else {
if (!stringReader.read(input, name)) {
return false;
}
nameHuffmanEncoded = stringReader.isHuffmanEncoded();
stringReader.reset();
}
firstValueRead = true;
return false;
} else {
if (!stringReader.read(input, value)) {
return false;
}
}
valueHuffmanEncoded = stringReader.isHuffmanEncoded();
stringReader.reset();
return true;
}
private void cleanUpAfterReading() {
name.setLength(0);
value.setLength(0);
firstValueRead = false;
state = State.READY;
}
private enum State {
READY,
INDEXED,
LITERAL_NEVER_INDEXED,
LITERAL,
LITERAL_WITH_INDEXING,
SIZE_UPDATE
}
SimpleHeaderTable getTable() {
return table;
}
}

Some files were not shown because too many files have changed in this diff Show more