--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AbstractAsyncSSLConnection.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,188 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.http;
-
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
-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.SSLEngineResult;
-import javax.net.ssl.SSLParameters;
-
-import jdk.incubator.http.internal.common.SSLTube;
-import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.Utils;
-
-
-/**
- * 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;
-
- AbstractAsyncSSLConnection(InetSocketAddress addr,
- HttpClientImpl client,
- String serverName,
- String[] alpn) {
- super(addr, client);
- this.serverName = serverName;
- SSLContext context = client.theSSLContext();
- sslParameters = createSSLParameters(client, serverName, alpn);
- Log.logParams(sslParameters);
- engine = createEngine(context, 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,
- String serverName,
- String[] alpn) {
- SSLParameters sslp = client.sslParameters();
- SSLParameters sslParameters = Utils.copySSLParameters(sslp);
- 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 != null) {
- sslParameters.setServerNames(List.of(new SNIHostName(serverName)));
- }
- return sslParameters;
- }
-
- private static SSLEngine createEngine(SSLContext context,
- SSLParameters sslParameters) {
- SSLEngine engine = context.createSSLEngine();
- engine.setUseClientMode(true);
- engine.setSSLParameters(sslParameters);
- return engine;
- }
-
- @Override
- final boolean isSecure() {
- return true;
- }
-
- // Support for WebSocket/RawChannelImpl which unfortunately
- // still depends on synchronous read/writes.
- // It should be removed when RawChannelImpl moves to using asynchronous APIs.
- static final class SSLConnectionChannel extends DetachedConnectionChannel {
- final DetachedConnectionChannel delegate;
- final SSLDelegate sslDelegate;
- SSLConnectionChannel(DetachedConnectionChannel delegate, SSLDelegate sslDelegate) {
- this.delegate = delegate;
- this.sslDelegate = sslDelegate;
- }
-
- SocketChannel channel() {
- return delegate.channel();
- }
-
- @Override
- ByteBuffer read() throws IOException {
- SSLDelegate.WrapperResult r = sslDelegate.recvData(ByteBuffer.allocate(8192));
- // TODO: check for closure
- int n = r.result.bytesProduced();
- if (n > 0) {
- return r.buf;
- } else if (n == 0) {
- return Utils.EMPTY_BYTEBUFFER;
- } else {
- return null;
- }
- }
- @Override
- long write(ByteBuffer[] buffers, int start, int number) throws IOException {
- long l = SSLDelegate.countBytes(buffers, start, number);
- SSLDelegate.WrapperResult r = sslDelegate.sendData(buffers, start, number);
- if (r.result.getStatus() == SSLEngineResult.Status.CLOSED) {
- if (l > 0) {
- throw new IOException("SSLHttpConnection closed");
- }
- }
- return l;
- }
- @Override
- public void shutdownInput() throws IOException {
- delegate.shutdownInput();
- }
- @Override
- public void shutdownOutput() throws IOException {
- delegate.shutdownOutput();
- }
- @Override
- public void close() {
- delegate.close();
- }
- }
-
- // Support for WebSocket/RawChannelImpl which unfortunately
- // still depends on synchronous read/writes.
- // It should be removed when RawChannelImpl moves to using asynchronous APIs.
- @Override
- DetachedConnectionChannel detachChannel() {
- assert client() != null;
- DetachedConnectionChannel detachedChannel = plainConnection().detachChannel();
- SSLDelegate sslDelegate = new SSLDelegate(engine,
- detachedChannel.channel());
- return new SSLConnectionChannel(detachedChannel, sslDelegate);
- }
-
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AbstractSubscription.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,45 +0,0 @@
-/*
- * Copyright (c) 2017, 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.incubator.http;
-
-import java.util.concurrent.Flow;
-import jdk.incubator.http.internal.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; }
-
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncEvent.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.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;
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLConnection.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,115 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.http;
-
-import java.io.IOException;
-import java.lang.System.Logger.Level;
-import java.net.InetSocketAddress;
-import java.nio.channels.SocketChannel;
-import java.util.concurrent.CompletableFuture;
-import jdk.incubator.http.internal.common.SSLTube;
-import jdk.incubator.http.internal.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), 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
- void shutdownInput() throws IOException {
- debug.log(Level.DEBUG, "plainConnection.channel().shutdownInput()");
- plainConnection.channel().shutdownInput();
- }
-
- @Override
- void shutdownOutput() throws IOException {
- debug.log(Level.DEBUG, "plainConnection.channel().shutdownOutput()");
- plainConnection.channel().shutdownOutput();
- }
-
- @Override
- SSLTube getConnectionFlow() {
- return flow;
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLTunnelConnection.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,128 +0,0 @@
-/*
- * 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.incubator.http;
-
-import java.io.IOException;
-import java.lang.System.Logger.Level;
-import java.net.InetSocketAddress;
-import java.nio.channels.SocketChannel;
-import java.util.concurrent.CompletableFuture;
-import jdk.incubator.http.internal.common.SSLTube;
-import jdk.incubator.http.internal.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), alpn);
- this.plainConnection = new PlainTunnelingConnection(addr, proxy, client, proxyHeaders);
- this.writePublisher = new PlainHttpPublisher();
- }
-
- @Override
- public CompletableFuture<Void> connectAsync() {
- debug.log(Level.DEBUG, "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 -> {
- debug.log(Level.DEBUG, "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
- void shutdownInput() throws IOException {
- plainConnection.channel().shutdownInput();
- }
-
- @Override
- void shutdownOutput() throws IOException {
- plainConnection.channel().shutdownOutput();
- }
-
- @Override
- SocketChannel channel() {
- return plainConnection.channel();
- }
-
- @Override
- boolean isProxied() {
- return true;
- }
-
- @Override
- SSLTube getConnectionFlow() {
- return flow;
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncTriggerEvent.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-/*
- * Copyright (c) 2017, 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.incubator.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; }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AuthenticationFilter.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,398 +0,0 @@
-/*
- * 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.incubator.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 jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.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);
-
- // needs to be instance method in Authenticator
- return auth.requestPasswordAuthenticationInstance(uri.getHost(),
- null,
- uri.getPort(),
- uri.getScheme(),
- 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,
- 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;
- cache.store(au.scheme, req.uri(), false, 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;
- }
- 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;
- }
- addBasicCredentials(req, proxy, au.credentials);
- au.retries++;
- return req;
- }
- }
-
- // Use a WeakHashMap to make it possible for the HttpClient to
- // be garbaged 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 class Cache {
- final LinkedList<CacheEntry> entries = new LinkedList<>();
-
- 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 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 = uri.resolve(".").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 = uri.toString();
- return other.startsWith(root);
- }
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ConnectionPool.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,490 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.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.incubator.http.internal.common.FlowTube;
-import jdk.incubator.http.internal.common.Utils;
-
-/**
- * Http 1.1 connection pool.
- */
-final class ConnectionPool {
-
- static final long KEEP_ALIVE = Utils.getIntegerNetProperty(
- "jdk.httpclient.keepalive.timeout", 1200); // seconds
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- final System.Logger debug = Utils.getDebugLogger(this::dbgString, 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();
- debug.log(Level.DEBUG, "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) {
- debug.log(Level.DEBUG,
- "%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() + ")";
- }
-
- }
-
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/CookieFilter.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,88 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.http;
-
-import java.io.IOException;
-import java.net.CookieHandler;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import jdk.incubator.http.internal.common.HttpHeadersImpl;
-import jdk.incubator.http.internal.common.Log;
-
-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 (String hdrname : cookies.keySet()) {
- List<String> vals = cookies.get(hdrname);
- for (String val : vals) {
- 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;
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,571 +0,0 @@
-/*
- * 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.incubator.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 jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.internal.common.Utils;
-import jdk.incubator.http.internal.common.Log;
-
-import static jdk.incubator.http.internal.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> {
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- final System.Logger debug = Utils.getDebugLogger(this::dbgString, 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
- debug.log(Level.DEBUG, "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.
- debug.log(Level.DEBUG, "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.isLoggable(Level.DEBUG)) {
- debug.log(Level.DEBUG,
- "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;
- Response syntheticResponse = new Response(request, this,
- proxyResponse.headers, 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 tunelling through HTTP/1.1 proxy
- // or by sendHeaderAsync (case of HTTP/1.1 SSL tunelling 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(int status, HttpHeaders hdrs) {
- return HttpResponse.BodySubscriber.discard((T)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?
- debug.log(Level.DEBUG, "Upgrading async %s", e.connection());
- return e.readBodyAsync(this::ignoreBody, false, parentExecutor)
- .thenCompose((T v) -> {// v is null
- debug.log(Level.DEBUG, "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);
- }
- debug.log(Level.DEBUG, "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;
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ExchangeImpl.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,210 +0,0 @@
-/*
- * 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.incubator.http;
-
-import java.io.IOException;
-import java.lang.System.Logger.Level;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Executor;
-import java.util.function.Function;
-import jdk.incubator.http.internal.common.MinimalFuture;
-import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
-import jdk.incubator.http.internal.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> {
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- private static final System.Logger DEBUG_LOGGER =
- Utils.getDebugLogger("ExchangeImpl"::toString, 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) {
- DEBUG_LOGGER.log(Level.DEBUG, "get: HTTP/1.1: new Http1Exchange");
- return createHttp1Exchange(exchange, connection);
- } else {
- Http2ClientImpl c2 = exchange.client().client2(); // TODO: improve
- HttpRequestImpl request = exchange.request();
- CompletableFuture<Http2Connection> c2f = c2.getConnectionFor(request);
- DEBUG_LOGGER.log(Level.DEBUG, "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)
- {
- DEBUG_LOGGER.log(Level.DEBUG, "handling HTTP/2 connection creation result");
- boolean secure = exchange.request().secure();
- if (t != null) {
- DEBUG_LOGGER.log(Level.DEBUG,
- "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();
- DEBUG_LOGGER.log(Level.DEBUG, "downgrading to HTTP/1.1 with: %s", as);
- CompletableFuture<? extends ExchangeImpl<U>> ex =
- createHttp1Exchange(exchange, as);
- return ex;
- } else {
- DEBUG_LOGGER.log(Level.DEBUG, "HTTP/2 connection creation failed "
- + "with unexpected exception: %s", (Object)t);
- return MinimalFuture.failedFuture(t);
- }
- }
- if (secure && c== null) {
- DEBUG_LOGGER.log(Level.DEBUG, "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
- DEBUG_LOGGER.log(Level.DEBUG, "new Http1Exchange, try to upgrade");
- return createHttp1Exchange(exchange, connection)
- .thenApply((e) -> {
- exchange.h2Upgrade();
- return e;
- });
- } else {
- DEBUG_LOGGER.log(Level.DEBUG, "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();
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/FilterFactory.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.http;
-
-import java.util.LinkedList;
-import java.util.List;
-
-class FilterFactory {
-
- final LinkedList<Class<? extends HeaderFilter>> filterClasses = new LinkedList<>();
-
- public void addFilter(Class<? extends HeaderFilter> type) {
- filterClasses.add(type);
- }
-
- List<HeaderFilter> getFilterChain() {
- List<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;
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HeaderFilter.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,45 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.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;
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HeaderParser.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,252 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.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();
- }
- } 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();
- }
- }
- 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;
-// }
-// }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1AsyncReceiver.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,651 +0,0 @@
-/*
- * 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.incubator.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.stream.Collectors;
-import jdk.incubator.http.internal.common.Demand;
-import jdk.incubator.http.internal.common.FlowTube.TubeSubscriber;
-import jdk.incubator.http.internal.common.SequentialScheduler;
-import jdk.incubator.http.internal.common.ConnectionExpiredException;
-import jdk.incubator.http.internal.common.Utils;
-
-
-/**
- * A helper class that will queue up incoming data until the receiving
- * side is ready to handle it.
- */
-class Http1AsyncReceiver {
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- final System.Logger debug = Utils.getDebugLogger(this::dbgString, 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();
-
- }
-
- /**
- * 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 SequentialScheduler scheduler;
- Http1AsyncDelegateSubscription(SequentialScheduler scheduler,
- Runnable onCancel) {
- this.scheduler = scheduler;
- this.onCancel = onCancel;
- }
- @Override
- public void request(long n) {
- final Demand demand = demand();
- if (demand.increase(n)) {
- scheduler.runOrSchedule();
- }
- }
- @Override
- public void cancel() { 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;
-
- 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) {
- Http1AsyncDelegate delegate = this.delegate;
- debug.log(Level.DEBUG, "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;
- debug.log(Level.DEBUG, "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();
- debug.log(Level.DEBUG, () -> {
- // 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());
- 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
- debug.log(Level.DEBUG, "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 && queue.isEmpty()) {
- // forward error only after emptying the queue.
- final Object captured = delegate;
- debug.log(Level.DEBUG, () -> "flushing " + x
- + "\n\t delegate: " + captured
- + "\t\t queue.isEmpty: " + queue.isEmpty());
- scheduler.stop();
- delegate.onReadError(x);
- }
- }
-
- /**
- * 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);
- debug.log(Level.DEBUG, () -> "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();
- debug.log(Level.DEBUG, "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);
- Runnable cancel = () -> {
- debug.log(Level.DEBUG, "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);
- pending.onSubscribe(subscription);
- this.delegate = delegate = pending;
- final Object captured = delegate;
- debug.log(Level.DEBUG, () -> "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() {
- debug.log(Level.DEBUG, "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);
- }
- debug.log(Level.DEBUG, () ->
- "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) {
- debug.log(Level.DEBUG, "Unsubscribed %s", delegate);
- this.delegate = null;
- }
- }
- }
-
- // Callback: Consumer of ByteBuffer
- private void asyncReceive(ByteBuffer buf) {
- debug.log(Level.DEBUG, "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;
- debug.log(Level.DEBUG, "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);
- debug.log(Level.DEBUG, () -> "recorded " + t
- + "\n\t delegate: " + delegate
- + "\t\t queue.isEmpty: " + queue.isEmpty(), ex);
- }
- if (queue.isEmpty() || pendingDelegateRef.get() != null) {
- // 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() {
- debug.log(Level.DEBUG, "stopping");
- scheduler.stop();
- 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) {
- debug.log(Level.DEBUG,
- "Http1TubeSubscriber: requesting one more from upstream");
- s.request(1);
- return;
- }
- }
- debug.log(Level.DEBUG, "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() {
- debug.log(Level.DEBUG, "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()]);
- 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;
- debug.log(Level.DEBUG,
- "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) {
- debug.log(Level.DEBUG,
- "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;
- debug.log(Level.DEBUG, () -> "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 = flowTag + " Http1AsyncReceiver";
- } else {
- tag = "Http1AsyncReceiver";
- }
- }
- return tag;
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Exchange.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,616 +0,0 @@
-/*
- * 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.incubator.http;
-
-import java.io.IOException;
-import java.lang.System.Logger.Level;
-import java.net.InetSocketAddress;
-import jdk.incubator.http.HttpResponse.BodyHandler;
-import jdk.incubator.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.incubator.http.internal.common.Demand;
-import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.FlowTube;
-import jdk.incubator.http.internal.common.SequentialScheduler;
-import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.internal.common.Utils;
-import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
-
-/**
- * Encapsulates one HTTP/1.1 request/response exchange.
- */
-class Http1Exchange<T> extends ExchangeImpl<T> {
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
- private static final System.Logger DEBUG_LOGGER =
- Utils.getDebugLogger("Http1Exchange"::toString, 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> {
- protected volatile Flow.Subscription subscription;
- protected volatile boolean complete;
-
- /** Final sentinel in the stream of request body. */
- static final List<ByteBuffer> COMPLETED = List.of(ByteBuffer.allocate(0));
-
- void request(long n) {
- DEBUG_LOGGER.log(Level.DEBUG, () ->
- "Http1BodySubscriber requesting " + n + ", from " + subscription);
- subscription.request(n);
- }
-
- static Http1BodySubscriber completeSubscriber() {
- return new Http1BodySubscriber() {
- @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;
- }
-
- public AbstractSubscription subscription() {
- return s;
- }
- }
-
- @Override
- HttpConnection connection() {
- return connection;
- }
-
- private void connectFlows(HttpConnection connection) {
- FlowTube tube = connection.getConnectionFlow();
- debug.log(Level.DEBUG, "%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.
- debug.log(Level.DEBUG, "Sending headers only");
- if (response == null) {
- response = new Http1Response<>(connection, this, asyncReceiver);
- }
-
- debug.log(Level.DEBUG, "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()) {
- debug.log(Level.DEBUG, "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);
-
- debug.log(Level.DEBUG, "requestAction.headers");
- List<ByteBuffer> data = requestAction.headers();
- synchronized (lock) {
- state = State.HEADERS;
- }
- debug.log(Level.DEBUG, "setting outgoing with headers");
- assert outgoing.isEmpty() : "Unexpected outgoing:" + outgoing;
- appendToOutgoing(data);
- cf.complete(null);
- return cf;
- } catch (Throwable t) {
- debug.log(Level.DEBUG, "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();
- appendToOutgoing(Http1BodySubscriber.COMPLETED);
- } else {
- bodySubscriber.request(1); // start
- }
- } catch (Throwable t) {
- connection.close();
- 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);
- debug.log(Level.DEBUG,
- () -> 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(response.responseCode(),
- response.responseHeaders());
- 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;
- synchronized (lock) {
- if (failed == null)
- failed = 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;
- while (!toComplete.isEmpty()) {
- CompletableFuture<?> cf = toComplete.poll();
- exec.execute(() -> {
- if (cf.completeExceptionally(cause)) {
- debug.log(Level.DEBUG, "completed cf with %s",
- (Object) cause);
- }
- });
- }
- }
- }
-
- 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) {
- debug.log(Level.DEBUG, "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();
- }
-
- // 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(() -> {
- connection.close();
- headersSentCF.completeExceptionally(dp.throwable);
- bodySentCF.completeExceptionally(dp.throwable);
- });
- return dp;
- }
-
- switch (state) {
- case HEADERS:
- state = State.BODY;
- // completeAsync, since dependent tasks should run in another thread
- debug.log(Level.DEBUG, "initiating completion of headersSentCF");
- headersSentCF.completeAsync(() -> this, exec);
- break;
- case BODY:
- if (dp.data == Http1BodySubscriber.COMPLETED) {
- state = State.COMPLETING;
- debug.log(Level.DEBUG, "initiating completion of bodySentCF");
- bodySentCF.completeAsync(() -> this, exec);
- } else {
- debug.log(Level.DEBUG, "requesting more body from the subscriber");
- exec.execute(() -> bodySubscriber.request(1));
- }
- 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 System.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;
- debug.log(Level.DEBUG, "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;
- debug.log(Level.DEBUG, "WriteTask");
- if (subscriber == null) {
- debug.log(Level.DEBUG, "no subscriber yet");
- return;
- }
- debug.log(Level.DEBUG, () -> "hasOutgoing = " + hasOutgoing());
- while (hasOutgoing() && demand.tryDecrement()) {
- DataPair dp = getOutgoing();
-
- if (dp.throwable != null) {
- debug.log(Level.DEBUG, "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;
- }
- debug.log(Level.DEBUG,
- "completed, stopping %s", writeScheduler);
- writeScheduler.stop();
- // Do nothing more. Just do not publish anything further.
- // The next Subscriber will eventually take over.
-
- } else {
- debug.log(Level.DEBUG, () ->
- "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);
- debug.log(Level.DEBUG,
- "subscription request(%d), demand=%s", n, demand);
- writeScheduler.runOrSchedule(client.theExecutor());
- }
-
- @Override
- public void cancel() {
- debug.log(Level.DEBUG, "subscription cancelled");
- if (cancelled)
- return; //no-op
- cancelled = true;
- writeScheduler.stop();
- }
- }
- }
-
- String dbgString() {
- return "Http1Exchange";
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1HeaderParser.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,274 +0,0 @@
-/*
- * Copyright (c) 2017, 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.incubator.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 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));
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,388 +0,0 @@
-/*
- * 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.incubator.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 jdk.incubator.http.Http1Exchange.Http1BodySubscriber;
-import jdk.incubator.http.internal.common.HttpHeadersImpl;
-import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.Utils;
-
-import static java.nio.charset.StandardCharsets.US_ASCII;
-
-/**
- * An HTTP/1.1 request.
- */
-class Http1Request {
- 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");
- Log.logHeaders("REQUEST HEADERS:\n" + s);
- }
- }
-
-
- private void collectHeaders0(StringBuilder sb) {
- BiPredicate<String,List<String>> filter =
- connection.headerFilter(request);
-
- // 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, filter);
-
- // If we're sending this request through a tunnel,
- // don't send any user-supplied proxy-* headers
- // to the target server.
- collectHeaders1(sb, userHeaders, filter);
- sb.append("\r\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(value).append("\r\n");
- }
- }
- }
-
- private String getPathAndQuery(URI uri) {
- String path = uri.getPath();
- String query = uri.getQuery();
- 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;
- }
-
- class StreamSubscriber extends Http1BodySubscriber {
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- if (this.subscription != null) {
- Throwable t = new IllegalStateException("already subscribed");
- http1Exchange.appendToOutgoing(t);
- } else {
- this.subscription = 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;
-
- subscription.cancel();
- 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?
-
- }
- }
- }
-
- class FixedContentSubscriber extends Http1BodySubscriber {
-
- private volatile long contentWritten;
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- if (this.subscription != null) {
- Throwable t = new IllegalStateException("already subscribed");
- http1Exchange.appendToOutgoing(t);
- } else {
- this.subscription = subscription;
- }
- }
-
- @Override
- public void onNext(ByteBuffer item) {
- debug.log(Level.DEBUG, "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) {
- subscription.cancel();
- 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) {
- debug.log(Level.DEBUG, "onError");
- if (complete) // TODO: error?
- return;
-
- subscription.cancel();
- http1Exchange.appendToOutgoing(throwable);
- }
-
- @Override
- public void onComplete() {
- debug.log(Level.DEBUG, "onComplete");
- if (complete) {
- Throwable t = new IllegalStateException("subscription already completed");
- http1Exchange.appendToOutgoing(t);
- } else {
- complete = true;
- long written = contentWritten;
- if (contentLength > written) {
- subscription.cancel();
- 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);
- }
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- final System.Logger debug = Utils.getDebugLogger(this::toString, DEBUG);
-
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Response.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,516 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.http;
-
-import java.io.EOFException;
-import java.lang.System.Logger.Level;
-import java.nio.ByteBuffer;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-import java.util.concurrent.Executor;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import jdk.incubator.http.ResponseContent.BodyParser;
-import jdk.incubator.http.internal.common.Log;
-import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
-import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.internal.common.Utils;
-
-/**
- * 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;
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- final System.Logger debug = Utils.getDebugLogger(this.getClass()::getSimpleName, DEBUG);
-
-
- 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);
- }
-
- public CompletableFuture<Response> readHeadersAsync(Executor executor) {
- debug.log(Level.DEBUG, () -> "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;
- debug.log(Level.DEBUG, () ->
- "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,
- responseCode,
- HTTP_1_1);
- 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(HttpResponse.BodySubscriber.discard((Void)null), true, executor);
- }
- }
-
- public <U> CompletableFuture<U> readBody(HttpResponse.BodySubscriber<U> p,
- boolean return2Cache,
- Executor executor) {
- this.return2Cache = return2Cache;
- final HttpResponse.BodySubscriber<U> pusher = p;
- final CompletionStage<U> bodyCF = p.getBody();
- final CompletableFuture<U> cf = MinimalFuture.of(bodyCF);
-
- 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();
-
- executor.execute(() -> {
- try {
- HttpClientImpl client = connection.client();
- content = new ResponseContent(
- connection, clen, headers, pusher,
- 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.
- client.reference();
- bodyReader.start(content.getBodyParser(
- (t) -> {
- try {
- if (t != null) {
- pusher.onError(t);
- connection.close();
- if (!cf.isDone())
- cf.completeExceptionally(t);
- }
- } finally {
- // 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.
- client.unreference();
- 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) {
- debug.log(Level.DEBUG, () ->
- "Finished reading body: " + s);
- assert s == State.READING_BODY;
- }
- if (t != null && !cf.isDone()) {
- pusher.onError(t);
- cf.completeExceptionally(t);
- }
- } catch (Throwable x) {
- // not supposed to happen
- asyncReceiver.onReadError(x);
- }
- });
- connection.addTrailingOperation(trailingOp);
- } catch (Throwable t) {
- debug.log(Level.DEBUG, () -> "Failed reading body: " + t);
- try {
- if (!cf.isDone()) {
- pusher.onError(t);
- cf.completeExceptionally(t);
- }
- } finally {
- asyncReceiver.onReadError(t);
- }
- }
- });
- return cf;
- }
-
-
- 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.
- debug.log(Level.DEBUG, () -> 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.isDone()) 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();
- debug.log(Level.DEBUG, () -> "Sending " + b.remaining()
- + "/" + b.capacity() + " bytes to header parser");
- if (parser.parse(b)) {
- count -= b.remaining();
- debug.log(Level.DEBUG, () ->
- "Parsing headers completed. bytes=" + count);
- onComplete.accept(State.READING_HEADERS);
- cf.complete(State.READING_HEADERS);
- }
- } catch (Throwable t) {
- debug.log(Level.DEBUG,
- () -> "Header parser failed to handle buffer: " + t);
- cf.completeExceptionally(t);
- }
- }
- }
-
- // 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;
- parser.onSubscribe(s);
- }
-
- @Override
- final void handle(ByteBuffer b,
- BodyParser parser,
- CompletableFuture<State> cf) {
- assert cf != null : "parsing not started";
- assert parser != null : "no parser";
- try {
- debug.log(Level.DEBUG, () -> "Sending " + b.remaining()
- + "/" + b.capacity() + " bytes to body parser");
- parser.accept(b);
- } catch (Throwable t) {
- debug.log(Level.DEBUG,
- () -> "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 String toString() {
- return super.toString() + "/parser=" + String.valueOf(parser);
- }
-
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2ClientImpl.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,231 +0,0 @@
-/*
- * 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.incubator.http;
-
-import java.lang.System.Logger.Level;
-import java.net.InetSocketAddress;
-import java.net.URI;
-import java.util.Base64;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CompletableFuture;
-
-import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.internal.common.Utils;
-import jdk.incubator.http.internal.frame.SettingsFrame;
-import static jdk.incubator.http.internal.frame.SettingsFrame.INITIAL_WINDOW_SIZE;
-import static jdk.incubator.http.internal.frame.SettingsFrame.ENABLE_PUSH;
-import static jdk.incubator.http.internal.frame.SettingsFrame.HEADER_TABLE_SIZE;
-import static jdk.incubator.http.internal.frame.SettingsFrame.MAX_CONCURRENT_STREAMS;
-import static jdk.incubator.http.internal.frame.SettingsFrame.MAX_FRAME_SIZE;
-
-/**
- * Http2 specific aspects of HttpClientImpl
- */
-class Http2ClientImpl {
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- final static System.Logger debug =
- Utils.getDebugLogger("Http2ClientImpl"::toString, 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) { // fast path if connection already exists
- 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
- 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) {
- String key = c.key();
- Http2Connection c1 = connections.putIfAbsent(key, c);
- if (c1 != null) {
- c.setSingleStream(true);
- return false;
- }
- return true;
- }
-
- void deleteConnection(Http2Connection c) {
- connections.remove(c.key());
- }
-
- void stop() {
- debug.log(Level.DEBUG, "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;
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2Connection.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1288 +0,0 @@
-/*
- * 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.incubator.http;
-
-import java.io.EOFException;
-import java.io.IOException;
-import java.lang.System.Logger.Level;
-import java.net.InetSocketAddress;
-import java.net.URI;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.ArrayList;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.Flow;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLException;
-import jdk.incubator.http.HttpConnection.HttpPublisher;
-import jdk.incubator.http.internal.common.FlowTube;
-import jdk.incubator.http.internal.common.FlowTube.TubeSubscriber;
-import jdk.incubator.http.internal.common.HttpHeadersImpl;
-import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.internal.common.SequentialScheduler;
-import jdk.incubator.http.internal.common.Utils;
-import jdk.incubator.http.internal.frame.ContinuationFrame;
-import jdk.incubator.http.internal.frame.DataFrame;
-import jdk.incubator.http.internal.frame.ErrorFrame;
-import jdk.incubator.http.internal.frame.FramesDecoder;
-import jdk.incubator.http.internal.frame.FramesEncoder;
-import jdk.incubator.http.internal.frame.GoAwayFrame;
-import jdk.incubator.http.internal.frame.HeaderFrame;
-import jdk.incubator.http.internal.frame.HeadersFrame;
-import jdk.incubator.http.internal.frame.Http2Frame;
-import jdk.incubator.http.internal.frame.MalformedFrame;
-import jdk.incubator.http.internal.frame.OutgoingHeaders;
-import jdk.incubator.http.internal.frame.PingFrame;
-import jdk.incubator.http.internal.frame.PushPromiseFrame;
-import jdk.incubator.http.internal.frame.ResetFrame;
-import jdk.incubator.http.internal.frame.SettingsFrame;
-import jdk.incubator.http.internal.frame.WindowUpdateFrame;
-import jdk.incubator.http.internal.hpack.Encoder;
-import jdk.incubator.http.internal.hpack.Decoder;
-import jdk.incubator.http.internal.hpack.DecodingCallback;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static jdk.incubator.http.internal.frame.SettingsFrame.*;
-
-
-/**
- * An Http2Connection. Encapsulates the socket(channel) and any SSLEngine used
- * over it. Contains an HttpConnection which hides the SocketChannel SSL stuff.
- *
- * Http2Connections belong to a Http2ClientImpl, (one of) which belongs
- * to a HttpClientImpl.
- *
- * Creation cases:
- * 1) upgraded HTTP/1.1 plain tcp connection
- * 2) prior knowledge directly created plain tcp connection
- * 3) directly created HTTP/2 SSL connection which uses ALPN.
- *
- * Sending is done by writing directly to underlying HttpConnection object which
- * is operating in async mode. No flow control applies on output at this level
- * and all writes are just executed as puts to an output Q belonging to HttpConnection
- * Flow control is implemented by HTTP/2 protocol itself.
- *
- * Hpack header compression
- * and outgoing stream creation is also done here, because these operations
- * must be synchronized at the socket level. Stream objects send frames simply
- * by placing them on the connection's output Queue. sendFrame() is called
- * from a higher level (Stream) thread.
- *
- * asyncReceive(ByteBuffer) is always called from the selector thread. It assembles
- * incoming Http2Frames, and directs them to the appropriate Stream.incoming()
- * or handles them directly itself. This thread performs hpack decompression
- * and incoming stream creation (Server push). Incoming frames destined for a
- * stream are provided by calling Stream.incoming().
- */
-class Http2Connection {
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- static final boolean DEBUG_HPACK = Utils.DEBUG_HPACK; // Revisit: temporary dev flag.
- final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
- final static System.Logger DEBUG_LOGGER =
- Utils.getDebugLogger("Http2Connection"::toString, DEBUG);
- private final System.Logger debugHpack =
- Utils.getHpackLogger(this::dbgString, DEBUG_HPACK);
- static final ByteBuffer EMPTY_TRIGGER = ByteBuffer.allocate(0);
-
- private boolean singleStream; // used only for stream 1, then closed
-
- /*
- * ByteBuffer pooling strategy for HTTP/2 protocol:
- *
- * In general there are 4 points where ByteBuffers are used:
- * - incoming/outgoing frames from/to ByteBuffers plus incoming/outgoing encrypted data
- * in case of SSL connection.
- *
- * 1. Outgoing frames encoded to ByteBuffers.
- * Outgoing ByteBuffers are created with requited size and frequently small (except DataFrames, etc)
- * At this place no pools at all. All outgoing buffers should be collected by GC.
- *
- * 2. Incoming ByteBuffers (decoded to frames).
- * Here, total elimination of BB pool is not a good idea.
- * We don't know how many bytes we will receive through network.
- * So here we allocate buffer of reasonable size. The following life of the BB:
- * - If all frames decoded from the BB are other than DataFrame and HeaderFrame (and HeaderFrame subclasses)
- * BB is returned to pool,
- * - If we decoded DataFrame from the BB. In that case DataFrame refers to subbuffer obtained by slice() method.
- * Such BB is never returned to pool and will be GCed.
- * - If we decoded HeadersFrame from the BB. Then header decoding is performed inside processFrame method and
- * the buffer could be release to pool.
- *
- * 3. SLL encrypted buffers. Here another pool was introduced and all net buffers are to/from the pool,
- * because of we can't predict size encrypted packets.
- *
- */
-
-
- // A small class that allows to control frames with respect to the state of
- // the connection preface. Any data received before the connection
- // preface is sent will be buffered.
- private final class FramesController {
- volatile boolean prefaceSent;
- volatile List<ByteBuffer> pending;
-
- boolean processReceivedData(FramesDecoder decoder, ByteBuffer buf)
- throws IOException
- {
- // if preface is not sent, buffers data in the pending list
- if (!prefaceSent) {
- debug.log(Level.DEBUG, "Preface is not sent: buffering %d",
- buf.remaining());
- synchronized (this) {
- if (!prefaceSent) {
- if (pending == null) pending = new ArrayList<>();
- pending.add(buf);
- debug.log(Level.DEBUG, () -> "there are now "
- + Utils.remaining(pending)
- + " bytes buffered waiting for preface to be sent");
- return false;
- }
- }
- }
-
- // Preface is sent. Checks for pending data and flush it.
- // We rely on this method being called from within the Http2TubeSubscriber
- // scheduler, so we know that no other thread could execute this method
- // concurrently while we're here.
- // This ensures that later incoming buffers will not
- // be processed before we have flushed the pending queue.
- // No additional synchronization is therefore necessary here.
- List<ByteBuffer> pending = this.pending;
- this.pending = null;
- if (pending != null) {
- // flush pending data
- debug.log(Level.DEBUG, () -> "Processing buffered data: "
- + Utils.remaining(pending));
- for (ByteBuffer b : pending) {
- decoder.decode(b);
- }
- }
- // push the received buffer to the frames decoder.
- if (buf != EMPTY_TRIGGER) {
- debug.log(Level.DEBUG, "Processing %d", buf.remaining());
- decoder.decode(buf);
- }
- return true;
- }
-
- // Mark that the connection preface is sent
- void markPrefaceSent() {
- assert !prefaceSent;
- synchronized (this) {
- prefaceSent = true;
- }
- }
- }
-
- volatile boolean closed;
-
- //-------------------------------------
- final HttpConnection connection;
- private final Http2ClientImpl client2;
- private final Map<Integer,Stream<?>> streams = new ConcurrentHashMap<>();
- private int nextstreamid;
- private int nextPushStream = 2;
- private final Encoder hpackOut;
- private final Decoder hpackIn;
- final SettingsFrame clientSettings;
- private volatile SettingsFrame serverSettings;
- private final String key; // for HttpClientImpl.connections map
- private final FramesDecoder framesDecoder;
- private final FramesEncoder framesEncoder = new FramesEncoder();
-
- /**
- * Send Window controller for both connection and stream windows.
- * Each of this connection's Streams MUST use this controller.
- */
- private final WindowController windowController = new WindowController();
- private final FramesController framesController = new FramesController();
- private final Http2TubeSubscriber subscriber = new Http2TubeSubscriber();
- final ConnectionWindowUpdateSender windowUpdater;
- private volatile Throwable cause;
- private volatile Supplier<ByteBuffer> initial;
-
- static final int DEFAULT_FRAME_SIZE = 16 * 1024;
-
-
- // TODO: need list of control frames from other threads
- // that need to be sent
-
- private Http2Connection(HttpConnection connection,
- Http2ClientImpl client2,
- int nextstreamid,
- String key) {
- this.connection = connection;
- this.client2 = client2;
- this.nextstreamid = nextstreamid;
- this.key = key;
- this.clientSettings = this.client2.getClientSettings();
- this.framesDecoder = new FramesDecoder(this::processFrame,
- clientSettings.getParameter(SettingsFrame.MAX_FRAME_SIZE));
- // serverSettings will be updated by server
- this.serverSettings = SettingsFrame.getDefaultSettings();
- this.hpackOut = new Encoder(serverSettings.getParameter(HEADER_TABLE_SIZE));
- this.hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE));
- debugHpack.log(Level.DEBUG, () -> "For the record:" + super.toString());
- debugHpack.log(Level.DEBUG, "Decoder created: %s", hpackIn);
- debugHpack.log(Level.DEBUG, "Encoder created: %s", hpackOut);
- this.windowUpdater = new ConnectionWindowUpdateSender(this,
- client2.getConnectionWindowSize(clientSettings));
- }
-
- /**
- * Case 1) Create from upgraded HTTP/1.1 connection.
- * Is ready to use. Can be SSL. exchange is the Exchange
- * that initiated the connection, whose response will be delivered
- * on a Stream.
- */
- private Http2Connection(HttpConnection connection,
- Http2ClientImpl client2,
- Exchange<?> exchange,
- Supplier<ByteBuffer> initial)
- throws IOException, InterruptedException
- {
- this(connection,
- client2,
- 3, // stream 1 is registered during the upgrade
- keyFor(connection));
- Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
-
- Stream<?> initialStream = createStream(exchange);
- initialStream.registerStream(1);
- windowController.registerStream(1, getInitialSendWindowSize());
- initialStream.requestSent();
- // Upgrading:
- // set callbacks before sending preface - makes sure anything that
- // might be sent by the server will come our way.
- this.initial = initial;
- connectFlows(connection);
- sendConnectionPreface();
- }
-
- // Used when upgrading an HTTP/1.1 connection to HTTP/2 after receiving
- // agreement from the server. Async style but completes immediately, because
- // the connection is already connected.
- static CompletableFuture<Http2Connection> createAsync(HttpConnection connection,
- Http2ClientImpl client2,
- Exchange<?> exchange,
- Supplier<ByteBuffer> initial)
- {
- return MinimalFuture.supply(() -> new Http2Connection(connection, client2, exchange, initial));
- }
-
- // Requires TLS handshake. So, is really async
- static CompletableFuture<Http2Connection> createAsync(HttpRequestImpl request,
- Http2ClientImpl h2client) {
- assert request.secure();
- AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection)
- HttpConnection.getConnection(request.getAddress(),
- h2client.client(),
- request,
- HttpClient.Version.HTTP_2);
-
- return connection.connectAsync()
- .thenCompose(unused -> checkSSLConfig(connection))
- .thenCompose(notused-> {
- CompletableFuture<Http2Connection> cf = new MinimalFuture<>();
- try {
- Http2Connection hc = new Http2Connection(request, h2client, connection);
- cf.complete(hc);
- } catch (IOException e) {
- cf.completeExceptionally(e);
- }
- return cf; } );
- }
-
- /**
- * Cases 2) 3)
- *
- * request is request to be sent.
- */
- private Http2Connection(HttpRequestImpl request,
- Http2ClientImpl h2client,
- HttpConnection connection)
- throws IOException
- {
- this(connection,
- h2client,
- 1,
- keyFor(request.uri(), request.proxy()));
-
- Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
-
- // safe to resume async reading now.
- connectFlows(connection);
- sendConnectionPreface();
- }
-
- private void connectFlows(HttpConnection connection) {
- FlowTube tube = connection.getConnectionFlow();
- // Connect the flow to our Http2TubeSubscriber:
- tube.connectFlows(connection.publisher(), subscriber);
- }
-
- final HttpClientImpl client() {
- return client2.client();
- }
-
- /**
- * Throws an IOException if h2 was not negotiated
- */
- private static CompletableFuture<?> checkSSLConfig(AbstractAsyncSSLConnection aconn) {
- assert aconn.isSecure();
-
- Function<String, CompletableFuture<Void>> checkAlpnCF = (alpn) -> {
- CompletableFuture<Void> cf = new MinimalFuture<>();
- SSLEngine engine = aconn.getEngine();
- assert Objects.equals(alpn, engine.getApplicationProtocol());
-
- DEBUG_LOGGER.log(Level.DEBUG, "checkSSLConfig: alpn: %s", alpn );
-
- if (alpn == null || !alpn.equals("h2")) {
- String msg;
- if (alpn == null) {
- Log.logSSL("ALPN not supported");
- msg = "ALPN not supported";
- } else {
- switch (alpn) {
- case "":
- Log.logSSL(msg = "No ALPN negotiated");
- break;
- case "http/1.1":
- Log.logSSL( msg = "HTTP/1.1 ALPN returned");
- break;
- default:
- Log.logSSL(msg = "Unexpected ALPN: " + alpn);
- cf.completeExceptionally(new IOException(msg));
- }
- }
- cf.completeExceptionally(new ALPNException(msg, aconn));
- return cf;
- }
- cf.complete(null);
- return cf;
- };
-
- return aconn.getALPN()
- .whenComplete((r,t) -> {
- if (t != null && t instanceof SSLException) {
- // something went wrong during the initial handshake
- // close the connection
- aconn.close();
- }
- })
- .thenCompose(checkAlpnCF);
- }
-
- synchronized boolean singleStream() {
- return singleStream;
- }
-
- synchronized void setSingleStream(boolean use) {
- singleStream = use;
- }
-
- static String keyFor(HttpConnection connection) {
- boolean isProxy = connection.isProxied();
- boolean isSecure = connection.isSecure();
- InetSocketAddress addr = connection.address();
-
- return keyString(isSecure, isProxy, addr.getHostString(), addr.getPort());
- }
-
- static String keyFor(URI uri, InetSocketAddress proxy) {
- boolean isSecure = uri.getScheme().equalsIgnoreCase("https");
- boolean isProxy = proxy != null;
-
- String host;
- int port;
-
- if (proxy != null) {
- host = proxy.getHostString();
- port = proxy.getPort();
- } else {
- host = uri.getHost();
- port = uri.getPort();
- }
- return keyString(isSecure, isProxy, host, port);
- }
-
- // {C,S}:{H:P}:host:port
- // C indicates clear text connection "http"
- // S indicates secure "https"
- // H indicates host (direct) connection
- // P indicates proxy
- // Eg: "S:H:foo.com:80"
- static String keyString(boolean secure, boolean proxy, String host, int port) {
- if (secure && port == -1)
- port = 443;
- else if (!secure && port == -1)
- port = 80;
- return (secure ? "S:" : "C:") + (proxy ? "P:" : "H:") + host + ":" + port;
- }
-
- String key() {
- return this.key;
- }
-
- boolean offerConnection() {
- return client2.offerConnection(this);
- }
-
- private HttpPublisher publisher() {
- return connection.publisher();
- }
-
- private void decodeHeaders(HeaderFrame frame, DecodingCallback decoder)
- throws IOException
- {
- debugHpack.log(Level.DEBUG, "decodeHeaders(%s)", decoder);
-
- boolean endOfHeaders = frame.getFlag(HeaderFrame.END_HEADERS);
-
- List<ByteBuffer> buffers = frame.getHeaderBlock();
- int len = buffers.size();
- for (int i = 0; i < len; i++) {
- ByteBuffer b = buffers.get(i);
- hpackIn.decode(b, endOfHeaders && (i == len - 1), decoder);
- }
- }
-
- final int getInitialSendWindowSize() {
- return serverSettings.getParameter(INITIAL_WINDOW_SIZE);
- }
-
- void close() {
- Log.logTrace("Closing HTTP/2 connection: to {0}", connection.address());
- GoAwayFrame f = new GoAwayFrame(0,
- ErrorFrame.NO_ERROR,
- "Requested by user".getBytes(UTF_8));
- // TODO: set last stream. For now zero ok.
- sendFrame(f);
- }
-
- long count;
- final void asyncReceive(ByteBuffer buffer) {
- // We don't need to read anything and
- // we don't want to send anything back to the server
- // until the connection preface has been sent.
- // Therefore we're going to wait if needed before reading
- // (and thus replying) to anything.
- // Starting to reply to something (e.g send an ACK to a
- // SettingsFrame sent by the server) before the connection
- // preface is fully sent might result in the server
- // sending a GOAWAY frame with 'invalid_preface'.
- //
- // Note: asyncReceive is only called from the Http2TubeSubscriber
- // sequential scheduler.
- try {
- Supplier<ByteBuffer> bs = initial;
- // ensure that we always handle the initial buffer first,
- // if any.
- if (bs != null) {
- initial = null;
- ByteBuffer b = bs.get();
- if (b.hasRemaining()) {
- long c = ++count;
- debug.log(Level.DEBUG, () -> "H2 Receiving Initial("
- + c +"): " + b.remaining());
- framesController.processReceivedData(framesDecoder, b);
- }
- }
- ByteBuffer b = buffer;
- // the Http2TubeSubscriber scheduler ensures that the order of incoming
- // buffers is preserved.
- if (b == EMPTY_TRIGGER) {
- debug.log(Level.DEBUG, "H2 Received EMPTY_TRIGGER");
- boolean prefaceSent = framesController.prefaceSent;
- assert prefaceSent;
- // call framesController.processReceivedData to potentially
- // trigger the processing of all the data buffered there.
- framesController.processReceivedData(framesDecoder, buffer);
- debug.log(Level.DEBUG, "H2 processed buffered data");
- } else {
- long c = ++count;
- debug.log(Level.DEBUG, "H2 Receiving(%d): %d", c, b.remaining());
- framesController.processReceivedData(framesDecoder, buffer);
- debug.log(Level.DEBUG, "H2 processed(%d)", c);
- }
- } catch (Throwable e) {
- String msg = Utils.stackTrace(e);
- Log.logTrace(msg);
- shutdown(e);
- }
- }
-
- Throwable getRecordedCause() {
- return cause;
- }
-
- void shutdown(Throwable t) {
- debug.log(Level.DEBUG, () -> "Shutting down h2c (closed="+closed+"): " + t);
- if (closed == true) return;
- synchronized (this) {
- if (closed == true) return;
- closed = true;
- }
- Log.logError(t);
- Throwable initialCause = this.cause;
- if (initialCause == null) this.cause = t;
- client2.deleteConnection(this);
- List<Stream<?>> c = new LinkedList<>(streams.values());
- for (Stream<?> s : c) {
- s.cancelImpl(t);
- }
- connection.close();
- }
-
- /**
- * Streams initiated by a client MUST use odd-numbered stream
- * identifiers; those initiated by the server MUST use even-numbered
- * stream identifiers.
- */
- private static final boolean isSeverInitiatedStream(int streamid) {
- return (streamid & 0x1) == 0;
- }
-
- /**
- * Handles stream 0 (common) frames that apply to whole connection and passes
- * other stream specific frames to that Stream object.
- *
- * Invokes Stream.incoming() which is expected to process frame without
- * blocking.
- */
- void processFrame(Http2Frame frame) throws IOException {
- Log.logFrames(frame, "IN");
- int streamid = frame.streamid();
- if (frame instanceof MalformedFrame) {
- Log.logError(((MalformedFrame) frame).getMessage());
- if (streamid == 0) {
- framesDecoder.close("Malformed frame on stream 0");
- protocolError(((MalformedFrame) frame).getErrorCode(),
- ((MalformedFrame) frame).getMessage());
- } else {
- debug.log(Level.DEBUG, () -> "Reset stream: "
- + ((MalformedFrame) frame).getMessage());
- resetStream(streamid, ((MalformedFrame) frame).getErrorCode());
- }
- return;
- }
- if (streamid == 0) {
- handleConnectionFrame(frame);
- } else {
- if (frame instanceof SettingsFrame) {
- // The stream identifier for a SETTINGS frame MUST be zero
- framesDecoder.close(
- "The stream identifier for a SETTINGS frame MUST be zero");
- protocolError(GoAwayFrame.PROTOCOL_ERROR);
- return;
- }
-
- Stream<?> stream = getStream(streamid);
- if (stream == null) {
- // Should never receive a frame with unknown stream id
-
- if (frame instanceof HeaderFrame) {
- // always decode the headers as they may affect
- // connection-level HPACK decoding state
- HeaderDecoder decoder = new LoggingHeaderDecoder(new HeaderDecoder());
- decodeHeaders((HeaderFrame) frame, decoder);
- }
-
- if (!(frame instanceof ResetFrame)) {
- if (isSeverInitiatedStream(streamid)) {
- if (streamid < nextPushStream) {
- // trailing data on a cancelled push promise stream,
- // reset will already have been sent, ignore
- Log.logTrace("Ignoring cancelled push promise frame " + frame);
- } else {
- resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
- }
- } else if (streamid >= nextstreamid) {
- // otherwise the stream has already been reset/closed
- resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
- }
- }
- return;
- }
- if (frame instanceof PushPromiseFrame) {
- PushPromiseFrame pp = (PushPromiseFrame)frame;
- handlePushPromise(stream, pp);
- } else if (frame instanceof HeaderFrame) {
- // decode headers (or continuation)
- decodeHeaders((HeaderFrame) frame, stream.rspHeadersConsumer());
- stream.incoming(frame);
- } else {
- stream.incoming(frame);
- }
- }
- }
-
- private <T> void handlePushPromise(Stream<T> parent, PushPromiseFrame pp)
- throws IOException
- {
- // always decode the headers as they may affect connection-level HPACK
- // decoding state
- HeaderDecoder decoder = new LoggingHeaderDecoder(new HeaderDecoder());
- decodeHeaders(pp, decoder);
-
- HttpRequestImpl parentReq = parent.request;
- int promisedStreamid = pp.getPromisedStream();
- if (promisedStreamid != nextPushStream) {
- resetStream(promisedStreamid, ResetFrame.PROTOCOL_ERROR);
- return;
- } else {
- nextPushStream += 2;
- }
-
- HttpHeadersImpl headers = decoder.headers();
- HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest(parentReq, headers);
- Exchange<T> pushExch = new Exchange<>(pushReq, parent.exchange.multi);
- Stream.PushedStream<T> pushStream = createPushStream(parent, pushExch);
- pushExch.exchImpl = pushStream;
- pushStream.registerStream(promisedStreamid);
- parent.incoming_pushPromise(pushReq, pushStream);
- }
-
- private void handleConnectionFrame(Http2Frame frame)
- throws IOException
- {
- switch (frame.type()) {
- case SettingsFrame.TYPE:
- handleSettings((SettingsFrame)frame);
- break;
- case PingFrame.TYPE:
- handlePing((PingFrame)frame);
- break;
- case GoAwayFrame.TYPE:
- handleGoAway((GoAwayFrame)frame);
- break;
- case WindowUpdateFrame.TYPE:
- handleWindowUpdate((WindowUpdateFrame)frame);
- break;
- default:
- protocolError(ErrorFrame.PROTOCOL_ERROR);
- }
- }
-
- void resetStream(int streamid, int code) throws IOException {
- Log.logError(
- "Resetting stream {0,number,integer} with error code {1,number,integer}",
- streamid, code);
- ResetFrame frame = new ResetFrame(streamid, code);
- sendFrame(frame);
- closeStream(streamid);
- }
-
- void closeStream(int streamid) {
- debug.log(Level.DEBUG, "Closed stream %d", streamid);
- Stream<?> s = streams.remove(streamid);
- if (s != null) {
- // 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.
- client().unreference();
- }
- // ## Remove s != null. It is a hack for delayed cancellation,reset
- if (s != null && !(s instanceof Stream.PushedStream)) {
- // Since PushStreams have no request body, then they have no
- // corresponding entry in the window controller.
- windowController.removeStream(streamid);
- }
- if (singleStream() && streams.isEmpty()) {
- // should be only 1 stream, but there might be more if server push
- close();
- }
- }
-
- /**
- * Increments this connection's send Window by the amount in the given frame.
- */
- private void handleWindowUpdate(WindowUpdateFrame f)
- throws IOException
- {
- int amount = f.getUpdate();
- if (amount <= 0) {
- // ## temporarily disable to workaround a bug in Jetty where it
- // ## sends Window updates with a 0 update value.
- //protocolError(ErrorFrame.PROTOCOL_ERROR);
- } else {
- boolean success = windowController.increaseConnectionWindow(amount);
- if (!success) {
- protocolError(ErrorFrame.FLOW_CONTROL_ERROR); // overflow
- }
- }
- }
-
- private void protocolError(int errorCode)
- throws IOException
- {
- protocolError(errorCode, null);
- }
-
- private void protocolError(int errorCode, String msg)
- throws IOException
- {
- GoAwayFrame frame = new GoAwayFrame(0, errorCode);
- sendFrame(frame);
- shutdown(new IOException("protocol error" + (msg == null?"":(": " + msg))));
- }
-
- private void handleSettings(SettingsFrame frame)
- throws IOException
- {
- assert frame.streamid() == 0;
- if (!frame.getFlag(SettingsFrame.ACK)) {
- int oldWindowSize = serverSettings.getParameter(INITIAL_WINDOW_SIZE);
- int newWindowSize = frame.getParameter(INITIAL_WINDOW_SIZE);
- int diff = newWindowSize - oldWindowSize;
- if (diff != 0) {
- windowController.adjustActiveStreams(diff);
- }
- serverSettings = frame;
- sendFrame(new SettingsFrame(SettingsFrame.ACK));
- }
- }
-
- private void handlePing(PingFrame frame)
- throws IOException
- {
- frame.setFlag(PingFrame.ACK);
- sendUnorderedFrame(frame);
- }
-
- private void handleGoAway(GoAwayFrame frame)
- throws IOException
- {
- shutdown(new IOException(
- String.valueOf(connection.channel().getLocalAddress())
- +": GOAWAY received"));
- }
-
- /**
- * Max frame size we are allowed to send
- */
- public int getMaxSendFrameSize() {
- int param = serverSettings.getParameter(MAX_FRAME_SIZE);
- if (param == -1) {
- param = DEFAULT_FRAME_SIZE;
- }
- return param;
- }
-
- /**
- * Max frame size we will receive
- */
- public int getMaxReceiveFrameSize() {
- return clientSettings.getParameter(MAX_FRAME_SIZE);
- }
-
- private static final String CLIENT_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
-
- private static final byte[] PREFACE_BYTES =
- CLIENT_PREFACE.getBytes(StandardCharsets.ISO_8859_1);
-
- /**
- * Sends Connection preface and Settings frame with current preferred
- * values
- */
- private void sendConnectionPreface() throws IOException {
- Log.logTrace("{0}: start sending connection preface to {1}",
- connection.channel().getLocalAddress(),
- connection.address());
- SettingsFrame sf = new SettingsFrame(clientSettings);
- int initialWindowSize = sf.getParameter(INITIAL_WINDOW_SIZE);
- ByteBuffer buf = framesEncoder.encodeConnectionPreface(PREFACE_BYTES, sf);
- Log.logFrames(sf, "OUT");
- // send preface bytes and SettingsFrame together
- HttpPublisher publisher = publisher();
- publisher.enqueue(List.of(buf));
- publisher.signalEnqueued();
- // mark preface sent.
- framesController.markPrefaceSent();
- Log.logTrace("PREFACE_BYTES sent");
- Log.logTrace("Settings Frame sent");
-
- // send a Window update for the receive buffer we are using
- // minus the initial 64 K specified in protocol
- final int len = windowUpdater.initialWindowSize - initialWindowSize;
- if (len > 0) {
- windowUpdater.sendWindowUpdate(len);
- }
- // there will be an ACK to the windows update - which should
- // cause any pending data stored before the preface was sent to be
- // flushed (see PrefaceController).
- Log.logTrace("finished sending connection preface");
- debug.log(Level.DEBUG, "Triggering processing of buffered data"
- + " after sending connection preface");
- subscriber.onNext(List.of(EMPTY_TRIGGER));
- }
-
- /**
- * Returns an existing Stream with given id, or null if doesn't exist
- */
- @SuppressWarnings("unchecked")
- <T> Stream<T> getStream(int streamid) {
- return (Stream<T>)streams.get(streamid);
- }
-
- /**
- * Creates Stream with given id.
- */
- final <T> Stream<T> createStream(Exchange<T> exchange) {
- Stream<T> stream = new Stream<>(this, exchange, windowController);
- return stream;
- }
-
- <T> Stream.PushedStream<T> createPushStream(Stream<T> parent, Exchange<T> pushEx) {
- PushGroup<T> pg = parent.exchange.getPushGroup();
- return new Stream.PushedStream<>(pg, this, pushEx);
- }
-
- <T> void putStream(Stream<T> stream, int streamid) {
- // increment the reference count on the HttpClientImpl
- // to prevent the SelectorManager thread from exiting until
- // the stream is closed.
- client().reference();
- streams.put(streamid, stream);
- }
-
- /**
- * Encode the headers into a List<ByteBuffer> and then create HEADERS
- * and CONTINUATION frames from the list and return the List<Http2Frame>.
- */
- private List<HeaderFrame> encodeHeaders(OutgoingHeaders<Stream<?>> frame) {
- List<ByteBuffer> buffers = encodeHeadersImpl(
- getMaxSendFrameSize(),
- frame.getAttachment().getRequestPseudoHeaders(),
- frame.getUserHeaders(),
- frame.getSystemHeaders());
-
- List<HeaderFrame> frames = new ArrayList<>(buffers.size());
- Iterator<ByteBuffer> bufIterator = buffers.iterator();
- HeaderFrame oframe = new HeadersFrame(frame.streamid(), frame.getFlags(), bufIterator.next());
- frames.add(oframe);
- while(bufIterator.hasNext()) {
- oframe = new ContinuationFrame(frame.streamid(), bufIterator.next());
- frames.add(oframe);
- }
- oframe.setFlag(HeaderFrame.END_HEADERS);
- return frames;
- }
-
- // Dedicated cache for headers encoding ByteBuffer.
- // There can be no concurrent access to this buffer as all access to this buffer
- // and its content happen within a single critical code block section protected
- // by the sendLock. / (see sendFrame())
- // private final ByteBufferPool headerEncodingPool = new ByteBufferPool();
-
- private ByteBuffer getHeaderBuffer(int maxFrameSize) {
- ByteBuffer buf = ByteBuffer.allocate(maxFrameSize);
- buf.limit(maxFrameSize);
- return buf;
- }
-
- /*
- * Encodes all the headers from the given HttpHeaders into the given List
- * of buffers.
- *
- * From https://tools.ietf.org/html/rfc7540#section-8.1.2 :
- *
- * ...Just as in HTTP/1.x, header field names are strings of ASCII
- * characters that are compared in a case-insensitive fashion. However,
- * header field names MUST be converted to lowercase prior to their
- * encoding in HTTP/2...
- */
- private List<ByteBuffer> encodeHeadersImpl(int maxFrameSize, HttpHeaders... headers) {
- ByteBuffer buffer = getHeaderBuffer(maxFrameSize);
- List<ByteBuffer> buffers = new ArrayList<>();
- for(HttpHeaders header : headers) {
- for (Map.Entry<String, List<String>> e : header.map().entrySet()) {
- String lKey = e.getKey().toLowerCase();
- List<String> values = e.getValue();
- for (String value : values) {
- hpackOut.header(lKey, value);
- while (!hpackOut.encode(buffer)) {
- buffer.flip();
- buffers.add(buffer);
- buffer = getHeaderBuffer(maxFrameSize);
- }
- }
- }
- }
- buffer.flip();
- buffers.add(buffer);
- return buffers;
- }
-
- private List<ByteBuffer> encodeHeaders(OutgoingHeaders<Stream<?>> oh, Stream<?> stream) {
- oh.streamid(stream.streamid);
- if (Log.headers()) {
- StringBuilder sb = new StringBuilder("HEADERS FRAME (stream=");
- sb.append(stream.streamid).append(")\n");
- Log.dumpHeaders(sb, " ", oh.getAttachment().getRequestPseudoHeaders());
- Log.dumpHeaders(sb, " ", oh.getSystemHeaders());
- Log.dumpHeaders(sb, " ", oh.getUserHeaders());
- Log.logHeaders(sb.toString());
- }
- List<HeaderFrame> frames = encodeHeaders(oh);
- return encodeFrames(frames);
- }
-
- private List<ByteBuffer> encodeFrames(List<HeaderFrame> frames) {
- if (Log.frames()) {
- frames.forEach(f -> Log.logFrames(f, "OUT"));
- }
- return framesEncoder.encodeFrames(frames);
- }
-
- private Stream<?> registerNewStream(OutgoingHeaders<Stream<?>> oh) {
- Stream<?> stream = oh.getAttachment();
- int streamid = nextstreamid;
- nextstreamid += 2;
- stream.registerStream(streamid);
- // set outgoing window here. This allows thread sending
- // body to proceed.
- windowController.registerStream(streamid, getInitialSendWindowSize());
- return stream;
- }
-
- private final Object sendlock = new Object();
-
- void sendFrame(Http2Frame frame) {
- try {
- HttpPublisher publisher = publisher();
- synchronized (sendlock) {
- if (frame instanceof OutgoingHeaders) {
- @SuppressWarnings("unchecked")
- OutgoingHeaders<Stream<?>> oh = (OutgoingHeaders<Stream<?>>) frame;
- Stream<?> stream = registerNewStream(oh);
- // provide protection from inserting unordered frames between Headers and Continuation
- publisher.enqueue(encodeHeaders(oh, stream));
- } else {
- publisher.enqueue(encodeFrame(frame));
- }
- }
- publisher.signalEnqueued();
- } catch (IOException e) {
- if (!closed) {
- Log.logError(e);
- shutdown(e);
- }
- }
- }
-
- private List<ByteBuffer> encodeFrame(Http2Frame frame) {
- Log.logFrames(frame, "OUT");
- return framesEncoder.encodeFrame(frame);
- }
-
- void sendDataFrame(DataFrame frame) {
- try {
- HttpPublisher publisher = publisher();
- publisher.enqueue(encodeFrame(frame));
- publisher.signalEnqueued();
- } catch (IOException e) {
- if (!closed) {
- Log.logError(e);
- shutdown(e);
- }
- }
- }
-
- /*
- * Direct call of the method bypasses synchronization on "sendlock" and
- * allowed only of control frames: WindowUpdateFrame, PingFrame and etc.
- * prohibited for such frames as DataFrame, HeadersFrame, ContinuationFrame.
- */
- void sendUnorderedFrame(Http2Frame frame) {
- try {
- HttpPublisher publisher = publisher();
- publisher.enqueueUnordered(encodeFrame(frame));
- publisher.signalEnqueued();
- } catch (IOException e) {
- if (!closed) {
- Log.logError(e);
- shutdown(e);
- }
- }
- }
-
- /**
- * A simple tube subscriber for reading from the connection flow.
- */
- final class Http2TubeSubscriber implements TubeSubscriber {
- volatile Flow.Subscription subscription;
- volatile boolean completed;
- volatile boolean dropped;
- volatile Throwable error;
- final ConcurrentLinkedQueue<ByteBuffer> queue
- = new ConcurrentLinkedQueue<>();
- final SequentialScheduler scheduler =
- SequentialScheduler.synchronizedScheduler(this::processQueue);
-
- final void processQueue() {
- try {
- while (!queue.isEmpty() && !scheduler.isStopped()) {
- ByteBuffer buffer = queue.poll();
- debug.log(Level.DEBUG,
- "sending %d to Http2Connection.asyncReceive",
- buffer.remaining());
- asyncReceive(buffer);
- }
- } catch (Throwable t) {
- Throwable x = error;
- if (x == null) error = t;
- } finally {
- Throwable x = error;
- if (x != null) {
- debug.log(Level.DEBUG, "Stopping scheduler", x);
- scheduler.stop();
- Http2Connection.this.shutdown(x);
- }
- }
- }
-
- @Override
- 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;
- // TODO FIXME: request(1) should be done by the delegate.
- if (!completed) {
- debug.log(Level.DEBUG, "onSubscribe: requesting Long.MAX_VALUE for reading");
- subscription.request(Long.MAX_VALUE);
- } else {
- debug.log(Level.DEBUG, "onSubscribe: already completed");
- }
- }
-
- @Override
- public void onNext(List<ByteBuffer> item) {
- debug.log(Level.DEBUG, () -> "onNext: got " + Utils.remaining(item)
- + " bytes in " + item.size() + " buffers");
- queue.addAll(item);
- scheduler.runOrSchedule(client().theExecutor());
- }
-
- @Override
- public void onError(Throwable throwable) {
- debug.log(Level.DEBUG, () -> "onError: " + throwable);
- error = throwable;
- completed = true;
- scheduler.runOrSchedule(client().theExecutor());
- }
-
- @Override
- public void onComplete() {
- debug.log(Level.DEBUG, "EOF");
- error = new EOFException("EOF reached while reading");
- completed = true;
- scheduler.runOrSchedule(client().theExecutor());
- }
-
- @Override
- public void dropSubscription() {
- debug.log(Level.DEBUG, "dropSubscription");
- // we could probably set subscription to null here...
- // then we might not need the 'dropped' boolean?
- dropped = true;
- }
- }
-
- @Override
- public final String toString() {
- return dbgString();
- }
-
- final String dbgString() {
- return "Http2Connection("
- + connection.getConnectionFlow() + ")";
- }
-
- final class LoggingHeaderDecoder extends HeaderDecoder {
-
- private final HeaderDecoder delegate;
- private final System.Logger debugHpack =
- Utils.getHpackLogger(this::dbgString, DEBUG_HPACK);
-
- LoggingHeaderDecoder(HeaderDecoder delegate) {
- this.delegate = delegate;
- }
-
- String dbgString() {
- return Http2Connection.this.dbgString() + "/LoggingHeaderDecoder";
- }
-
- @Override
- public void onDecoded(CharSequence name, CharSequence value) {
- delegate.onDecoded(name, value);
- }
-
- @Override
- public void onIndexed(int index,
- CharSequence name,
- CharSequence value) {
- debugHpack.log(Level.DEBUG, "onIndexed(%s, %s, %s)%n",
- index, name, value);
- delegate.onIndexed(index, name, value);
- }
-
- @Override
- public void onLiteral(int index,
- CharSequence name,
- CharSequence value,
- boolean valueHuffman) {
- debugHpack.log(Level.DEBUG, "onLiteral(%s, %s, %s, %s)%n",
- index, name, value, valueHuffman);
- delegate.onLiteral(index, name, value, valueHuffman);
- }
-
- @Override
- public void onLiteral(CharSequence name,
- boolean nameHuffman,
- CharSequence value,
- boolean valueHuffman) {
- debugHpack.log(Level.DEBUG, "onLiteral(%s, %s, %s, %s)%n",
- name, nameHuffman, value, valueHuffman);
- delegate.onLiteral(name, nameHuffman, value, valueHuffman);
- }
-
- @Override
- public void onLiteralNeverIndexed(int index,
- CharSequence name,
- CharSequence value,
- boolean valueHuffman) {
- debugHpack.log(Level.DEBUG, "onLiteralNeverIndexed(%s, %s, %s, %s)%n",
- index, name, value, valueHuffman);
- delegate.onLiteralNeverIndexed(index, name, value, valueHuffman);
- }
-
- @Override
- public void onLiteralNeverIndexed(CharSequence name,
- boolean nameHuffman,
- CharSequence value,
- boolean valueHuffman) {
- debugHpack.log(Level.DEBUG, "onLiteralNeverIndexed(%s, %s, %s, %s)%n",
- name, nameHuffman, value, valueHuffman);
- delegate.onLiteralNeverIndexed(name, nameHuffman, value, valueHuffman);
- }
-
- @Override
- public void onLiteralWithIndexing(int index,
- CharSequence name,
- CharSequence value,
- boolean valueHuffman) {
- debugHpack.log(Level.DEBUG, "onLiteralWithIndexing(%s, %s, %s, %s)%n",
- index, name, value, valueHuffman);
- delegate.onLiteralWithIndexing(index, name, value, valueHuffman);
- }
-
- @Override
- public void onLiteralWithIndexing(CharSequence name,
- boolean nameHuffman,
- CharSequence value,
- boolean valueHuffman) {
- debugHpack.log(Level.DEBUG, "onLiteralWithIndexing(%s, %s, %s, %s)%n",
- name, nameHuffman, value, valueHuffman);
- delegate.onLiteralWithIndexing(name, nameHuffman, value, valueHuffman);
- }
-
- @Override
- public void onSizeUpdate(int capacity) {
- debugHpack.log(Level.DEBUG, "onSizeUpdate(%s)%n", capacity);
- delegate.onSizeUpdate(capacity);
- }
-
- @Override
- HttpHeadersImpl headers() {
- return delegate.headers();
- }
- }
-
- static class HeaderDecoder implements DecodingCallback {
- HttpHeadersImpl headers;
-
- HeaderDecoder() {
- this.headers = new HttpHeadersImpl();
- }
-
- @Override
- public void onDecoded(CharSequence name, CharSequence value) {
- headers.addHeader(name.toString(), value.toString());
- }
-
- HttpHeadersImpl headers() {
- return headers;
- }
- }
-
- static final class ConnectionWindowUpdateSender extends WindowUpdateSender {
-
- final int initialWindowSize;
- public ConnectionWindowUpdateSender(Http2Connection connection,
- int initialWindowSize) {
- super(connection, initialWindowSize);
- this.initialWindowSize = initialWindowSize;
- }
-
- @Override
- int getStreamId() {
- return 0;
- }
- }
-
- /**
- * Thrown when https handshake negotiates http/1.1 alpn instead of h2
- */
- static final class ALPNException extends IOException {
- private static final long serialVersionUID = 0L;
- final transient AbstractAsyncSSLConnection connection;
-
- ALPNException(String msg, AbstractAsyncSSLConnection connection) {
- super(msg);
- this.connection = connection;
- }
-
- AbstractAsyncSSLConnection getConnection() {
- return connection;
- }
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClient.java Tue Feb 06 11:39:55 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClient.java Tue Feb 06 14:10:28 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * 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
@@ -31,7 +31,6 @@
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
-import java.net.URI;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@@ -41,6 +40,7 @@
import javax.net.ssl.SSLParameters;
import jdk.incubator.http.HttpResponse.BodyHandler;
import jdk.incubator.http.HttpResponse.PushPromiseHandler;
+import jdk.incubator.http.internal.HttpClientBuilderImpl;
/**
* A container for configuration information common to multiple {@link
@@ -111,9 +111,10 @@
/**
* A proxy selector that always return {@link Proxy#NO_PROXY} implying
* a direct connection.
- * 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.
+ *
+ * <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);
@@ -207,7 +208,7 @@
* 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.
+ * @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
*
@@ -335,9 +336,9 @@
* 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
+ * @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
*/
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientBuilderImpl.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.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 jdk.incubator.http.internal.common.Utils;
-import static java.util.Objects.requireNonNull;
-
-class HttpClientBuilderImpl extends 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);
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientFacade.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,145 +0,0 @@
-/*
- * Copyright (c) 2017, 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.incubator.http;
-
-import java.io.IOException;
-import java.lang.ref.Reference;
-import java.net.Authenticator;
-import java.net.CookieHandler;
-import java.net.ProxySelector;
-import java.net.URI;
-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 jdk.incubator.http.HttpResponse.BodyHandler;
-import jdk.incubator.http.HttpResponse.PushPromiseHandler;
-
-/**
- * An HttpClientFacade is a simple class that wraps an HttpClient implementation
- * and delegates everything to its implementation delegate.
- */
-final class HttpClientFacade extends HttpClient {
-
- final HttpClientImpl impl;
-
- /**
- * Creates an HttpClientFacade.
- */
- HttpClientFacade(HttpClientImpl impl) {
- this.impl = impl;
- }
-
- @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();
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientImpl.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1013 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.http;
-
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLParameters;
-import java.io.IOException;
-import java.lang.System.Logger.Level;
-import java.lang.ref.WeakReference;
-import java.net.Authenticator;
-import java.net.CookieHandler;
-import java.net.ProxySelector;
-import java.nio.channels.CancelledKeyException;
-import java.nio.channels.ClosedChannelException;
-import java.nio.channels.SelectableChannel;
-import java.nio.channels.SelectionKey;
-import java.nio.channels.Selector;
-import java.nio.channels.SocketChannel;
-import java.security.AccessControlContext;
-import java.security.AccessController;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivilegedAction;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.stream.Stream;
-import jdk.incubator.http.HttpResponse.BodyHandler;
-import jdk.incubator.http.HttpResponse.PushPromiseHandler;
-import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.Pair;
-import jdk.incubator.http.internal.common.Utils;
-import jdk.incubator.http.internal.websocket.BuilderImpl;
-import jdk.internal.misc.InnocuousThread;
-
-/**
- * Client implementation. Contains all configuration information and also
- * the selector manager thread which allows async events to be registered
- * and delivered when they occur. See AsyncEvent.
- */
-class HttpClientImpl extends HttpClient {
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- static final boolean DEBUGELAPSED = Utils.TESTING || DEBUG; // Revisit: temporary dev flag.
- static final boolean DEBUGTIMEOUT = false; // Revisit: temporary dev flag.
- final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
- final System.Logger debugelapsed = Utils.getDebugLogger(this::dbgString, DEBUGELAPSED);
- final System.Logger debugtimeout = Utils.getDebugLogger(this::dbgString, DEBUGTIMEOUT);
- static final AtomicLong CLIENT_IDS = new AtomicLong();
-
- // Define the default factory as a static inner class
- // that embeds all the necessary logic to avoid
- // the risk of using a lambda that might keep a reference on the
- // HttpClient instance from which it was created (helps with
- // heapdump analysis).
- private static final class DefaultThreadFactory implements ThreadFactory {
- private final String namePrefix;
- private final AtomicInteger nextId = new AtomicInteger();
-
- DefaultThreadFactory(long clientID) {
- namePrefix = "HttpClient-" + clientID + "-Worker-";
- }
-
- @Override
- public Thread newThread(Runnable r) {
- String name = namePrefix + nextId.getAndIncrement();
- Thread t;
- if (System.getSecurityManager() == null) {
- t = new Thread(null, r, name, 0, false);
- } else {
- t = InnocuousThread.newThread(name, r);
- }
- t.setDaemon(true);
- return t;
- }
- }
-
- private final CookieHandler cookieHandler;
- private final Redirect followRedirects;
- private final Optional<ProxySelector> userProxySelector;
- private final ProxySelector proxySelector;
- private final Authenticator authenticator;
- private final Version version;
- private final ConnectionPool connections;
- private final Executor executor;
- private final boolean isDefaultExecutor;
- // Security parameters
- private final SSLContext sslContext;
- private final SSLParameters sslParams;
- private final SelectorManager selmgr;
- private final FilterFactory filters;
- private final Http2ClientImpl client2;
- private final long id;
- private final String dbgTag;
-
- // This reference is used to keep track of the facade HttpClient
- // that was returned to the application code.
- // It makes it possible to know when the application no longer
- // holds any reference to the HttpClient.
- // Unfortunately, this information is not enough to know when
- // to exit the SelectorManager thread. Because of the asynchronous
- // nature of the API, we also need to wait until all pending operations
- // have completed.
- private final WeakReference<HttpClientFacade> facadeRef;
-
- // This counter keeps track of the number of operations pending
- // on the HttpClient. The SelectorManager thread will wait
- // until there are no longer any pending operations and the
- // facadeRef is cleared before exiting.
- //
- // The pendingOperationCount is incremented every time a send/sendAsync
- // operation is invoked on the HttpClient, and is decremented when
- // the HttpResponse<T> object is returned to the user.
- // However, at this point, the body may not have been fully read yet.
- // This is the case when the response T is implemented as a streaming
- // subscriber (such as an InputStream).
- //
- // To take care of this issue the pendingOperationCount will additionally
- // be incremented/decremented in the following cases:
- //
- // 1. For HTTP/2 it is incremented when a stream is added to the
- // Http2Connection streams map, and decreased when the stream is removed
- // from the map. This should also take care of push promises.
- // 2. For WebSocket the count is increased when creating a
- // DetachedConnectionChannel for the socket, and decreased
- // when the the channel is closed.
- // In addition, the HttpClient facade is passed to the WebSocket builder,
- // (instead of the client implementation delegate).
- // 3. For HTTP/1.1 the count is incremented before starting to parse the body
- // response, and decremented when the parser has reached the end of the
- // response body flow.
- //
- // This should ensure that the selector manager thread remains alive until
- // the response has been fully received or the web socket is closed.
- private final AtomicLong pendingOperationCount = new AtomicLong();
- private final AtomicLong pendingWebSocketCount = new AtomicLong();
- private final AtomicLong pendingHttpRequestCount = new AtomicLong();
-
- /** A Set of, deadline first, ordered timeout events. */
- private final TreeSet<TimeoutEvent> timeouts;
-
- /**
- * This is a bit tricky:
- * 1. an HttpClientFacade has a final HttpClientImpl field.
- * 2. an HttpClientImpl has a final WeakReference<HttpClientFacade> field,
- * where the referent is the facade created for that instance.
- * 3. We cannot just create the HttpClientFacade in the HttpClientImpl
- * constructor, because it would be only weakly referenced and could
- * be GC'ed before we can return it.
- * The solution is to use an instance of SingleFacadeFactory which will
- * allow the caller of new HttpClientImpl(...) to retrieve the facade
- * after the HttpClientImpl has been created.
- */
- private static final class SingleFacadeFactory {
- HttpClientFacade facade;
- HttpClientFacade createFacade(HttpClientImpl impl) {
- assert facade == null;
- return (facade = new HttpClientFacade(impl));
- }
- }
-
- static HttpClientFacade create(HttpClientBuilderImpl builder) {
- SingleFacadeFactory facadeFactory = new SingleFacadeFactory();
- HttpClientImpl impl = new HttpClientImpl(builder, facadeFactory);
- impl.start();
- assert facadeFactory.facade != null;
- assert impl.facadeRef.get() == facadeFactory.facade;
- return facadeFactory.facade;
- }
-
- private HttpClientImpl(HttpClientBuilderImpl builder,
- SingleFacadeFactory facadeFactory) {
- id = CLIENT_IDS.incrementAndGet();
- dbgTag = "HttpClientImpl(" + id +")";
- if (builder.sslContext == null) {
- try {
- sslContext = SSLContext.getDefault();
- } catch (NoSuchAlgorithmException ex) {
- throw new InternalError(ex);
- }
- } else {
- sslContext = builder.sslContext;
- }
- Executor ex = builder.executor;
- if (ex == null) {
- ex = Executors.newCachedThreadPool(new DefaultThreadFactory(id));
- isDefaultExecutor = true;
- } else {
- ex = builder.executor;
- isDefaultExecutor = false;
- }
- facadeRef = new WeakReference<>(facadeFactory.createFacade(this));
- client2 = new Http2ClientImpl(this);
- executor = ex;
- cookieHandler = builder.cookieHandler;
- followRedirects = builder.followRedirects == null ?
- Redirect.NEVER : builder.followRedirects;
- this.userProxySelector = Optional.ofNullable(builder.proxy);
- this.proxySelector = userProxySelector
- .orElseGet(HttpClientImpl::getDefaultProxySelector);
- debug.log(Level.DEBUG, "proxySelector is %s (user-supplied=%s)",
- this.proxySelector, userProxySelector.isPresent());
- authenticator = builder.authenticator;
- if (builder.version == null) {
- version = HttpClient.Version.HTTP_2;
- } else {
- version = builder.version;
- }
- if (builder.sslParams == null) {
- sslParams = getDefaultParams(sslContext);
- } else {
- sslParams = builder.sslParams;
- }
- connections = new ConnectionPool(id);
- connections.start();
- timeouts = new TreeSet<>();
- try {
- selmgr = new SelectorManager(this);
- } catch (IOException e) {
- // unlikely
- throw new InternalError(e);
- }
- selmgr.setDaemon(true);
- filters = new FilterFactory();
- initFilters();
- assert facadeRef.get() != null;
- }
-
- private void start() {
- selmgr.start();
- }
-
- // Called from the SelectorManager thread, just before exiting.
- // Clears the HTTP/1.1 and HTTP/2 cache, ensuring that the connections
- // that may be still lingering there are properly closed (and their
- // possibly still opened SocketChannel released).
- private void stop() {
- // Clears HTTP/1.1 cache and close its connections
- connections.stop();
- // Clears HTTP/2 cache and close its connections.
- client2.stop();
- }
-
- private static SSLParameters getDefaultParams(SSLContext ctx) {
- SSLParameters params = ctx.getSupportedSSLParameters();
- params.setProtocols(new String[]{"TLSv1.2"});
- return params;
- }
-
- private static ProxySelector getDefaultProxySelector() {
- PrivilegedAction<ProxySelector> action = ProxySelector::getDefault;
- return AccessController.doPrivileged(action);
- }
-
- // Returns the facade that was returned to the application code.
- // May be null if that facade is no longer referenced.
- final HttpClientFacade facade() {
- return facadeRef.get();
- }
-
- // Increments the pendingOperationCount.
- final long reference() {
- pendingHttpRequestCount.incrementAndGet();
- return pendingOperationCount.incrementAndGet();
- }
-
- // Decrements the pendingOperationCount.
- final long unreference() {
- final long count = pendingOperationCount.decrementAndGet();
- final long httpCount = pendingHttpRequestCount.decrementAndGet();
- final long webSocketCount = pendingWebSocketCount.get();
- if (count == 0 && facade() == null) {
- selmgr.wakeupSelector();
- }
- assert httpCount >= 0 : "count of HTTP operations < 0";
- assert webSocketCount >= 0 : "count of WS operations < 0";
- assert count >= 0 : "count of pending operations < 0";
- return count;
- }
-
- // Increments the pendingOperationCount.
- final long webSocketOpen() {
- pendingWebSocketCount.incrementAndGet();
- return pendingOperationCount.incrementAndGet();
- }
-
- // Decrements the pendingOperationCount.
- final long webSocketClose() {
- final long count = pendingOperationCount.decrementAndGet();
- final long webSocketCount = pendingWebSocketCount.decrementAndGet();
- final long httpCount = pendingHttpRequestCount.get();
- if (count == 0 && facade() == null) {
- selmgr.wakeupSelector();
- }
- assert httpCount >= 0 : "count of HTTP operations < 0";
- assert webSocketCount >= 0 : "count of WS operations < 0";
- assert count >= 0 : "count of pending operations < 0";
- return count;
- }
-
- // Returns the pendingOperationCount.
- final long referenceCount() {
- return pendingOperationCount.get();
- }
-
- // Called by the SelectorManager thread to figure out whether it's time
- // to terminate.
- final boolean isReferenced() {
- HttpClient facade = facade();
- return facade != null || referenceCount() > 0;
- }
-
- /**
- * Wait for activity on given exchange.
- * The following occurs in the SelectorManager thread.
- *
- * 1) add to selector
- * 2) If selector fires for this exchange then
- * call AsyncEvent.handle()
- *
- * If exchange needs to change interest ops, then call registerEvent() again.
- */
- void registerEvent(AsyncEvent exchange) throws IOException {
- selmgr.register(exchange);
- }
-
- /**
- * Only used from RawChannel to disconnect the channel from
- * the selector
- */
- void cancelRegistration(SocketChannel s) {
- selmgr.cancel(s);
- }
-
- /**
- * Allows an AsyncEvent to modify its interestOps.
- * @param event The modified event.
- */
- void eventUpdated(AsyncEvent event) throws ClosedChannelException {
- assert !(event instanceof AsyncTriggerEvent);
- selmgr.eventUpdated(event);
- }
-
- boolean isSelectorThread() {
- return Thread.currentThread() == selmgr;
- }
-
- Http2ClientImpl client2() {
- return client2;
- }
-
- private void debugCompleted(String tag, long startNanos, HttpRequest req) {
- if (debugelapsed.isLoggable(Level.DEBUG)) {
- debugelapsed.log(Level.DEBUG, () -> tag + " elapsed "
- + (System.nanoTime() - startNanos)/1000_000L
- + " millis for " + req.method()
- + " to " + req.uri());
- }
- }
-
- @Override
- public <T> HttpResponse<T>
- send(HttpRequest req, BodyHandler<T> responseHandler)
- throws IOException, InterruptedException
- {
- try {
- return sendAsync(req, responseHandler).get();
- } catch (ExecutionException e) {
- Throwable t = e.getCause();
- if (t instanceof Error)
- throw (Error)t;
- if (t instanceof RuntimeException)
- throw (RuntimeException)t;
- else if (t instanceof IOException)
- throw Utils.getIOException(t);
- else
- throw new InternalError("Unexpected exception", t);
- }
- }
-
- @Override
- public <T> CompletableFuture<HttpResponse<T>>
- sendAsync(HttpRequest userRequest, BodyHandler<T> responseHandler)
- {
- return sendAsync(userRequest, responseHandler, null);
- }
-
-
- @Override
- public <T> CompletableFuture<HttpResponse<T>>
- sendAsync(HttpRequest userRequest,
- BodyHandler<T> responseHandler,
- PushPromiseHandler<T> pushPromiseHandler)
- {
- AccessControlContext acc = null;
- if (System.getSecurityManager() != null)
- acc = AccessController.getContext();
-
- // Clone the, possibly untrusted, HttpRequest
- HttpRequestImpl requestImpl = new HttpRequestImpl(userRequest, proxySelector, acc);
- if (requestImpl.method().equals("CONNECT"))
- throw new IllegalArgumentException("Unsupported method CONNECT");
-
- long start = DEBUGELAPSED ? System.nanoTime() : 0;
- reference();
- try {
- debugelapsed.log(Level.DEBUG, "ClientImpl (async) send %s", userRequest);
-
- MultiExchange<T> mex = new MultiExchange<>(userRequest,
- requestImpl,
- this,
- responseHandler,
- pushPromiseHandler,
- acc);
- CompletableFuture<HttpResponse<T>> res =
- mex.responseAsync().whenComplete((b,t) -> unreference());
- if (DEBUGELAPSED) {
- res = res.whenComplete(
- (b,t) -> debugCompleted("ClientImpl (async)", start, userRequest));
- }
- // makes sure that any dependent actions happen in the executor
- if (acc != null) {
- res.whenCompleteAsync((r, t) -> { /* do nothing */},
- new PrivilegedExecutor(executor, acc));
- }
-
- return res;
- } catch(Throwable t) {
- unreference();
- debugCompleted("ClientImpl (async)", start, userRequest);
- throw t;
- }
- }
-
- // Main loop for this client's selector
- private final static class SelectorManager extends Thread {
-
- // For testing purposes we have an internal System property that
- // can control the frequency at which the selector manager will wake
- // up when there are no pending operations.
- // Increasing the frequency (shorter delays) might allow the selector
- // to observe that the facade is no longer referenced and might allow
- // the selector thread to terminate more timely - for when nothing is
- // ongoing it will only check for that condition every NODEADLINE ms.
- // To avoid misuse of the property, the delay that can be specified
- // is comprised between [MIN_NODEADLINE, MAX_NODEADLINE], and its default
- // value if unspecified (or <= 0) is DEF_NODEADLINE = 3000ms
- // The property is -Djdk.httpclient.internal.selector.timeout=<millis>
- private static final int MIN_NODEADLINE = 1000; // ms
- private static final int MAX_NODEADLINE = 1000 * 1200; // ms
- private static final int DEF_NODEADLINE = 3000; // ms
- private static final long NODEADLINE; // default is DEF_NODEADLINE ms
- static {
- // ensure NODEADLINE is initialized with some valid value.
- long deadline = Utils.getIntegerNetProperty(
- "jdk.httpclient.internal.selector.timeout",
- DEF_NODEADLINE); // millis
- if (deadline <= 0) deadline = DEF_NODEADLINE;
- deadline = Math.max(deadline, MIN_NODEADLINE);
- NODEADLINE = Math.min(deadline, MAX_NODEADLINE);
- }
-
- private final Selector selector;
- private volatile boolean closed;
- private final List<AsyncEvent> registrations;
- private final System.Logger debug;
- private final System.Logger debugtimeout;
- HttpClientImpl owner;
- ConnectionPool pool;
-
- SelectorManager(HttpClientImpl ref) throws IOException {
- super(null, null, "HttpClient-" + ref.id + "-SelectorManager", 0, false);
- owner = ref;
- debug = ref.debug;
- debugtimeout = ref.debugtimeout;
- pool = ref.connectionPool();
- registrations = new ArrayList<>();
- selector = Selector.open();
- }
-
- void eventUpdated(AsyncEvent e) throws ClosedChannelException {
- if (Thread.currentThread() == this) {
- SelectionKey key = e.channel().keyFor(selector);
- SelectorAttachment sa = (SelectorAttachment) key.attachment();
- if (sa != null) sa.register(e);
- } else {
- register(e);
- }
- }
-
- // This returns immediately. So caller not allowed to send/receive
- // on connection.
- synchronized void register(AsyncEvent e) {
- registrations.add(e);
- selector.wakeup();
- }
-
- synchronized void cancel(SocketChannel e) {
- SelectionKey key = e.keyFor(selector);
- if (key != null) {
- key.cancel();
- }
- selector.wakeup();
- }
-
- void wakeupSelector() {
- selector.wakeup();
- }
-
- synchronized void shutdown() {
- debug.log(Level.DEBUG, "SelectorManager shutting down");
- closed = true;
- try {
- selector.close();
- } catch (IOException ignored) {
- } finally {
- owner.stop();
- }
- }
-
- @Override
- public void run() {
- List<Pair<AsyncEvent,IOException>> errorList = new ArrayList<>();
- List<AsyncEvent> readyList = new ArrayList<>();
- try {
- while (!Thread.currentThread().isInterrupted()) {
- synchronized (this) {
- assert errorList.isEmpty();
- assert readyList.isEmpty();
- for (AsyncEvent event : registrations) {
- if (event instanceof AsyncTriggerEvent) {
- readyList.add(event);
- continue;
- }
- SelectableChannel chan = event.channel();
- SelectionKey key = null;
- try {
- key = chan.keyFor(selector);
- SelectorAttachment sa;
- if (key == null || !key.isValid()) {
- if (key != null) {
- // key is canceled.
- // invoke selectNow() to purge it
- // before registering the new event.
- selector.selectNow();
- }
- sa = new SelectorAttachment(chan, selector);
- } else {
- sa = (SelectorAttachment) key.attachment();
- }
- // may throw IOE if channel closed: that's OK
- sa.register(event);
- if (!chan.isOpen()) {
- throw new IOException("Channel closed");
- }
- } catch (IOException e) {
- Log.logTrace("HttpClientImpl: " + e);
- debug.log(Level.DEBUG, () ->
- "Got " + e.getClass().getName()
- + " while handling"
- + " registration events");
- chan.close();
- // let the event abort deal with it
- errorList.add(new Pair<>(event, e));
- if (key != null) {
- key.cancel();
- selector.selectNow();
- }
- }
- }
- registrations.clear();
- selector.selectedKeys().clear();
- }
-
- for (AsyncEvent event : readyList) {
- assert event instanceof AsyncTriggerEvent;
- event.handle();
- }
- readyList.clear();
-
- for (Pair<AsyncEvent,IOException> error : errorList) {
- // an IOException was raised and the channel closed.
- handleEvent(error.first, error.second);
- }
- errorList.clear();
-
- // Check whether client is still alive, and if not,
- // gracefully stop this thread
- if (!owner.isReferenced()) {
- Log.logTrace("HttpClient no longer referenced. Exiting...");
- return;
- }
-
- // Timeouts will have milliseconds granularity. It is important
- // to handle them in a timely fashion.
- long nextTimeout = owner.purgeTimeoutsAndReturnNextDeadline();
- debugtimeout.log(Level.DEBUG, "next timeout: %d", nextTimeout);
-
- // Keep-alive have seconds granularity. It's not really an
- // issue if we keep connections linger a bit more in the keep
- // alive cache.
- long nextExpiry = pool.purgeExpiredConnectionsAndReturnNextDeadline();
- debugtimeout.log(Level.DEBUG, "next expired: %d", nextExpiry);
-
- assert nextTimeout >= 0;
- assert nextExpiry >= 0;
-
- // Don't wait for ever as it might prevent the thread to
- // stop gracefully. millis will be 0 if no deadline was found.
- if (nextTimeout <= 0) nextTimeout = NODEADLINE;
-
- // Clip nextExpiry at NODEADLINE limit. The default
- // keep alive is 1200 seconds (half an hour) - we don't
- // want to wait that long.
- if (nextExpiry <= 0) nextExpiry = NODEADLINE;
- else nextExpiry = Math.min(NODEADLINE, nextExpiry);
-
- // takes the least of the two.
- long millis = Math.min(nextExpiry, nextTimeout);
-
- debugtimeout.log(Level.DEBUG, "Next deadline is %d",
- (millis == 0 ? NODEADLINE : millis));
- //debugPrint(selector);
- int n = selector.select(millis == 0 ? NODEADLINE : millis);
- if (n == 0) {
- // Check whether client is still alive, and if not,
- // gracefully stop this thread
- if (!owner.isReferenced()) {
- Log.logTrace("HttpClient no longer referenced. Exiting...");
- return;
- }
- owner.purgeTimeoutsAndReturnNextDeadline();
- continue;
- }
- Set<SelectionKey> keys = selector.selectedKeys();
-
- assert errorList.isEmpty();
- for (SelectionKey key : keys) {
- SelectorAttachment sa = (SelectorAttachment) key.attachment();
- if (!key.isValid()) {
- IOException ex = sa.chan.isOpen()
- ? new IOException("Invalid key")
- : new ClosedChannelException();
- sa.pending.forEach(e -> errorList.add(new Pair<>(e,ex)));
- sa.pending.clear();
- continue;
- }
-
- int eventsOccurred;
- try {
- eventsOccurred = key.readyOps();
- } catch (CancelledKeyException ex) {
- IOException io = Utils.getIOException(ex);
- sa.pending.forEach(e -> errorList.add(new Pair<>(e,io)));
- sa.pending.clear();
- continue;
- }
- sa.events(eventsOccurred).forEach(readyList::add);
- sa.resetInterestOps(eventsOccurred);
- }
- selector.selectNow(); // complete cancellation
- selector.selectedKeys().clear();
-
- for (AsyncEvent event : readyList) {
- handleEvent(event, null); // will be delegated to executor
- }
- readyList.clear();
- errorList.forEach((p) -> handleEvent(p.first, p.second));
- errorList.clear();
- }
- } catch (Throwable e) {
- //e.printStackTrace();
- if (!closed) {
- // This terminates thread. So, better just print stack trace
- String err = Utils.stackTrace(e);
- Log.logError("HttpClientImpl: fatal error: " + err);
- }
- debug.log(Level.DEBUG, "shutting down", e);
- if (Utils.ASSERTIONSENABLED && !debug.isLoggable(Level.DEBUG)) {
- e.printStackTrace(System.err); // always print the stack
- }
- } finally {
- shutdown();
- }
- }
-
-// void debugPrint(Selector selector) {
-// System.err.println("Selector: debugprint start");
-// Set<SelectionKey> keys = selector.keys();
-// for (SelectionKey key : keys) {
-// SelectableChannel c = key.channel();
-// int ops = key.interestOps();
-// System.err.printf("selector chan:%s ops:%d\n", c, ops);
-// }
-// System.err.println("Selector: debugprint end");
-// }
-
- /** Handles the given event. The given ioe may be null. */
- void handleEvent(AsyncEvent event, IOException ioe) {
- if (closed || ioe != null) {
- event.abort(ioe);
- } else {
- event.handle();
- }
- }
- }
-
- /**
- * Tracks multiple user level registrations associated with one NIO
- * registration (SelectionKey). In this implementation, registrations
- * are one-off and when an event is posted the registration is cancelled
- * until explicitly registered again.
- *
- * <p> No external synchronization required as this class is only used
- * by the SelectorManager thread. One of these objects required per
- * connection.
- */
- private static class SelectorAttachment {
- private final SelectableChannel chan;
- private final Selector selector;
- private final Set<AsyncEvent> pending;
- private final static System.Logger debug =
- Utils.getDebugLogger("SelectorAttachment"::toString, DEBUG);
- private int interestOps;
-
- SelectorAttachment(SelectableChannel chan, Selector selector) {
- this.pending = new HashSet<>();
- this.chan = chan;
- this.selector = selector;
- }
-
- void register(AsyncEvent e) throws ClosedChannelException {
- int newOps = e.interestOps();
- boolean reRegister = (interestOps & newOps) != newOps;
- interestOps |= newOps;
- pending.add(e);
- if (reRegister) {
- // first time registration happens here also
- chan.register(selector, interestOps, this);
- }
- }
-
- /**
- * Returns a Stream<AsyncEvents> containing only events that are
- * registered with the given {@code interestOps}.
- */
- Stream<AsyncEvent> events(int interestOps) {
- return pending.stream()
- .filter(ev -> (ev.interestOps() & interestOps) != 0);
- }
-
- /**
- * Removes any events with the given {@code interestOps}, and if no
- * events remaining, cancels the associated SelectionKey.
- */
- void resetInterestOps(int interestOps) {
- int newOps = 0;
-
- Iterator<AsyncEvent> itr = pending.iterator();
- while (itr.hasNext()) {
- AsyncEvent event = itr.next();
- int evops = event.interestOps();
- if (event.repeating()) {
- newOps |= evops;
- continue;
- }
- if ((evops & interestOps) != 0) {
- itr.remove();
- } else {
- newOps |= evops;
- }
- }
-
- this.interestOps = newOps;
- SelectionKey key = chan.keyFor(selector);
- if (newOps == 0 && pending.isEmpty()) {
- key.cancel();
- } else {
- try {
- key.interestOps(newOps);
- } catch (CancelledKeyException x) {
- // channel may have been closed
- debug.log(Level.DEBUG, "key cancelled for " + chan);
- abortPending(x);
- }
- }
- }
-
- void abortPending(Throwable x) {
- if (!pending.isEmpty()) {
- AsyncEvent[] evts = pending.toArray(new AsyncEvent[0]);
- pending.clear();
- IOException io = Utils.getIOException(x);
- for (AsyncEvent event : evts) {
- event.abort(io);
- }
- }
- }
- }
-
- /*package-private*/ SSLContext theSSLContext() {
- return sslContext;
- }
-
- @Override
- public SSLContext sslContext() {
- return sslContext;
- }
-
- @Override
- public SSLParameters sslParameters() {
- return Utils.copySSLParameters(sslParams);
- }
-
- @Override
- public Optional<Authenticator> authenticator() {
- return Optional.ofNullable(authenticator);
- }
-
- /*package-private*/ final Executor theExecutor() {
- return executor;
- }
-
- @Override
- public final Optional<Executor> executor() {
- return isDefaultExecutor ? Optional.empty() : Optional.of(executor);
- }
-
- ConnectionPool connectionPool() {
- return connections;
- }
-
- @Override
- public Redirect followRedirects() {
- return followRedirects;
- }
-
-
- @Override
- public Optional<CookieHandler> cookieHandler() {
- return Optional.ofNullable(cookieHandler);
- }
-
- @Override
- public Optional<ProxySelector> proxy() {
- return this.userProxySelector;
- }
-
- // Return the effective proxy that this client uses.
- ProxySelector proxySelector() {
- return proxySelector;
- }
-
- @Override
- public WebSocket.Builder newWebSocketBuilder() {
- // Make sure to pass the HttpClientFacade to the WebSocket builder.
- // This will ensure that the facade is not released before the
- // WebSocket has been created, at which point the pendingOperationCount
- // will have been incremented by the DetachedConnectionChannel
- // (see PlainHttpConnection.detachChannel())
- return new BuilderImpl(this.facade(), proxySelector);
- }
-
- @Override
- public Version version() {
- return version;
- }
-
- String dbgString() {
- return dbgTag;
- }
-
- @Override
- public String toString() {
- // Used by tests to get the client's id and compute the
- // name of the SelectorManager thread.
- return super.toString() + ("(" + id + ")");
- }
-
- private void initFilters() {
- addFilter(AuthenticationFilter.class);
- addFilter(RedirectFilter.class);
- if (this.cookieHandler != null) {
- addFilter(CookieFilter.class);
- }
- }
-
- private void addFilter(Class<? extends HeaderFilter> f) {
- filters.addFilter(f);
- }
-
- final List<HeaderFilter> filterChain() {
- return filters.getFilterChain();
- }
-
- // Timer controls.
- // Timers are implemented through timed Selector.select() calls.
-
- synchronized void registerTimer(TimeoutEvent event) {
- Log.logTrace("Registering timer {0}", event);
- timeouts.add(event);
- selmgr.wakeupSelector();
- }
-
- synchronized void cancelTimer(TimeoutEvent event) {
- Log.logTrace("Canceling timer {0}", event);
- timeouts.remove(event);
- }
-
- /**
- * Purges ( handles ) timer events that have passed their deadline, and
- * returns the amount of time, in milliseconds, until the next earliest
- * event. A return value of 0 means that there are no events.
- */
- private long purgeTimeoutsAndReturnNextDeadline() {
- long diff = 0L;
- List<TimeoutEvent> toHandle = null;
- int remaining = 0;
- // enter critical section to retrieve the timeout event to handle
- synchronized(this) {
- if (timeouts.isEmpty()) return 0L;
-
- Instant now = Instant.now();
- Iterator<TimeoutEvent> itr = timeouts.iterator();
- while (itr.hasNext()) {
- TimeoutEvent event = itr.next();
- diff = now.until(event.deadline(), ChronoUnit.MILLIS);
- if (diff <= 0) {
- itr.remove();
- toHandle = (toHandle == null) ? new ArrayList<>() : toHandle;
- toHandle.add(event);
- } else {
- break;
- }
- }
- remaining = timeouts.size();
- }
-
- // can be useful for debugging
- if (toHandle != null && Log.trace()) {
- Log.logTrace("purgeTimeoutsAndReturnNextDeadline: handling "
- + toHandle.size() + " events, "
- + "remaining " + remaining
- + ", next deadline: " + (diff < 0 ? 0L : diff));
- }
-
- // handle timeout events out of critical section
- if (toHandle != null) {
- Throwable failed = null;
- for (TimeoutEvent event : toHandle) {
- try {
- Log.logTrace("Firing timer {0}", event);
- event.handle();
- } catch (Error | RuntimeException e) {
- // Not expected. Handle remaining events then throw...
- // If e is an OOME or SOE it might simply trigger a new
- // error from here - but in this case there's not much we
- // could do anyway. Just let it flow...
- if (failed == null) failed = e;
- else failed.addSuppressed(e);
- Log.logTrace("Failed to handle event {0}: {1}", event, e);
- }
- }
- if (failed instanceof Error) throw (Error) failed;
- if (failed instanceof RuntimeException) throw (RuntimeException) failed;
- }
-
- // return time to wait until next event. 0L if there's no more events.
- return diff < 0 ? 0L : diff;
- }
-
- // used for the connection window
- int getReceiveBufferSize() {
- return Utils.getIntegerNetProperty(
- "jdk.httpclient.receiveBufferSize", 2 * 1024 * 1024
- );
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,491 +0,0 @@
-/*
- * 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.incubator.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 jdk.incubator.http.HttpClient.Version;
-import jdk.incubator.http.internal.common.Demand;
-import jdk.incubator.http.internal.common.FlowTube;
-import jdk.incubator.http.internal.common.SequentialScheduler;
-import jdk.incubator.http.internal.common.SequentialScheduler.DeferredCompleter;
-import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.Utils;
-import static jdk.incubator.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 {
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
- final static System.Logger DEBUG_LOGGER = Utils.getDebugLogger(
- () -> "HttpConnection(SocketTube(?))", 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. */
- 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) {
- HttpConnection c = null;
- InetSocketAddress proxy = request.proxy();
- if (proxy != null && proxy.isUnresolved()) {
- // The default proxy selector may select a proxy whose address is
- // unresolved. We must resolve the address before connecting to it.
- proxy = new InetSocketAddress(proxy.getHostString(), proxy.getPort());
- }
- 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;
- DEBUG_LOGGER.log(Level.DEBUG, () -> 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;
- DEBUG_LOGGER.log(Level.DEBUG, () -> 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 void shutdownInput() throws IOException;
-
- abstract void shutdownOutput() throws IOException;
-
- // Support for WebSocket/RawChannelImpl which unfortunately
- // still depends on synchronous read/writes.
- // It should be removed when RawChannelImpl moves to using asynchronous APIs.
- abstract static class DetachedConnectionChannel implements Closeable {
- DetachedConnectionChannel() {}
- abstract SocketChannel channel();
- abstract long write(ByteBuffer[] buffers, int start, int number)
- throws IOException;
- abstract void shutdownInput() throws IOException;
- abstract void shutdownOutput() throws IOException;
- abstract ByteBuffer read() throws IOException;
- @Override
- public abstract void close();
- @Override
- public String toString() {
- return this.getClass().getSimpleName() + ": " + channel().toString();
- }
- }
-
- // Support for WebSocket/RawChannelImpl which unfortunately
- // still depends on synchronous read/writes.
- // It should be removed when RawChannelImpl moves to using asynchronous APIs.
- abstract DetachedConnectionChannel detachChannel();
-
- 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<>();
- 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);
- debug.log(Level.DEBUG, () -> "HttpPublisher: got request of "
- + n + " from "
- + getConnectionFlow());
- writeScheduler.runOrSchedule();
- }
-
- @Override
- public void cancel() {
- debug.log(Level.DEBUG, () -> "HttpPublisher: cancelled by "
- + getConnectionFlow());
- }
-
- void flush() {
- while (!queue.isEmpty() && demand.tryDecrement()) {
- List<ByteBuffer> elem = queue.poll();
- debug.log(Level.DEBUG, () -> "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(Level.DEBUG, "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();
- queue.addFirst(buffers);
- debug.log(Level.DEBUG, "inserted %d bytes in the write queue", bytes);
- }
-
- @Override
- public void signalEnqueued() throws IOException {
- debug.log(Level.DEBUG, "signalling the publisher of the write queue");
- signal();
- }
- }
-
- String dbgTag = null;
- 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();
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequest.java Tue Feb 06 11:39:55 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequest.java Tue Feb 06 14:10:28 2018 +0000
@@ -44,6 +44,8 @@
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.function.Supplier;
+import jdk.incubator.http.internal.HttpRequestBuilderImpl;
+import jdk.incubator.http.internal.RequestPublishers;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestBuilderImpl.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,228 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.http;
-
-import java.net.URI;
-import java.time.Duration;
-import java.util.Optional;
-import jdk.incubator.http.HttpRequest.BodyPublisher;
-import jdk.incubator.http.internal.common.HttpHeadersImpl;
-import jdk.incubator.http.internal.common.Utils;
-
-import static java.lang.String.format;
-import static java.util.Objects.requireNonNull;
-import static jdk.incubator.http.internal.common.Utils.isValidName;
-import static jdk.incubator.http.internal.common.Utils.isValidValue;
-
-class HttpRequestBuilderImpl extends 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;
- }
-
- private static IllegalArgumentException newIAE(String message, Object... args) {
- return new IllegalArgumentException(format(message, args));
- }
-
- private static void checkURI(URI uri) {
- String scheme = uri.getScheme();
- if (scheme == null)
- throw newIAE("URI with undefined scheme");
- scheme = scheme.toLowerCase();
- 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(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(BodyPublisher body) {
- return method0("DELETE", requireNonNull(body));
- }
-
- @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");
- return method0(method, requireNonNull(body));
- }
-
- private HttpRequest.Builder method0(String method, BodyPublisher body) {
- assert method != null;
- assert !method.equals("GET") ? body != null : true;
- 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; }
-
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,331 +0,0 @@
-/*
- * 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.incubator.http;
-
-import jdk.incubator.http.internal.common.HttpHeadersImpl;
-import jdk.incubator.http.internal.websocket.WebSocketRequest;
-
-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.Optional;
-
-import static jdk.incubator.http.internal.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, AccessControlContext acc) {
- String method = request.method();
- this.method = method == null ? "GET" : method;
- this.userHeaders = request.headers();
- if (request instanceof HttpRequestImpl) {
- this.systemHeaders = ((HttpRequestImpl) request).systemHeaders;
- this.isWebSocket = ((HttpRequestImpl) request).isWebSocket;
- } else {
- this.systemHeaders = new HttpHeadersImpl();
- }
- this.systemHeaders.setHeader("User-Agent", USER_AGENT);
- this.uri = request.uri();
- 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);
- if (acc != null && requestPublisher instanceof RequestPublishers.FilePublisher) {
- // Restricts the file publisher with the senders ACC, if any
- ((RequestPublishers.FilePublisher)requestPublisher).setAccessControlContext(acc);
- }
- this.timeout = request.timeout().orElse(null);
- this.version = request.version();
- this.authority = null;
- }
-
- /** Creates a HttpRequestImpl using fields of an existing request impl. */
- public HttpRequestImpl(URI uri,
- String method,
- HttpRequestImpl other) {
- this.method = method == null? "GET" : method;
- this.userHeaders = other.userHeaders;
- this.isWebSocket = other.isWebSocket;
- this.systemHeaders = other.systemHeaders;
- 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);
- }
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponse.java Tue Feb 06 11:39:55 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponse.java Tue Feb 06 14:10:28 2018 +0000
@@ -35,9 +35,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.OpenOption;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
-import java.security.AccessControlContext;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -46,15 +44,16 @@
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Flow;
import java.util.concurrent.Flow.Subscriber;
-import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.net.ssl.SSLParameters;
import jdk.incubator.http.internal.BufferingSubscriber;
import jdk.incubator.http.internal.LineSubscriberAdapter;
+import jdk.incubator.http.internal.ResponseBodyHandlers.FileDownloadBodyHandler;
+import jdk.incubator.http.internal.ResponseBodyHandlers.PathBodyHandler;
+import jdk.incubator.http.internal.ResponseBodyHandlers.PushPromisesHandlerWithMap;
import jdk.incubator.http.internal.ResponseSubscribers;
-import static jdk.incubator.http.internal.common.Utils.unchecked;
import static jdk.incubator.http.internal.common.Utils.charsetFrom;
/**
@@ -176,128 +175,6 @@
return path.toFile().getPath();
}
- /** A body handler that is further restricted by a given ACC. */
- interface UntrustedBodyHandler<T> extends BodyHandler<T> {
- void setAccessControlContext(AccessControlContext acc);
- }
-
- /**
- * A Path body handler.
- *
- * Note: Exists mainly too allow setting of the senders ACC post creation of
- * the handler.
- */
- static class PathBodyHandler implements UntrustedBodyHandler<Path> {
- private final Path file;
- private final OpenOption[]openOptions;
- private volatile AccessControlContext acc;
-
- PathBodyHandler(Path file, OpenOption... openOptions) {
- this.file = file;
- this.openOptions = openOptions;
- }
-
- @Override
- public void setAccessControlContext(AccessControlContext acc) {
- this.acc = acc;
- }
-
- @Override
- public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
- ResponseSubscribers.PathSubscriber bs = (ResponseSubscribers.PathSubscriber)
- BodySubscriber.asFileImpl(file, openOptions);
- bs.setAccessControlContext(acc);
- return bs;
- }
- }
-
- /* package-private with push promise Map implementation */
- static class PushPromisesHandlerWithMap<T> implements PushPromiseHandler<T> {
-
- private final ConcurrentMap<HttpRequest,CompletableFuture<HttpResponse<T>>> pushPromisesMap;
- private final Function<HttpRequest,BodyHandler<T>> pushPromiseHandler;
-
- 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. Supports setting ACC.
- static class FileDownloadBodyHandler implements UntrustedBodyHandler<Path> {
- private final Path directory;
- private final OpenOption[]openOptions;
- private volatile AccessControlContext acc;
-
- FileDownloadBodyHandler(Path directory, OpenOption... openOptions) {
- this.directory = directory;
- this.openOptions = openOptions;
- }
-
- @Override
- public void setAccessControlContext(AccessControlContext acc) {
- this.acc = acc;
- }
-
- @Override
- public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
- String dispoHeader = headers.firstValue("Content-Disposition")
- .orElseThrow(() -> unchecked(new IOException("No Content-Disposition")));
- if (!dispoHeader.startsWith("attachment;")) {
- throw unchecked(new IOException("Unknown Content-Disposition type"));
- }
- int n = dispoHeader.indexOf("filename=");
- if (n == -1) {
- throw unchecked(new IOException("Bad Content-Disposition type"));
- }
- int lastsemi = dispoHeader.lastIndexOf(';');
- String disposition;
- if (lastsemi < n) {
- disposition = dispoHeader.substring(n + 9);
- } else {
- disposition = dispoHeader.substring(n + 9, lastsemi);
- }
- Path file = Paths.get(directory.toString(), disposition);
-
- ResponseSubscribers.PathSubscriber bs = (ResponseSubscribers.PathSubscriber)
- BodySubscriber.asFileImpl(file, openOptions);
- bs.setAccessControlContext(acc);
- return bs;
- }
- }
/**
* A handler for response bodies.
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponseImpl.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,173 +0,0 @@
-/*
- * 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.incubator.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.SSLParameters;
-import jdk.incubator.http.internal.websocket.RawChannel;
-
-/**
- * The implementation class for HttpResponse
- */
-class HttpResponseImpl<T> extends HttpResponse<T> implements RawChannel.Provider {
-
- final int responseCode;
- final Exchange<T> exchange;
- final HttpRequest initialRequest;
- final Optional<HttpResponse<T>> previousResponse;
- final HttpHeaders headers;
- final SSLParameters sslParameters;
- 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.sslParameters = exch.client().sslParameters();
- 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 SSLParameters sslParameters() {
- return sslParameters;
- }
-
- @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 RawChannelImpl(exchange.client(), 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();
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ImmutableHeaders.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,93 +0,0 @@
-/*
- * 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.incubator.http;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-import java.util.function.BiPredicate;
-import java.util.function.Predicate;
-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());
- }
-
- 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()
- .filter(e -> headerAllowed.test(e.getKey(), e.getValue()))
- .forEach(e ->
- {
- List<String> values = new ArrayList<>(e.getValue());
- m.put(e.getKey(), unmodifiableList(values));
- }
- );
- this.map = unmodifiableMap(m);
- }
-
- 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;
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiExchange.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,321 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.http;
-
-import java.io.IOException;
-import java.lang.System.Logger.Level;
-import java.time.Duration;
-import java.util.List;
-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 jdk.incubator.http.HttpResponse.PushPromiseHandler;
-import jdk.incubator.http.HttpResponse.UntrustedBodyHandler;
-import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.internal.common.ConnectionExpiredException;
-import jdk.incubator.http.internal.common.Utils;
-import static jdk.incubator.http.internal.common.MinimalFuture.completedFuture;
-import static jdk.incubator.http.internal.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 boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- static final System.Logger DEBUG_LOGGER =
- Utils.getDebugLogger("MultiExchange"::toString, 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 async only
- 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 List<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.client = client;
- this.filters = client.filterChain();
- this.acc = acc;
- this.executor = client.theExecutor();
- this.responseHandler = responseHandler;
- if (acc != null) {
- // Restricts the file publisher with the senders ACC, if any
- if (responseHandler instanceof UntrustedBodyHandler)
- ((UntrustedBodyHandler)this.responseHandler).setAccessControlContext(acc);
- }
-
- if (pushPromiseHandler != null) {
- this.pushGroup = new PushGroup<>(pushPromiseHandler, request, acc);
- } 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");
- for (HeaderFilter filter : filters) {
- 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() {
-// cancelled = true;
-// getExchange().cancel();
-// }
-
- 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<>(userRequest, 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
- 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) -> {
- 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;
- }
-
- /**
- * 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) {
- // allow the retry mechanism to do its work
- // ####: method (GET,HEAD, not POST?), no bytes written or read ( differentiate? )
- if (t.getCause() != null) retryCause = t.getCause();
- if (!expiredOnce) {
- DEBUG_LOGGER.log(Level.DEBUG,
- "MultiExchange: ConnectionExpiredException (async): retrying...",
- t);
- expiredOnce = true;
- return null;
- } else {
- DEBUG_LOGGER.log(Level.DEBUG,
- "MultiExchange: 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() {
- DEBUG_LOGGER.log(Level.DEBUG,
- "Cancelling MultiExchange due to timeout for request %s",
- request);
- cancel(new HttpTimeoutException("request timed out"));
- }
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainHttpConnection.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,313 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.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.incubator.http.internal.common.FlowTube;
-import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.internal.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 FlowTube tube;
- 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";
- debug.log(Level.DEBUG, "ConnectEvent: finishing connect");
- boolean finished = chan.finishConnect();
- assert finished : "Expected channel to be connected";
- debug.log(Level.DEBUG,
- "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(address);
- try {
- finished = AccessController.doPrivileged(pa);
- } catch (PrivilegedActionException e) {
- cf.completeExceptionally(e.getCause());
- }
- if (finished) {
- debug.log(Level.DEBUG, "connect finished without blocking");
- connected = true;
- cf.complete(null);
- } else {
- debug.log(Level.DEBUG, "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);
- return true;
- } catch(IOException x) {
- debug.log(Level.DEBUG,
- "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 synchronized void close() {
- if (closed) {
- return;
- }
- closed = true;
- try {
- Log.logTrace("Closing: " + toString());
- chan.close();
- } catch (IOException e) {}
- }
-
- @Override
- void shutdownInput() throws IOException {
- debug.log(Level.DEBUG, "Shutting down input");
- chan.shutdownInput();
- }
-
- @Override
- void shutdownOutput() throws IOException {
- debug.log(Level.DEBUG, "Shutting down output");
- chan.shutdownOutput();
- }
-
- @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;
- }
-
- // Support for WebSocket/RawChannelImpl which unfortunately
- // still depends on synchronous read/writes.
- // It should be removed when RawChannelImpl moves to using asynchronous APIs.
- private static final class PlainDetachedChannel
- extends DetachedConnectionChannel {
- final PlainHttpConnection plainConnection;
- boolean closed;
- PlainDetachedChannel(PlainHttpConnection conn) {
- // We're handing the connection channel over to a web socket.
- // We need the selector manager's thread to stay alive until
- // the WebSocket is closed.
- conn.client().webSocketOpen();
- this.plainConnection = conn;
- }
-
- @Override
- SocketChannel channel() {
- return plainConnection.channel();
- }
-
- @Override
- ByteBuffer read() throws IOException {
- ByteBuffer dst = ByteBuffer.allocate(8192);
- int n = readImpl(dst);
- if (n > 0) {
- return dst;
- } else if (n == 0) {
- return Utils.EMPTY_BYTEBUFFER;
- } else {
- return null;
- }
- }
-
- @Override
- public void close() {
- HttpClientImpl client = plainConnection.client();
- try {
- plainConnection.close();
- } finally {
- // notify the HttpClientImpl that the websocket is no
- // no longer operating.
- synchronized(this) {
- if (closed == true) return;
- closed = true;
- }
- client.webSocketClose();
- }
- }
-
- @Override
- public long write(ByteBuffer[] buffers, int start, int number)
- throws IOException
- {
- return channel().write(buffers, start, number);
- }
-
- @Override
- public void shutdownInput() throws IOException {
- plainConnection.shutdownInput();
- }
-
- @Override
- public void shutdownOutput() throws IOException {
- plainConnection.shutdownOutput();
- }
-
- private int readImpl(ByteBuffer buf) throws IOException {
- int mark = buf.position();
- int n;
- n = channel().read(buf);
- if (n == -1) {
- return -1;
- }
- Utils.flipToMark(buf, mark);
- return n;
- }
- }
-
- // Support for WebSocket/RawChannelImpl which unfortunately
- // still depends on synchronous read/writes.
- // It should be removed when RawChannelImpl moves to using asynchronous APIs.
- @Override
- DetachedConnectionChannel detachChannel() {
- client().cancelRegistration(channel());
- return new PlainDetachedChannel(this);
- }
-
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainProxyConnection.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.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);
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,167 +0,0 @@
-/*
- * 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.incubator.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 jdk.incubator.http.internal.common.FlowTube;
-import jdk.incubator.http.internal.common.MinimalFuture;
-
-import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
-
-/**
- * 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() {
- debug.log(Level.DEBUG, "Connecting plain connection");
- return delegate.connectAsync()
- .thenCompose((Void v) -> {
- debug.log(Level.DEBUG, "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, discard(null), null, null);
- Exchange<Void> connectExchange = new Exchange<>(req, mulEx);
-
- return connectExchange
- .responseAsyncImpl(delegate)
- .thenCompose((Response resp) -> {
- CompletableFuture<Void> cf = new MinimalFuture<>();
- debug.log(Level.DEBUG, "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
- void shutdownInput() throws IOException {
- delegate.shutdownInput();
- }
-
- @Override
- void shutdownOutput() throws IOException {
- delegate.shutdownOutput();
- }
-
- @Override
- boolean isSecure() {
- return false;
- }
-
- @Override
- boolean isProxied() {
- return true;
- }
-
- // Support for WebSocket/RawChannelImpl which unfortunately
- // still depends on synchronous read/writes.
- // It should be removed when RawChannelImpl moves to using asynchronous APIs.
- @Override
- DetachedConnectionChannel detachChannel() {
- return delegate.detachChannel();
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PrivilegedExecutor.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.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));
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ProxyAuthenticationRequired.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-/*
- * 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.incubator.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 ConnectionExpiredException} 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;
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PullPublisher.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,138 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, 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.incubator.http;
-
-import java.util.Iterator;
-import java.util.concurrent.Flow;
-import jdk.incubator.http.internal.common.Demand;
-import jdk.incubator.http.internal.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;
- }
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PushGroup.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,163 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, 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.incubator.http;
-
-import java.security.AccessControlContext;
-import java.security.AccessController;
-import java.util.Objects;
-import java.util.concurrent.CompletableFuture;
-import jdk.incubator.http.HttpResponse.BodyHandler;
-import jdk.incubator.http.HttpResponse.PushPromiseHandler;
-import jdk.incubator.http.HttpResponse.UntrustedBodyHandler;
-import jdk.incubator.http.internal.common.MinimalFuture;
-import jdk.incubator.http.internal.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 AccessControlContext acc;
-
- int numberOfPushes;
- int remainingPushes;
- boolean noMorePushes = false;
-
- PushGroup(PushPromiseHandler<T> pushPromiseHandler,
- HttpRequestImpl initiatingRequest,
- AccessControlContext acc) {
- this(pushPromiseHandler, initiatingRequest, new MinimalFuture<>(), acc);
- }
-
- // Check mainBodyHandler before calling nested constructor.
- private PushGroup(HttpResponse.PushPromiseHandler<T> pushPromiseHandler,
- HttpRequestImpl initiatingRequest,
- CompletableFuture<HttpResponse<T>> mainResponse,
- AccessControlContext acc) {
- this.noMorePushesCF = new MinimalFuture<>();
- this.pushPromiseHandler = pushPromiseHandler;
- this.initiatingRequest = initiatingRequest;
- // Restricts the file publisher with the senders ACC, if any
- if (pushPromiseHandler instanceof UntrustedBodyHandler)
- ((UntrustedBodyHandler)this.pushPromiseHandler).setAccessControlContext(acc);
- this.acc = acc;
- }
-
- interface Acceptor<T> {
- BodyHandler<T> bodyHandler();
- CompletableFuture<HttpResponse<T>> cf();
- boolean accepted();
- }
-
- private static class AcceptorImpl<T> implements Acceptor<T> {
- private volatile HttpResponse.BodyHandler<T> bodyHandler;
- private volatile CompletableFuture<HttpResponse<T>> cf;
-
- 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;
- }
-
- @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<>();
-
- pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, acceptor::accept);
-
- synchronized (this) {
- if (acceptor.accepted()) {
- if (acceptor.bodyHandler instanceof UntrustedBodyHandler) {
- ((UntrustedBodyHandler) acceptor.bodyHandler).setAccessControlContext(acc);
- }
- 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;
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/RawChannelImpl.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,158 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.http;
-
-import jdk.incubator.http.internal.common.Utils;
-import jdk.incubator.http.internal.websocket.RawChannel;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.SelectableChannel;
-import java.nio.channels.SocketChannel;
-import java.util.function.Supplier;
-
-/*
- * Each RawChannel corresponds to a TCP connection (SocketChannel) but is
- * connected to a Selector and an ExecutorService for invoking the send and
- * receive callbacks. Also includes SSL processing.
- */
-final class RawChannelImpl implements RawChannel {
-
- private final HttpClientImpl client;
- private final HttpConnection.DetachedConnectionChannel detachedChannel;
- private final Object initialLock = new Object();
- private Supplier<ByteBuffer> initial;
-
- RawChannelImpl(HttpClientImpl client,
- HttpConnection connection,
- Supplier<ByteBuffer> initial)
- throws IOException
- {
- this.client = client;
- this.detachedChannel = connection.detachChannel();
- this.initial = initial;
-
- SocketChannel chan = connection.channel();
- client.cancelRegistration(chan);
- // Constructing a RawChannel is supposed to have a "hand over"
- // semantics, in other words if construction fails, the channel won't be
- // needed by anyone, in which case someone still needs to close it
- try {
- chan.configureBlocking(false);
- } catch (IOException e) {
- try {
- chan.close();
- } catch (IOException e1) {
- e.addSuppressed(e1);
- } finally {
- detachedChannel.close();
- }
- throw e;
- }
- }
-
- private class NonBlockingRawAsyncEvent extends AsyncEvent {
-
- private final RawEvent re;
-
- NonBlockingRawAsyncEvent(RawEvent re) {
- // !BLOCKING & !REPEATING
- this.re = re;
- }
-
- @Override
- public SelectableChannel channel() {
- return detachedChannel.channel();
- }
-
- @Override
- public int interestOps() {
- return re.interestOps();
- }
-
- @Override
- public void handle() {
- re.handle();
- }
-
- @Override
- public void abort(IOException ioe) { }
- }
-
- @Override
- public void registerEvent(RawEvent event) throws IOException {
- client.registerEvent(new NonBlockingRawAsyncEvent(event));
- }
-
- @Override
- public ByteBuffer read() throws IOException {
- assert !detachedChannel.channel().isBlocking();
- // connection.read() will no longer be available.
- return detachedChannel.read();
- }
-
- @Override
- public ByteBuffer initialByteBuffer() {
- synchronized (initialLock) {
- if (initial == null) {
- throw new IllegalStateException();
- }
- ByteBuffer ref = initial.get();
- ref = ref.hasRemaining() ? Utils.copy(ref)
- : Utils.EMPTY_BYTEBUFFER;
- initial = null;
- return ref;
- }
- }
-
- @Override
- public long write(ByteBuffer[] src, int offset, int len) throws IOException {
- // this makes the whitebox driver test fail.
- return detachedChannel.write(src, offset, len);
- }
-
- @Override
- public void shutdownInput() throws IOException {
- detachedChannel.shutdownInput();
- }
-
- @Override
- public void shutdownOutput() throws IOException {
- detachedChannel.shutdownOutput();
- }
-
- @Override
- public void close() throws IOException {
- detachedChannel.close();
- }
-
- @Override
- public String toString() {
- return super.toString()+"("+ detachedChannel.toString() + ")";
- }
-
-
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/RedirectFilter.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,117 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.http;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.net.URI;
-import jdk.incubator.http.internal.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);
- }
-
- /**
- * checks to see if new request 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());
- if (canRedirect(redir) && ++exchange.numberOfRedirects < max_redirects) {
- //System.out.println("Redirecting to: " + redir);
- return new HttpRequestImpl(redir, method, request);
- } else {
- //System.out.println("Redirect: giving up");
- 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 SECURE:
- return newScheme.equalsIgnoreCase("https");
- case SAME_PROTOCOL:
- return newScheme.equalsIgnoreCase(oldScheme);
- default:
- throw new InternalError();
- }
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/RequestPublishers.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,375 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, 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.incubator.http;
-
-import java.io.File;
-import java.io.FileInputStream;
-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.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 jdk.incubator.http.HttpRequest.BodyPublisher;
-import jdk.incubator.http.internal.common.Utils;
-
-class RequestPublishers {
-
- static class ByteArrayPublisher implements HttpRequest.BodyPublisher {
- private volatile Flow.Publisher<ByteBuffer> delegate;
- private final int length;
- private final byte[] content;
- private final int offset;
- private final int bufSize;
-
- ByteArrayPublisher(byte[] content) {
- this(content, 0, content.length);
- }
-
- 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.
- static class IterablePublisher implements HttpRequest.BodyPublisher {
- private volatile Flow.Publisher<ByteBuffer> delegate;
- private final Iterable<byte[]> content;
- private volatile long contentLength;
-
- 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;
- }
- }
-
- static class StringPublisher extends ByteArrayPublisher {
- public StringPublisher(String content, Charset charset) {
- super(content.getBytes(charset));
- }
- }
-
- static class EmptyPublisher implements HttpRequest.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);
- }
- }
-
- static class FilePublisher implements BodyPublisher {
- private final File file;
- private volatile AccessControlContext acc;
-
- FilePublisher(Path name) {
- file = name.toFile();
- }
-
- void setAccessControlContext(AccessControlContext acc) {
- this.acc = acc;
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
- if (System.getSecurityManager() != null && acc == null)
- throw new InternalError(
- "Unexpected null acc when security manager has been installed");
-
- InputStream is;
- try {
- PrivilegedExceptionAction<FileInputStream> pa =
- () -> new FileInputStream(file);
- is = AccessController.doPrivileged(pa, acc);
- } catch (PrivilegedActionException pae) {
- throw new UncheckedIOException((IOException)pae.getCause());
- }
- PullPublisher<ByteBuffer> publisher =
- new PullPublisher<>(() -> new StreamIterator(is));
- publisher.subscribe(subscriber);
- }
-
- @Override
- public long contentLength() {
- assert System.getSecurityManager() != null ? acc != null: true;
- PrivilegedAction<Long> pa = () -> file.length();
- return AccessController.doPrivileged(pa, acc);
- }
- }
-
- /**
- * Reads one buffer ahead all the time, blocking in hasNext()
- */
- 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;
- }
-
- }
-
- static class InputStreamPublisher implements BodyPublisher {
- private final Supplier<? extends InputStream> streamSupplier;
-
- 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;
- }
- }
-
- static final class PublisherAdapter implements BodyPublisher {
-
- private final Publisher<? extends ByteBuffer> publisher;
- private final long contentLength;
-
- 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);
- }
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Response.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,98 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, 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.incubator.http;
-
-import java.net.URI;
-
-/**
- * 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;
-
- Response(HttpRequestImpl req,
- Exchange<?> exchange,
- HttpHeaders headers,
- int statusCode,
- HttpClient.Version version) {
- this(req, exchange, headers, statusCode, version,
- "CONNECT".equalsIgnoreCase(req.method()));
- }
-
- Response(HttpRequestImpl req,
- Exchange<?> exchange,
- HttpHeaders headers,
- int statusCode,
- HttpClient.Version version,
- boolean isConnectResponse) {
- this.headers = headers;
- this.request = req;
- this.version = version;
- this.exchange = exchange;
- this.statusCode = statusCode;
- this.isConnectResponse = isConnectResponse;
- }
-
- HttpRequestImpl request() {
- return request;
- }
-
- HttpClient.Version version() {
- return version;
- }
-
- HttpHeaders headers() {
- return headers;
- }
-
-// Exchange<?> exchange() {
-// return exchange;
-// }
-
- int statusCode() {
- return statusCode;
- }
-
- @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();
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseContent.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,465 +0,0 @@
-/*
- * 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.incubator.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 jdk.incubator.http.internal.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 {
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
-
- 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 System.Logger debug = Utils.getDebugLogger(this::dbgString, 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) {
- debug.log(Level.DEBUG, () -> "onSubscribe: "
- + pusher.getClass().getName());
- pusher.onSubscribe(this.sub = sub);
- }
-
- @Override
- public void accept(ByteBuffer b) {
- if (closedExceptionally != null) {
- debug.log(Level.DEBUG, () -> "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));
- }
- debug.log(Level.DEBUG, () -> "done!");
- assert closedExceptionally == null;
- assert state == ChunkState.DONE;
- onFinished.run();
- pusher.onComplete();
- 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));
- }
- assert state == ChunkState.DONE || !b.hasRemaining();
- } catch(Throwable 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) {
- debug.log(Level.DEBUG, () -> "Trying to read chunk len"
- + " (remaining in buffer:"+chunk.remaining()+")");
- int clen = chunklen = tryReadChunkLen(chunk);
- if (clen == -1) return READMORE;
- debug.log(Level.DEBUG, "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) {
- debug.log(Level.DEBUG,
- "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;
- debug.log(Level.DEBUG, "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();
- debug.log(Level.DEBUG, "Reading chunk: available %d, needed %d",
- bytesread, unfulfilled);
-
- int bytes2return = Math.min(bytesread, unfulfilled);
- debug.log(Level.DEBUG, "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) {
- debug.log(Level.DEBUG,
- "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;
- debug.log(Level.DEBUG, "Ready to read next chunk");
- }
- }
- if (returnBuffer == READMORE) {
- debug.log(Level.DEBUG, "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()) {
- debug.log(Level.DEBUG, "Sending chunk to consumer (%d)",
- b1.remaining());
- out.add(b1);
- debug.log(Level.DEBUG, "Chunk sent.");
- }
- 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 System.Logger debug = Utils.getDebugLogger(this::dbgString, 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) {
- debug.log(Level.DEBUG, () -> "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) {
- debug.log(Level.DEBUG, () -> "already closed: "
- + closedExceptionally);
- return;
- }
- boolean completed = false;
- try {
- int unfulfilled = remaining;
- debug.log(Level.DEBUG, "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.
- assert closedExceptionally == null;
- onFinished.run();
- pusher.onComplete();
- completed = true;
- onComplete.accept(closedExceptionally); // should be null
- } else {
- assert b.remaining() == 0;
- }
- } catch (Throwable t) {
- debug.log(Level.DEBUG, "Unexpected exception", t);
- closedExceptionally = t;
- if (!completed) {
- onComplete.accept(t);
- }
- }
- }
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLDelegate.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,489 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.http;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import javax.net.ssl.SSLEngineResult.HandshakeStatus;
-import javax.net.ssl.SSLEngineResult.Status;
-import javax.net.ssl.*;
-import jdk.incubator.http.internal.common.Log;
-import jdk.incubator.http.internal.common.Utils;
-import static javax.net.ssl.SSLEngineResult.HandshakeStatus.*;
-
-/**
- * Implements the mechanics of SSL by managing an SSLEngine object.
- * <p>
- * This class is only used to implement the {@link
- * AbstractAsyncSSLConnection.SSLConnectionChannel} which is handed of
- * to RawChannelImpl when creating a WebSocket.
- */
-class SSLDelegate {
-
- final SSLEngine engine;
- final EngineWrapper wrapper;
- final Lock handshaking = new ReentrantLock();
- final SocketChannel chan;
-
- SSLDelegate(SSLEngine eng, SocketChannel chan)
- {
- this.engine = eng;
- this.chan = chan;
- this.wrapper = new EngineWrapper(chan, engine);
- }
-
- // alpn[] may be null
-// SSLDelegate(SocketChannel chan, HttpClientImpl client, String[] alpn, String sn)
-// throws IOException
-// {
-// serverName = sn;
-// SSLContext context = client.sslContext();
-// engine = context.createSSLEngine();
-// engine.setUseClientMode(true);
-// SSLParameters sslp = client.sslParameters();
-// sslParameters = Utils.copySSLParameters(sslp);
-// if (sn != null) {
-// SNIHostName sni = new SNIHostName(sn);
-// sslParameters.setServerNames(List.of(sni));
-// }
-// if (alpn != null) {
-// sslParameters.setApplicationProtocols(alpn);
-// Log.logSSL("SSLDelegate: Setting application protocols: {0}" + Arrays.toString(alpn));
-// } else {
-// Log.logSSL("SSLDelegate: No application protocols proposed");
-// }
-// engine.setSSLParameters(sslParameters);
-// wrapper = new EngineWrapper(chan, engine);
-// this.chan = chan;
-// this.client = client;
-// }
-
-// SSLParameters getSSLParameters() {
-// return sslParameters;
-// }
-
- static long countBytes(ByteBuffer[] buffers, int start, int number) {
- long c = 0;
- for (int i=0; i<number; i++) {
- c+= buffers[start+i].remaining();
- }
- return c;
- }
-
-
- static class WrapperResult {
- static WrapperResult createOK() {
- WrapperResult r = new WrapperResult();
- r.buf = null;
- r.result = new SSLEngineResult(Status.OK, NOT_HANDSHAKING, 0, 0);
- return r;
- }
- SSLEngineResult result;
-
- ByteBuffer buf; // buffer containing result data
- }
-
- int app_buf_size;
- int packet_buf_size;
-
- enum BufType {
- PACKET,
- APPLICATION
- }
-
- ByteBuffer allocate (BufType type) {
- return allocate (type, -1);
- }
-
- // TODO: Use buffer pool for this
- ByteBuffer allocate (BufType type, int len) {
- assert engine != null;
- synchronized (this) {
- int size;
- if (type == BufType.PACKET) {
- if (packet_buf_size == 0) {
- SSLSession sess = engine.getSession();
- packet_buf_size = sess.getPacketBufferSize();
- }
- if (len > packet_buf_size) {
- packet_buf_size = len;
- }
- size = packet_buf_size;
- } else {
- if (app_buf_size == 0) {
- SSLSession sess = engine.getSession();
- app_buf_size = sess.getApplicationBufferSize();
- }
- if (len > app_buf_size) {
- app_buf_size = len;
- }
- size = app_buf_size;
- }
- return ByteBuffer.allocate (size);
- }
- }
-
- /* reallocates the buffer by :-
- * 1. creating a new buffer double the size of the old one
- * 2. putting the contents of the old buffer into the new one
- * 3. set xx_buf_size to the new size if it was smaller than new size
- *
- * flip is set to true if the old buffer needs to be flipped
- * before it is copied.
- */
- private ByteBuffer realloc (ByteBuffer b, boolean flip, BufType type) {
- // TODO: there should be the linear growth, rather than exponential as
- // we definitely know the maximum amount of space required to unwrap
- synchronized (this) {
- int nsize = 2 * b.capacity();
- ByteBuffer n = allocate (type, nsize);
- if (flip) {
- b.flip();
- }
- n.put(b);
- b = n;
- }
- return b;
- }
-
- /**
- * This is a thin wrapper over SSLEngine and the SocketChannel, which
- * guarantees the ordering of wraps/unwraps with respect to the underlying
- * channel read/writes. It handles the UNDER/OVERFLOW status codes
- * It does not handle the handshaking status codes, or the CLOSED status code
- * though once the engine is closed, any attempt to read/write to it
- * will get an exception. The overall result is returned.
- * It functions synchronously/blocking
- */
- class EngineWrapper {
-
- SocketChannel chan;
- SSLEngine engine;
- final Object wrapLock;
- final Object unwrapLock;
- ByteBuffer unwrap_src, wrap_dst;
- boolean closed = false;
- int u_remaining; // the number of bytes left in unwrap_src after an unwrap()
-
- EngineWrapper (SocketChannel chan, SSLEngine engine) {
- this.chan = chan;
- this.engine = engine;
- wrapLock = new Object();
- unwrapLock = new Object();
- unwrap_src = allocate(BufType.PACKET);
- wrap_dst = allocate(BufType.PACKET);
- }
-
-// void close () throws IOException {
-// }
-
- WrapperResult wrapAndSend(ByteBuffer src, boolean ignoreClose)
- throws IOException
- {
- ByteBuffer[] buffers = new ByteBuffer[1];
- buffers[0] = src;
- return wrapAndSend(buffers, 0, 1, ignoreClose);
- }
-
- /* try to wrap and send the data in src. Handles OVERFLOW.
- * Might block if there is an outbound blockage or if another
- * thread is calling wrap(). Also, might not send any data
- * if an unwrap is needed.
- */
- WrapperResult wrapAndSend(ByteBuffer[] src,
- int offset,
- int len,
- boolean ignoreClose)
- throws IOException
- {
- if (closed && !ignoreClose) {
- throw new IOException ("Engine is closed");
- }
- Status status;
- WrapperResult r = new WrapperResult();
- synchronized (wrapLock) {
- wrap_dst.clear();
- do {
- r.result = engine.wrap (src, offset, len, wrap_dst);
- status = r.result.getStatus();
- if (status == Status.BUFFER_OVERFLOW) {
- wrap_dst = realloc (wrap_dst, true, BufType.PACKET);
- }
- } while (status == Status.BUFFER_OVERFLOW);
- if (status == Status.CLOSED && !ignoreClose) {
- closed = true;
- return r;
- }
- if (r.result.bytesProduced() > 0) {
- wrap_dst.flip();
- int l = wrap_dst.remaining();
- assert l == r.result.bytesProduced();
- while (l>0) {
- l -= chan.write (wrap_dst);
- }
- }
- }
- return r;
- }
-
- /* block until a complete message is available and return it
- * in dst, together with the Result. dst may have been re-allocated
- * so caller should check the returned value in Result
- * If handshaking is in progress then, possibly no data is returned
- */
- WrapperResult recvAndUnwrap(ByteBuffer dst) throws IOException {
- Status status;
- WrapperResult r = new WrapperResult();
- r.buf = dst;
- if (closed) {
- throw new IOException ("Engine is closed");
- }
- boolean needData;
- if (u_remaining > 0) {
- unwrap_src.compact();
- unwrap_src.flip();
- needData = false;
- } else {
- unwrap_src.clear();
- needData = true;
- }
- synchronized (unwrapLock) {
- int x;
- do {
- if (needData) {
- x = chan.read (unwrap_src);
- if (x == -1) {
- throw new IOException ("connection closed for reading");
- }
- unwrap_src.flip();
- }
- r.result = engine.unwrap (unwrap_src, r.buf);
- status = r.result.getStatus();
- if (status == Status.BUFFER_UNDERFLOW) {
- if (unwrap_src.limit() == unwrap_src.capacity()) {
- /* buffer not big enough */
- unwrap_src = realloc (
- unwrap_src, false, BufType.PACKET
- );
- } else {
- /* Buffer not full, just need to read more
- * data off the channel. Reset pointers
- * for reading off SocketChannel
- */
- unwrap_src.position (unwrap_src.limit());
- unwrap_src.limit (unwrap_src.capacity());
- }
- needData = true;
- } else if (status == Status.BUFFER_OVERFLOW) {
- r.buf = realloc (r.buf, true, BufType.APPLICATION);
- needData = false;
- } else if (status == Status.CLOSED) {
- closed = true;
- r.buf.flip();
- return r;
- }
- } while (status != Status.OK);
- }
- u_remaining = unwrap_src.remaining();
- return r;
- }
- }
-
-// WrapperResult sendData (ByteBuffer src) throws IOException {
-// ByteBuffer[] buffers = new ByteBuffer[1];
-// buffers[0] = src;
-// return sendData(buffers, 0, 1);
-// }
-
- /**
- * send the data in the given ByteBuffer. If a handshake is needed
- * then this is handled within this method. When this call returns,
- * all of the given user data has been sent and any handshake has been
- * completed. Caller should check if engine has been closed.
- */
- WrapperResult sendData (ByteBuffer[] src, int offset, int len) throws IOException {
- WrapperResult r = WrapperResult.createOK();
- while (countBytes(src, offset, len) > 0) {
- r = wrapper.wrapAndSend(src, offset, len, false);
- Status status = r.result.getStatus();
- if (status == Status.CLOSED) {
- doClosure ();
- return r;
- }
- HandshakeStatus hs_status = r.result.getHandshakeStatus();
- if (hs_status != HandshakeStatus.FINISHED &&
- hs_status != HandshakeStatus.NOT_HANDSHAKING)
- {
- doHandshake(hs_status);
- }
- }
- return r;
- }
-
- /**
- * read data thru the engine into the given ByteBuffer. If the
- * given buffer was not large enough, a new one is allocated
- * and returned. This call handles handshaking automatically.
- * Caller should check if engine has been closed.
- */
- WrapperResult recvData (ByteBuffer dst) throws IOException {
- /* we wait until some user data arrives */
- int mark = dst.position();
- WrapperResult r = null;
- int pos = dst.position();
- while (dst.position() == pos) {
- r = wrapper.recvAndUnwrap (dst);
- dst = (r.buf != dst) ? r.buf: dst;
- Status status = r.result.getStatus();
- if (status == Status.CLOSED) {
- doClosure ();
- return r;
- }
-
- HandshakeStatus hs_status = r.result.getHandshakeStatus();
- if (hs_status != HandshakeStatus.FINISHED &&
- hs_status != HandshakeStatus.NOT_HANDSHAKING)
- {
- doHandshake (hs_status);
- }
- }
- Utils.flipToMark(dst, mark);
- return r;
- }
-
- /* we've received a close notify. Need to call wrap to send
- * the response
- */
- void doClosure () throws IOException {
- try {
- handshaking.lock();
- ByteBuffer tmp = allocate(BufType.APPLICATION);
- WrapperResult r;
- do {
- tmp.clear();
- tmp.flip ();
- r = wrapper.wrapAndSend(tmp, true);
- } while (r.result.getStatus() != Status.CLOSED);
- } finally {
- handshaking.unlock();
- }
- }
-
- /* do the (complete) handshake after acquiring the handshake lock.
- * If two threads call this at the same time, then we depend
- * on the wrapper methods being idempotent. eg. if wrapAndSend()
- * is called with no data to send then there must be no problem
- */
- @SuppressWarnings("fallthrough")
- void doHandshake (HandshakeStatus hs_status) throws IOException {
- boolean wasBlocking;
- try {
- wasBlocking = chan.isBlocking();
- handshaking.lock();
- chan.configureBlocking(true);
- ByteBuffer tmp = allocate(BufType.APPLICATION);
- while (hs_status != HandshakeStatus.FINISHED &&
- hs_status != HandshakeStatus.NOT_HANDSHAKING)
- {
- WrapperResult r = null;
- switch (hs_status) {
- case NEED_TASK:
- Runnable task;
- while ((task = engine.getDelegatedTask()) != null) {
- /* run in current thread, because we are already
- * running an external Executor
- */
- task.run();
- }
- /* fall thru - call wrap again */
- case NEED_WRAP:
- tmp.clear();
- tmp.flip();
- r = wrapper.wrapAndSend(tmp, false);
- break;
-
- case NEED_UNWRAP:
- tmp.clear();
- r = wrapper.recvAndUnwrap (tmp);
- if (r.buf != tmp) {
- tmp = r.buf;
- }
- assert tmp.position() == 0;
- break;
- }
- hs_status = r.result.getHandshakeStatus();
- }
- Log.logSSL(getSessionInfo());
- if (!wasBlocking) {
- chan.configureBlocking(false);
- }
- } finally {
- handshaking.unlock();
- }
- }
-
-// static void printParams(SSLParameters p) {
-// System.out.println("SSLParameters:");
-// if (p == null) {
-// System.out.println("Null params");
-// return;
-// }
-// for (String cipher : p.getCipherSuites()) {
-// System.out.printf("cipher: %s\n", cipher);
-// }
-// // JDK 8 EXCL START
-// for (String approto : p.getApplicationProtocols()) {
-// System.out.printf("application protocol: %s\n", approto);
-// }
-// // JDK 8 EXCL END
-// for (String protocol : p.getProtocols()) {
-// System.out.printf("protocol: %s\n", protocol);
-// }
-// if (p.getServerNames() != null) {
-// for (SNIServerName sname : p.getServerNames()) {
-// System.out.printf("server name: %s\n", sname.toString());
-// }
-// }
-// }
-
- String getSessionInfo() {
- StringBuilder sb = new StringBuilder();
- String application = engine.getApplicationProtocol();
- SSLSession sess = engine.getSession();
- String cipher = sess.getCipherSuite();
- String protocol = sess.getProtocol();
- sb.append("Handshake complete alpn: ")
- .append(application)
- .append(", Cipher: ")
- .append(cipher)
- .append(", Protocol: ")
- .append(protocol);
- return sb.toString();
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SocketTube.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,956 +0,0 @@
-/*
- * Copyright (c) 2017, 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.incubator.http;
-
-import java.io.EOFException;
-import java.io.IOException;
-import java.lang.System.Logger.Level;
-import java.nio.ByteBuffer;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.Flow;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.atomic.AtomicReference;
-import java.nio.channels.SelectableChannel;
-import java.nio.channels.SelectionKey;
-import java.nio.channels.SocketChannel;
-import java.util.ArrayList;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-import jdk.incubator.http.internal.common.Demand;
-import jdk.incubator.http.internal.common.FlowTube;
-import jdk.incubator.http.internal.common.SequentialScheduler;
-import jdk.incubator.http.internal.common.SequentialScheduler.DeferredCompleter;
-import jdk.incubator.http.internal.common.SequentialScheduler.RestartableTask;
-import jdk.incubator.http.internal.common.Utils;
-
-/**
- * A SocketTube is a terminal tube plugged directly into the socket.
- * The read subscriber should call {@code subscribe} on the SocketTube before
- * the SocketTube can be subscribed to the write publisher.
- */
-final class SocketTube implements FlowTube {
-
- static final boolean DEBUG = Utils.DEBUG; // revisit: temporary developer's flag
- final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
- static final AtomicLong IDS = new AtomicLong();
-
- private final HttpClientImpl client;
- private final SocketChannel channel;
- private final Supplier<ByteBuffer> buffersSource;
- private final Object lock = new Object();
- private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
- private final InternalReadPublisher readPublisher;
- private final InternalWriteSubscriber writeSubscriber;
- private final long id = IDS.incrementAndGet();
-
- public SocketTube(HttpClientImpl client, SocketChannel channel,
- Supplier<ByteBuffer> buffersSource) {
- this.client = client;
- this.channel = channel;
- this.buffersSource = buffersSource;
- this.readPublisher = new InternalReadPublisher();
- this.writeSubscriber = new InternalWriteSubscriber();
- }
-
-// private static Flow.Subscription nopSubscription() {
-// return new Flow.Subscription() {
-// @Override public void request(long n) { }
-// @Override public void cancel() { }
-// };
-// }
-
- /**
- * Returns {@code true} if this flow is finished.
- * This happens when this flow internal read subscription is completed,
- * either normally (EOF reading) or exceptionally (EOF writing, or
- * underlying socket closed, or some exception occurred while reading or
- * writing to the socket).
- *
- * @return {@code true} if this flow is finished.
- */
- public boolean isFinished() {
- InternalReadPublisher.InternalReadSubscription subscription =
- readPublisher.subscriptionImpl;
- return subscription != null && subscription.completed
- || subscription == null && errorRef.get() != null;
- }
-
- // ===================================================================== //
- // Flow.Publisher //
- // ======================================================================//
-
- /**
- * {@inheritDoc }
- * @apiNote This method should be called first. In particular, the caller
- * must ensure that this method must be called by the read
- * subscriber before the write publisher can call {@code onSubscribe}.
- * Failure to adhere to this contract may result in assertion errors.
- */
- @Override
- public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
- Objects.requireNonNull(s);
- assert s instanceof TubeSubscriber : "Expected TubeSubscriber, got:" + s;
- readPublisher.subscribe(s);
- }
-
-
- // ===================================================================== //
- // Flow.Subscriber //
- // ======================================================================//
-
- /**
- * {@inheritDoc }
- * @apiNote The caller must ensure that {@code subscribe} is called by
- * the read subscriber before {@code onSubscribe} is called by
- * the write publisher.
- * Failure to adhere to this contract may result in assertion errors.
- */
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- writeSubscriber.onSubscribe(subscription);
- }
-
- @Override
- public void onNext(List<ByteBuffer> item) {
- writeSubscriber.onNext(item);
- }
-
- @Override
- public void onError(Throwable throwable) {
- writeSubscriber.onError(throwable);
- }
-
- @Override
- public void onComplete() {
- writeSubscriber.onComplete();
- }
-
- // ===================================================================== //
- // Events //
- // ======================================================================//
-
- /**
- * A restartable task used to process tasks in sequence.
- */
- private static class SocketFlowTask implements RestartableTask {
- final Runnable task;
- private final Object monitor = new Object();
- SocketFlowTask(Runnable task) {
- this.task = task;
- }
- @Override
- public final void run(DeferredCompleter taskCompleter) {
- try {
- // non contentious synchronized for visibility.
- synchronized(monitor) {
- task.run();
- }
- } finally {
- taskCompleter.complete();
- }
- }
- }
-
- // This is best effort - there's no guarantee that the printed set
- // of values is consistent. It should only be considered as
- // weakly accurate - in particular in what concerns the events states,
- // especially when displaying a read event state from a write event
- // callback and conversely.
- void debugState(String when) {
- if (debug.isLoggable(Level.DEBUG)) {
- StringBuilder state = new StringBuilder();
-
- InternalReadPublisher.InternalReadSubscription sub =
- readPublisher.subscriptionImpl;
- InternalReadPublisher.ReadEvent readEvent =
- sub == null ? null : sub.readEvent;
- Demand rdemand = sub == null ? null : sub.demand;
- InternalWriteSubscriber.WriteEvent writeEvent =
- writeSubscriber.writeEvent;
- AtomicLong wdemand = writeSubscriber.writeDemand;
- int rops = readEvent == null ? 0 : readEvent.interestOps();
- long rd = rdemand == null ? 0 : rdemand.get();
- int wops = writeEvent == null ? 0 : writeEvent.interestOps();
- long wd = wdemand == null ? 0 : wdemand.get();
-
- state.append(when).append(" Reading: [ops=")
- .append(rops).append(", demand=").append(rd)
- .append(", stopped=")
- .append((sub == null ? false : sub.readScheduler.isStopped()))
- .append("], Writing: [ops=").append(wops)
- .append(", demand=").append(wd)
- .append("]");
- debug.log(Level.DEBUG, state.toString());
- }
- }
-
- /**
- * A repeatable event that can be paused or resumed by changing
- * its interestOps.
- * When the event is fired, it is first paused before being signaled.
- * It is the responsibility of the code triggered by {@code signalEvent}
- * to resume the event if required.
- */
- private static abstract class SocketFlowEvent extends AsyncEvent {
- final SocketChannel channel;
- final int defaultInterest;
- volatile int interestOps;
- volatile boolean registered;
- SocketFlowEvent(int defaultInterest, SocketChannel channel) {
- super(AsyncEvent.REPEATING);
- this.defaultInterest = defaultInterest;
- this.channel = channel;
- }
- final boolean registered() {return registered;}
- final void resume() {
- interestOps = defaultInterest;
- registered = true;
- }
- final void pause() {interestOps = 0;}
- @Override
- public final SelectableChannel channel() {return channel;}
- @Override
- public final int interestOps() {return interestOps;}
-
- @Override
- public final void handle() {
- pause(); // pause, then signal
- signalEvent(); // won't be fired again until resumed.
- }
- @Override
- public final void abort(IOException error) {
- debug().log(Level.DEBUG, () -> "abort: " + error);
- pause(); // pause, then signal
- signalError(error); // should not be resumed after abort (not checked)
- }
-
- protected abstract void signalEvent();
- protected abstract void signalError(Throwable error);
- abstract System.Logger debug();
- }
-
- // ===================================================================== //
- // Writing //
- // ======================================================================//
-
- // This class makes the assumption that the publisher will call
- // onNext sequentially, and that onNext won't be called if the demand
- // has not been incremented by request(1).
- // It has a 'queue of 1' meaning that it will call request(1) in
- // onSubscribe, and then only after its 'current' buffer list has been
- // fully written and current set to null;
- private final class InternalWriteSubscriber
- implements Flow.Subscriber<List<ByteBuffer>> {
-
- volatile Flow.Subscription subscription;
- volatile List<ByteBuffer> current;
- volatile boolean completed;
- final WriteEvent writeEvent = new WriteEvent(channel, this);
- final AtomicLong writeDemand = new AtomicLong();
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- Flow.Subscription previous = this.subscription;
- this.subscription = subscription;
- debug.log(Level.DEBUG, "subscribed for writing");
- if (current == null) {
- if (previous == subscription || previous == null) {
- if (writeDemand.compareAndSet(0, 1)) {
- subscription.request(1);
- }
- } else {
- writeDemand.set(1);
- subscription.request(1);
- }
- }
- }
-
- @Override
- public void onNext(List<ByteBuffer> bufs) {
- assert current == null; // this is a queue of 1.
- assert subscription != null;
- current = bufs;
- tryFlushCurrent(client.isSelectorThread()); // may be in selector thread
- // For instance in HTTP/2, a received SETTINGS frame might trigger
- // the sending of a SETTINGS frame in turn which might cause
- // onNext to be called from within the same selector thread that the
- // original SETTINGS frames arrived on. If rs is the read-subscriber
- // and ws is the write-subscriber then the following can occur:
- // ReadEvent -> rs.onNext(bytes) -> process server SETTINGS -> write
- // client SETTINGS -> ws.onNext(bytes) -> tryFlushCurrent
- debugState("leaving w.onNext");
- }
-
- // we don't use a SequentialScheduler here: we rely on
- // onNext() being called sequentially, and not being called
- // if we haven't call request(1)
- // onNext is usually called from within a user/executor thread.
- // we will perform the initial writing in that thread.
- // if for some reason, not all data can be written, the writeEvent
- // will be resumed, and the rest of the data will be written from
- // the selector manager thread when the writeEvent is fired.
- // If we are in the selector manager thread, then we will use the executor
- // to call request(1), ensuring that onNext() won't be called from
- // within the selector thread.
- // If we are not in the selector manager thread, then we don't care.
- void tryFlushCurrent(boolean inSelectorThread) {
- List<ByteBuffer> bufs = current;
- if (bufs == null) return;
- try {
- assert inSelectorThread == client.isSelectorThread() :
- "should " + (inSelectorThread ? "" : "not ")
- + " be in the selector thread";
- long remaining = Utils.remaining(bufs);
- debug.log(Level.DEBUG, "trying to write: %d", remaining);
- long written = writeAvailable(bufs);
- debug.log(Level.DEBUG, "wrote: %d", remaining);
- if (written == -1) {
- signalError(new EOFException("EOF reached while writing"));
- return;
- }
- assert written <= remaining;
- if (remaining - written == 0) {
- current = null;
- writeDemand.decrementAndGet();
- Runnable requestMore = this::requestMore;
- if (inSelectorThread) {
- assert client.isSelectorThread();
- client.theExecutor().execute(requestMore);
- } else {
- assert !client.isSelectorThread();
- requestMore.run();
- }
- } else {
- resumeWriteEvent(inSelectorThread);
- }
- } catch (Throwable t) {
- signalError(t);
- subscription.cancel();
- }
- }
-
- void requestMore() {
- try {
- if (completed) return;
- long d = writeDemand.get();
- if (writeDemand.compareAndSet(0,1)) {
- debug.log(Level.DEBUG, "write: requesting more...");
- subscription.request(1);
- } else {
- debug.log(Level.DEBUG, "write: no need to request more: %d", d);
- }
- } catch (Throwable t) {
- debug.log(Level.DEBUG, () ->
- "write: error while requesting more: " + t);
- signalError(t);
- subscription.cancel();
- } finally {
- debugState("leaving requestMore: ");
- }
- }
-
- @Override
- public void onError(Throwable throwable) {
- signalError(throwable);
- }
-
- @Override
- public void onComplete() {
- completed = true;
- // no need to pause the write event here: the write event will
- // be paused if there is nothing more to write.
- List<ByteBuffer> bufs = current;
- long remaining = bufs == null ? 0 : Utils.remaining(bufs);
- debug.log(Level.DEBUG, "write completed, %d yet to send", remaining);
- debugState("InternalWriteSubscriber::onComplete");
- }
-
- void resumeWriteEvent(boolean inSelectorThread) {
- debug.log(Level.DEBUG, "scheduling write event");
- resumeEvent(writeEvent, this::signalError);
- }
-
-// void pauseWriteEvent() {
-// debug.log(Level.DEBUG, "pausing write event");
-// pauseEvent(writeEvent, this::signalError);
-// }
-
- void signalWritable() {
- debug.log(Level.DEBUG, "channel is writable");
- tryFlushCurrent(true);
- }
-
- void signalError(Throwable error) {
- debug.log(Level.DEBUG, () -> "write error: " + error);
- completed = true;
- readPublisher.signalError(error);
- }
-
- // A repeatable WriteEvent which is paused after firing and can
- // be resumed if required - see SocketFlowEvent;
- final class WriteEvent extends SocketFlowEvent {
- final InternalWriteSubscriber sub;
- WriteEvent(SocketChannel channel, InternalWriteSubscriber sub) {
- super(SelectionKey.OP_WRITE, channel);
- this.sub = sub;
- }
- @Override
- protected final void signalEvent() {
- try {
- client.eventUpdated(this);
- sub.signalWritable();
- } catch(Throwable t) {
- sub.signalError(t);
- }
- }
-
- @Override
- protected void signalError(Throwable error) {
- sub.signalError(error);
- }
-
- @Override
- System.Logger debug() {
- return debug;
- }
-
- }
-
- }
-
- // ===================================================================== //
- // Reading //
- // ===================================================================== //
-
- // The InternalReadPublisher uses a SequentialScheduler to ensure that
- // onNext/onError/onComplete are called sequentially on the caller's
- // subscriber.
- // However, it relies on the fact that the only time where
- // runOrSchedule() is called from a user/executor thread is in signalError,
- // right after the errorRef has been set.
- // Because the sequential scheduler's task always checks for errors first,
- // and always terminate the scheduler on error, then it is safe to assume
- // that if it reaches the point where it reads from the channel, then
- // it is running in the SelectorManager thread. This is because all
- // other invocation of runOrSchedule() are triggered from within a
- // ReadEvent.
- //
- // When pausing/resuming the event, some shortcuts can then be taken
- // when we know we're running in the selector manager thread
- // (in that case there's no need to call client.eventUpdated(readEvent);
- //
- private final class InternalReadPublisher
- implements Flow.Publisher<List<ByteBuffer>> {
- private final InternalReadSubscription subscriptionImpl
- = new InternalReadSubscription();
- AtomicReference<ReadSubscription> pendingSubscription = new AtomicReference<>();
- private volatile ReadSubscription subscription;
-
- @Override
- public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
- Objects.requireNonNull(s);
-
- TubeSubscriber sub = FlowTube.asTubeSubscriber(s);
- ReadSubscription target = new ReadSubscription(subscriptionImpl, sub);
- ReadSubscription previous = pendingSubscription.getAndSet(target);
-
- if (previous != null && previous != target) {
- debug.log(Level.DEBUG,
- () -> "read publisher: dropping pending subscriber: "
- + previous.subscriber);
- previous.errorRef.compareAndSet(null, errorRef.get());
- previous.signalOnSubscribe();
- if (subscriptionImpl.completed) {
- previous.signalCompletion();
- } else {
- previous.subscriber.dropSubscription();
- }
- }
-
- debug.log(Level.DEBUG, "read publisher got subscriber");
- subscriptionImpl.signalSubscribe();
- debugState("leaving read.subscribe: ");
- }
-
- void signalError(Throwable error) {
- if (!errorRef.compareAndSet(null, error)) {
- return;
- }
- subscriptionImpl.handleError();
- }
-
- final class ReadSubscription implements Flow.Subscription {
- final InternalReadSubscription impl;
- final TubeSubscriber subscriber;
- final AtomicReference<Throwable> errorRef = new AtomicReference<>();
- volatile boolean subscribed;
- volatile boolean cancelled;
- volatile boolean completed;
-
- public ReadSubscription(InternalReadSubscription impl,
- TubeSubscriber subscriber) {
- this.impl = impl;
- this.subscriber = subscriber;
- }
-
- @Override
- public void cancel() {
- cancelled = true;
- }
-
- @Override
- public void request(long n) {
- if (!cancelled) {
- impl.request(n);
- } else {
- debug.log(Level.DEBUG,
- "subscription cancelled, ignoring request %d", n);
- }
- }
-
- void signalCompletion() {
- assert subscribed || cancelled;
- if (completed || cancelled) return;
- synchronized (this) {
- if (completed) return;
- completed = true;
- }
- Throwable error = errorRef.get();
- if (error != null) {
- debug.log(Level.DEBUG, () ->
- "forwarding error to subscriber: "
- + error);
- subscriber.onError(error);
- } else {
- debug.log(Level.DEBUG, "completing subscriber");
- subscriber.onComplete();
- }
- }
-
- void signalOnSubscribe() {
- if (subscribed || cancelled) return;
- synchronized (this) {
- if (subscribed || cancelled) return;
- subscribed = true;
- }
- subscriber.onSubscribe(this);
- debug.log(Level.DEBUG, "onSubscribe called");
- if (errorRef.get() != null) {
- signalCompletion();
- }
- }
- }
-
- final class InternalReadSubscription implements Flow.Subscription {
-
- private final Demand demand = new Demand();
- final SequentialScheduler readScheduler;
- private volatile boolean completed;
- private final ReadEvent readEvent;
- private final AsyncEvent subscribeEvent;
-
- InternalReadSubscription() {
- readScheduler = new SequentialScheduler(new SocketFlowTask(this::read));
- subscribeEvent = new AsyncTriggerEvent(this::signalError,
- this::handleSubscribeEvent);
- readEvent = new ReadEvent(channel, this);
- }
-
- /*
- * This method must be invoked before any other method of this class.
- */
- final void signalSubscribe() {
- if (readScheduler.isStopped() || completed) {
- // if already completed or stopped we can handle any
- // pending connection directly from here.
- debug.log(Level.DEBUG,
- "handling pending subscription while completed");
- handlePending();
- } else {
- try {
- debug.log(Level.DEBUG,
- "registering subscribe event");
- client.registerEvent(subscribeEvent);
- } catch (Throwable t) {
- signalError(t);
- handlePending();
- }
- }
- }
-
- final void handleSubscribeEvent() {
- assert client.isSelectorThread();
- debug.log(Level.DEBUG, "subscribe event raised");
- readScheduler.runOrSchedule();
- if (readScheduler.isStopped() || completed) {
- // if already completed or stopped we can handle any
- // pending connection directly from here.
- debug.log(Level.DEBUG,
- "handling pending subscription when completed");
- handlePending();
- }
- }
-
-
- /*
- * Although this method is thread-safe, the Reactive-Streams spec seems
- * to not require it to be as such. It's a responsibility of the
- * subscriber to signal demand in a thread-safe manner.
- *
- * https://github.com/reactive-streams/reactive-streams-jvm/blob/dd24d2ab164d7de6c316f6d15546f957bec29eaa/README.md
- * (rules 2.7 and 3.4)
- */
- @Override
- public final void request(long n) {
- if (n > 0L) {
- boolean wasFulfilled = demand.increase(n);
- if (wasFulfilled) {
- debug.log(Level.DEBUG, "got some demand for reading");
- resumeReadEvent();
- // if demand has been changed from fulfilled
- // to unfulfilled register read event;
- }
- } else {
- signalError(new IllegalArgumentException("non-positive request"));
- }
- debugState("leaving request("+n+"): ");
- }
-
- @Override
- public final void cancel() {
- pauseReadEvent();
- readScheduler.stop();
- }
-
- private void resumeReadEvent() {
- debug.log(Level.DEBUG, "resuming read event");
- resumeEvent(readEvent, this::signalError);
- }
-
- private void pauseReadEvent() {
- debug.log(Level.DEBUG, "pausing read event");
- pauseEvent(readEvent, this::signalError);
- }
-
-
- final void handleError() {
- assert errorRef.get() != null;
- readScheduler.runOrSchedule();
- }
-
- final void signalError(Throwable error) {
- if (!errorRef.compareAndSet(null, error)) {
- return;
- }
- debug.log(Level.DEBUG, () -> "got read error: " + error);
- readScheduler.runOrSchedule();
- }
-
- final void signalReadable() {
- readScheduler.runOrSchedule();
- }
-
- /** The body of the task that runs in SequentialScheduler. */
- final void read() {
- // It is important to only call pauseReadEvent() when stopping
- // the scheduler. The event is automatically paused before
- // firing, and trying to pause it again could cause a race
- // condition between this loop, which calls tryDecrementDemand(),
- // and the thread that calls request(n), which will try to resume
- // reading.
- try {
- while(!readScheduler.isStopped()) {
- if (completed) return;
-
- // make sure we have a subscriber
- if (handlePending()) {
- debug.log(Level.DEBUG, "pending subscriber subscribed");
- return;
- }
-
- // If an error was signaled, we might not be in the
- // the selector thread, and that is OK, because we
- // will just call onError and return.
- ReadSubscription current = subscription;
- TubeSubscriber subscriber = current.subscriber;
- Throwable error = errorRef.get();
- if (error != null) {
- completed = true;
- // safe to pause here because we're finished anyway.
- pauseReadEvent();
- debug.log(Level.DEBUG, () -> "Sending error " + error
- + " to subscriber " + subscriber);
- current.errorRef.compareAndSet(null, error);
- current.signalCompletion();
- readScheduler.stop();
- debugState("leaving read() loop with error: ");
- return;
- }
-
- // If we reach here then we must be in the selector thread.
- assert client.isSelectorThread();
- if (demand.tryDecrement()) {
- // we have demand.
- try {
- List<ByteBuffer> bytes = readAvailable();
- if (bytes == EOF) {
- if (!completed) {
- debug.log(Level.DEBUG, "got read EOF");
- completed = true;
- // safe to pause here because we're finished
- // anyway.
- pauseReadEvent();
- current.signalCompletion();
- readScheduler.stop();
- }
- debugState("leaving read() loop after EOF: ");
- return;
- } else if (Utils.remaining(bytes) > 0) {
- // the subscriber is responsible for offloading
- // to another thread if needed.
- debug.log(Level.DEBUG, () -> "read bytes: "
- + Utils.remaining(bytes));
- assert !current.completed;
- subscriber.onNext(bytes);
- // we could continue looping until the demand
- // reaches 0. However, that would risk starving
- // other connections (bound to other socket
- // channels) - as other selected keys activated
- // by the selector manager thread might be
- // waiting for this event to terminate.
- // So resume the read event and return now...
- resumeReadEvent();
- debugState("leaving read() loop after onNext: ");
- return;
- } else {
- // nothing available!
- debug.log(Level.DEBUG, "no more bytes available");
- // re-increment the demand and resume the read
- // event. This ensures that this loop is
- // executed again when the socket becomes
- // readable again.
- demand.increase(1);
- resumeReadEvent();
- debugState("leaving read() loop with no bytes");
- return;
- }
- } catch (Throwable x) {
- signalError(x);
- continue;
- }
- } else {
- debug.log(Level.DEBUG, "no more demand for reading");
- // the event is paused just after firing, so it should
- // still be paused here, unless the demand was just
- // incremented from 0 to n, in which case, the
- // event will be resumed, causing this loop to be
- // invoked again when the socket becomes readable:
- // This is what we want.
- // Trying to pause the event here would actually
- // introduce a race condition between this loop and
- // request(n).
- debugState("leaving read() loop with no demand");
- break;
- }
- }
- } catch (Throwable t) {
- debug.log(Level.DEBUG, "Unexpected exception in read loop", t);
- signalError(t);
- } finally {
- handlePending();
- }
- }
-
- boolean handlePending() {
- ReadSubscription pending = pendingSubscription.getAndSet(null);
- if (pending == null) return false;
- debug.log(Level.DEBUG, "handling pending subscription for %s",
- pending.subscriber);
- ReadSubscription current = subscription;
- if (current != null && current != pending && !completed) {
- current.subscriber.dropSubscription();
- }
- debug.log(Level.DEBUG, "read demand reset to 0");
- subscriptionImpl.demand.reset(); // subscriber will increase demand if it needs to.
- pending.errorRef.compareAndSet(null, errorRef.get());
- if (!readScheduler.isStopped()) {
- subscription = pending;
- } else {
- debug.log(Level.DEBUG, "socket tube is already stopped");
- }
- debug.log(Level.DEBUG, "calling onSubscribe");
- pending.signalOnSubscribe();
- if (completed) {
- pending.errorRef.compareAndSet(null, errorRef.get());
- pending.signalCompletion();
- }
- return true;
- }
- }
-
-
- // A repeatable ReadEvent which is paused after firing and can
- // be resumed if required - see SocketFlowEvent;
- final class ReadEvent extends SocketFlowEvent {
- final InternalReadSubscription sub;
- ReadEvent(SocketChannel channel, InternalReadSubscription sub) {
- super(SelectionKey.OP_READ, channel);
- this.sub = sub;
- }
- @Override
- protected final void signalEvent() {
- try {
- client.eventUpdated(this);
- sub.signalReadable();
- } catch(Throwable t) {
- sub.signalError(t);
- }
- }
-
- @Override
- protected final void signalError(Throwable error) {
- sub.signalError(error);
- }
-
- @Override
- System.Logger debug() {
- return debug;
- }
- }
-
- }
-
- // ===================================================================== //
- // Socket Channel Read/Write //
- // ===================================================================== //
- static final int MAX_BUFFERS = 3;
- static final List<ByteBuffer> EOF = List.of();
-
- private List<ByteBuffer> readAvailable() throws IOException {
- ByteBuffer buf = buffersSource.get();
- assert buf.hasRemaining();
-
- int read;
- int pos = buf.position();
- List<ByteBuffer> list = null;
- while (buf.hasRemaining()) {
- while ((read = channel.read(buf)) > 0) {
- if (!buf.hasRemaining()) break;
- }
-
- // nothing read;
- if (buf.position() == pos) {
- // An empty list signal the end of data, and should only be
- // returned if read == -1.
- // If we already read some data, then we must return what we have
- // read, and -1 will be returned next time the caller attempts to
- // read something.
- if (list == null && read == -1) { // eof
- list = EOF;
- break;
- }
- }
- buf.limit(buf.position());
- buf.position(pos);
- if (list == null) {
- list = List.of(buf);
- } else {
- if (!(list instanceof ArrayList)) {
- list = new ArrayList<>(list);
- }
- list.add(buf);
- }
- if (read <= 0 || list.size() == MAX_BUFFERS) break;
- buf = buffersSource.get();
- pos = buf.position();
- assert buf.hasRemaining();
- }
- return list;
- }
-
- private long writeAvailable(List<ByteBuffer> bytes) throws IOException {
- ByteBuffer[] srcs = bytes.toArray(Utils.EMPTY_BB_ARRAY);
- final long remaining = Utils.remaining(srcs);
- long written = 0;
- while (remaining > written) {
- long w = channel.write(srcs);
- if (w == -1 && written == 0) return -1;
- if (w == 0) break;
- written += w;
- }
- return written;
- }
-
- private void resumeEvent(SocketFlowEvent event,
- Consumer<Throwable> errorSignaler) {
- boolean registrationRequired;
- synchronized(lock) {
- registrationRequired = !event.registered();
- event.resume();
- }
- try {
- if (registrationRequired) {
- client.registerEvent(event);
- } else {
- client.eventUpdated(event);
- }
- } catch(Throwable t) {
- errorSignaler.accept(t);
- }
- }
-
- private void pauseEvent(SocketFlowEvent event,
- Consumer<Throwable> errorSignaler) {
- synchronized(lock) {
- event.pause();
- }
- try {
- client.eventUpdated(event);
- } catch(Throwable t) {
- errorSignaler.accept(t);
- }
- }
-
- @Override
- public void connectFlows(TubePublisher writePublisher,
- TubeSubscriber readSubscriber) {
- debug.log(Level.DEBUG, "connecting flows");
- this.subscribe(readSubscriber);
- writePublisher.subscribe(this);
- }
-
-
- @Override
- public String toString() {
- return dbgString();
- }
-
- final String dbgString() {
- return "SocketTube("+id+")";
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Stream.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1166 +0,0 @@
-/*
- * 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.incubator.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.Collections;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentLinkedDeque;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Flow;
-import java.util.concurrent.Flow.Subscription;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.BiPredicate;
-import jdk.incubator.http.HttpResponse.BodySubscriber;
-import jdk.incubator.http.internal.common.*;
-import jdk.incubator.http.internal.frame.*;
-import jdk.incubator.http.internal.hpack.DecodingCallback;
-
-/**
- * Http/2 Stream handling.
- *
- * REQUESTS
- *
- * sendHeadersOnly() -- assembles HEADERS frame and puts on connection outbound Q
- *
- * sendRequest() -- sendHeadersOnly() + sendBody()
- *
- * sendBodyAsync() -- calls sendBody() in an executor thread.
- *
- * sendHeadersAsync() -- calls sendHeadersOnly() which does not block
- *
- * sendRequestAsync() -- calls sendRequest() in an executor thread
- *
- * RESPONSES
- *
- * Multiple responses can be received per request. Responses are queued up on
- * a LinkedList of CF<HttpResponse> and the the first one on the list is completed
- * with the next response
- *
- * getResponseAsync() -- queries list of response CFs and returns first one
- * if one exists. Otherwise, creates one and adds it to list
- * and returns it. Completion is achieved through the
- * incoming() upcall from connection reader thread.
- *
- * getResponse() -- calls getResponseAsync() and waits for CF to complete
- *
- * responseBodyAsync() -- calls responseBody() in an executor thread.
- *
- * incoming() -- entry point called from connection reader thread. Frames are
- * either handled immediately without blocking or for data frames
- * placed on the stream's inputQ which is consumed by the stream's
- * reader thread.
- *
- * PushedStream sub class
- * ======================
- * Sending side methods are not used because the request comes from a PUSH_PROMISE
- * frame sent by the server. When a PUSH_PROMISE is received the PushedStream
- * is created. PushedStream does not use responseCF list as there can be only
- * one response. The CF is created when the object created and when the response
- * HEADERS frame is received the object is completed.
- */
-class Stream<T> extends ExchangeImpl<T> {
-
- final static boolean DEBUG = Utils.DEBUG; // Revisit: temporary developer's flag
- final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
-
- final ConcurrentLinkedQueue<Http2Frame> inputQ = new ConcurrentLinkedQueue<>();
- final SequentialScheduler sched =
- SequentialScheduler.synchronizedScheduler(this::schedule);
- final SubscriptionBase userSubscription = new SubscriptionBase(sched, this::cancel);
-
- /**
- * This stream's identifier. Assigned lazily by the HTTP2Connection before
- * the stream's first frame is sent.
- */
- protected volatile int streamid;
-
- long requestContentLen;
-
- final Http2Connection connection;
- final HttpRequestImpl request;
- final DecodingCallback rspHeadersConsumer;
- HttpHeadersImpl responseHeaders;
- final HttpHeadersImpl requestPseudoHeaders;
- volatile HttpResponse.BodySubscriber<T> responseSubscriber;
- final HttpRequest.BodyPublisher requestPublisher;
- volatile RequestSubscriber requestSubscriber;
- volatile int responseCode;
- volatile Response response;
- volatile Throwable failed; // The exception with which this stream was canceled.
- final CompletableFuture<Void> requestBodyCF = new MinimalFuture<>();
- volatile CompletableFuture<T> responseBodyCF;
-
- /** True if END_STREAM has been seen in a frame received on this stream. */
- private volatile boolean remotelyClosed;
- private volatile boolean closed;
- private volatile boolean endStreamSent;
-
- // state flags
- private boolean requestSent, responseReceived;
-
- /**
- * A reference to this Stream's connection Send Window controller. The
- * stream MUST acquire the appropriate amount of Send Window before
- * sending any data. Will be null for PushStreams, as they cannot send data.
- */
- private final WindowController windowController;
- private final WindowUpdateSender windowUpdater;
-
- @Override
- HttpConnection connection() {
- return connection.connection;
- }
-
- /**
- * Invoked either from incoming() -> {receiveDataFrame() or receiveResetFrame() }
- * of after user subscription window has re-opened, from SubscriptionBase.request()
- */
- private void schedule() {
- if (responseSubscriber == null)
- // can't process anything yet
- return;
-
- try {
- while (!inputQ.isEmpty()) {
- Http2Frame frame = inputQ.peek();
- if (frame instanceof ResetFrame) {
- inputQ.remove();
- handleReset((ResetFrame)frame);
- return;
- }
- DataFrame df = (DataFrame)frame;
- boolean finished = df.getFlag(DataFrame.END_STREAM);
-
- List<ByteBuffer> buffers = df.getData();
- List<ByteBuffer> dsts = Collections.unmodifiableList(buffers);
- int size = Utils.remaining(dsts, Integer.MAX_VALUE);
- if (size == 0 && finished) {
- inputQ.remove();
- Log.logTrace("responseSubscriber.onComplete");
- debug.log(Level.DEBUG, "incoming: onComplete");
- sched.stop();
- responseSubscriber.onComplete();
- setEndStreamReceived();
- return;
- } else if (userSubscription.tryDecrement()) {
- inputQ.remove();
- Log.logTrace("responseSubscriber.onNext {0}", size);
- debug.log(Level.DEBUG, "incoming: onNext(%d)", size);
- responseSubscriber.onNext(dsts);
- if (consumed(df)) {
- Log.logTrace("responseSubscriber.onComplete");
- debug.log(Level.DEBUG, "incoming: onComplete");
- sched.stop();
- responseSubscriber.onComplete();
- setEndStreamReceived();
- return;
- }
- } else {
- return;
- }
- }
- } catch (Throwable throwable) {
- failed = throwable;
- }
-
- Throwable t = failed;
- if (t != null) {
- sched.stop();
- responseSubscriber.onError(t);
- close();
- }
- }
-
- // Callback invoked after the Response BodySubscriber has consumed the
- // buffers contained in a DataFrame.
- // Returns true if END_STREAM is reached, false otherwise.
- private boolean consumed(DataFrame df) {
- // RFC 7540 6.1:
- // The entire DATA frame payload is included in flow control,
- // including the Pad Length and Padding fields if present
- int len = df.payloadLength();
- connection.windowUpdater.update(len);
-
- if (!df.getFlag(DataFrame.END_STREAM)) {
- // Don't send window update on a stream which is
- // closed or half closed.
- windowUpdater.update(len);
- return false; // more data coming
- }
- return true; // end of stream
- }
-
- @Override
- CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
- boolean returnConnectionToPool,
- Executor executor)
- {
- Log.logTrace("Reading body on stream {0}", streamid);
- BodySubscriber<T> bodySubscriber = handler.apply(responseCode, responseHeaders);
- CompletableFuture<T> cf = receiveData(bodySubscriber);
-
- PushGroup<?> pg = exchange.getPushGroup();
- if (pg != null) {
- // if an error occurs make sure it is recorded in the PushGroup
- cf = cf.whenComplete((t,e) -> pg.pushError(e));
- }
- return cf;
- }
-
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- sb.append("streamid: ")
- .append(streamid);
- return sb.toString();
- }
-
- private void receiveDataFrame(DataFrame df) {
- inputQ.add(df);
- sched.runOrSchedule();
- }
-
- /** Handles a RESET frame. RESET is always handled inline in the queue. */
- private void receiveResetFrame(ResetFrame frame) {
- inputQ.add(frame);
- sched.runOrSchedule();
- }
-
- // pushes entire response body into response subscriber
- // blocking when required by local or remote flow control
- CompletableFuture<T> receiveData(BodySubscriber<T> bodySubscriber) {
- responseBodyCF = MinimalFuture.of(bodySubscriber.getBody());
-
- if (isCanceled()) {
- Throwable t = getCancelCause();
- responseBodyCF.completeExceptionally(t);
- } else {
- bodySubscriber.onSubscribe(userSubscription);
- }
- // Set the responseSubscriber field now that onSubscribe has been called.
- // This effectively allows the scheduler to start invoking the callbacks.
- responseSubscriber = bodySubscriber;
- sched.runOrSchedule(); // in case data waiting already to be processed
- return responseBodyCF;
- }
-
- @Override
- CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
- return sendBodyImpl().thenApply( v -> this);
- }
-
- @SuppressWarnings("unchecked")
- Stream(Http2Connection connection,
- Exchange<T> e,
- WindowController windowController)
- {
- super(e);
- this.connection = connection;
- this.windowController = windowController;
- this.request = e.request();
- this.requestPublisher = request.requestPublisher; // may be null
- responseHeaders = new HttpHeadersImpl();
- rspHeadersConsumer = (name, value) -> {
- responseHeaders.addHeader(name.toString(), value.toString());
- if (Log.headers() && Log.trace()) {
- Log.logTrace("RECEIVED HEADER (streamid={0}): {1}: {2}",
- streamid, name, value);
- }
- };
- this.requestPseudoHeaders = new HttpHeadersImpl();
- // NEW
- this.windowUpdater = new StreamWindowUpdateSender(connection);
- }
-
- /**
- * Entry point from Http2Connection reader thread.
- *
- * Data frames will be removed by response body thread.
- */
- void incoming(Http2Frame frame) throws IOException {
- debug.log(Level.DEBUG, "incoming: %s", frame);
- if ((frame instanceof HeaderFrame)) {
- HeaderFrame hframe = (HeaderFrame)frame;
- if (hframe.endHeaders()) {
- Log.logTrace("handling response (streamid={0})", streamid);
- handleResponse();
- if (hframe.getFlag(HeaderFrame.END_STREAM)) {
- receiveDataFrame(new DataFrame(streamid, DataFrame.END_STREAM, List.of()));
- }
- }
- } else if (frame instanceof DataFrame) {
- receiveDataFrame((DataFrame)frame);
- } else {
- otherFrame(frame);
- }
- }
-
- void otherFrame(Http2Frame frame) throws IOException {
- switch (frame.type()) {
- case WindowUpdateFrame.TYPE:
- incoming_windowUpdate((WindowUpdateFrame) frame);
- break;
- case ResetFrame.TYPE:
- incoming_reset((ResetFrame) frame);
- break;
- case PriorityFrame.TYPE:
- incoming_priority((PriorityFrame) frame);
- break;
- default:
- String msg = "Unexpected frame: " + frame.toString();
- throw new IOException(msg);
- }
- }
-
- // The Hpack decoder decodes into one of these consumers of name,value pairs
-
- DecodingCallback rspHeadersConsumer() {
- return rspHeadersConsumer;
- }
-
- protected void handleResponse() throws IOException {
- responseCode = (int)responseHeaders
- .firstValueAsLong(":status")
- .orElseThrow(() -> new IOException("no statuscode in response"));
-
- response = new Response(
- request, exchange, responseHeaders,
- responseCode, HttpClient.Version.HTTP_2);
-
- /* TODO: review if needs to be removed
- the value is not used, but in case `content-length` doesn't parse as
- long, there will be NumberFormatException. If left as is, make sure
- code up the stack handles NFE correctly. */
- responseHeaders.firstValueAsLong("content-length");
-
- if (Log.headers()) {
- StringBuilder sb = new StringBuilder("RESPONSE HEADERS:\n");
- Log.dumpHeaders(sb, " ", responseHeaders);
- Log.logHeaders(sb.toString());
- }
-
- completeResponse(response);
- }
-
- void incoming_reset(ResetFrame frame) {
- Log.logTrace("Received RST_STREAM on stream {0}", streamid);
- if (endStreamReceived()) {
- Log.logTrace("Ignoring RST_STREAM frame received on remotely closed stream {0}", streamid);
- } else if (closed) {
- Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
- } else {
- // put it in the input queue in order to read all
- // pending data frames first. Indeed, a server may send
- // RST_STREAM after sending END_STREAM, in which case we should
- // ignore it. However, we won't know if we have received END_STREAM
- // or not until all pending data frames are read.
- receiveResetFrame(frame);
- // RST_STREAM was pushed to the queue. It will be handled by
- // asyncReceive after all pending data frames have been
- // processed.
- Log.logTrace("RST_STREAM pushed in queue for stream {0}", streamid);
- }
- }
-
- void handleReset(ResetFrame frame) {
- Log.logTrace("Handling RST_STREAM on stream {0}", streamid);
- if (!closed) {
- close();
- int error = frame.getErrorCode();
- completeResponseExceptionally(new IOException(ErrorFrame.stringForCode(error)));
- } else {
- Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
- }
- }
-
- void incoming_priority(PriorityFrame frame) {
- // TODO: implement priority
- throw new UnsupportedOperationException("Not implemented");
- }
-
- private void incoming_windowUpdate(WindowUpdateFrame frame)
- throws IOException
- {
- int amount = frame.getUpdate();
- if (amount <= 0) {
- Log.logTrace("Resetting stream: {0} %d, Window Update amount: %d\n",
- streamid, streamid, amount);
- connection.resetStream(streamid, ResetFrame.FLOW_CONTROL_ERROR);
- } else {
- assert streamid != 0;
- boolean success = windowController.increaseStreamWindow(amount, streamid);
- if (!success) { // overflow
- connection.resetStream(streamid, ResetFrame.FLOW_CONTROL_ERROR);
- }
- }
- }
-
- void incoming_pushPromise(HttpRequestImpl pushRequest,
- PushedStream<T> pushStream)
- throws IOException
- {
- if (Log.requests()) {
- Log.logRequest("PUSH_PROMISE: " + pushRequest.toString());
- }
- PushGroup<T> pushGroup = exchange.getPushGroup();
- if (pushGroup == null) {
- Log.logTrace("Rejecting push promise stream " + streamid);
- connection.resetStream(pushStream.streamid, ResetFrame.REFUSED_STREAM);
- pushStream.close();
- return;
- }
-
- PushGroup.Acceptor<T> acceptor = pushGroup.acceptPushRequest(pushRequest);
-
- if (!acceptor.accepted()) {
- // cancel / reject
- IOException ex = new IOException("Stream " + streamid + " cancelled by users handler");
- if (Log.trace()) {
- Log.logTrace("No body subscriber for {0}: {1}", pushRequest,
- ex.getMessage());
- }
- pushStream.cancelImpl(ex);
- return;
- }
-
- CompletableFuture<HttpResponse<T>> pushResponseCF = acceptor.cf();
- HttpResponse.BodyHandler<T> pushHandler = acceptor.bodyHandler();
- assert pushHandler != null;
-
- pushStream.requestSent();
- pushStream.setPushHandler(pushHandler); // TODO: could wrap the handler to throw on acceptPushPromise ?
- // setup housekeeping for when the push is received
- // TODO: deal with ignoring of CF anti-pattern
- CompletableFuture<HttpResponse<T>> cf = pushStream.responseCF();
- cf.whenComplete((HttpResponse<T> resp, Throwable t) -> {
- t = Utils.getCompletionCause(t);
- if (Log.trace()) {
- Log.logTrace("Push completed on stream {0} for {1}{2}",
- pushStream.streamid, resp,
- ((t==null) ? "": " with exception " + t));
- }
- if (t != null) {
- pushGroup.pushError(t);
- pushResponseCF.completeExceptionally(t);
- } else {
- pushResponseCF.complete(resp);
- }
- pushGroup.pushCompleted();
- });
-
- }
-
- private OutgoingHeaders<Stream<T>> headerFrame(long contentLength) {
- HttpHeadersImpl h = request.getSystemHeaders();
- if (contentLength > 0) {
- h.setHeader("content-length", Long.toString(contentLength));
- }
- setPseudoHeaderFields();
- HttpHeaders sysh = filter(h);
- HttpHeaders userh = filter(request.getUserHeaders());
- OutgoingHeaders<Stream<T>> f = new OutgoingHeaders<>(sysh, userh, this);
- if (contentLength == 0) {
- f.setFlag(HeadersFrame.END_STREAM);
- endStreamSent = true;
- }
- return f;
- }
-
- private boolean hasProxyAuthorization(HttpHeaders headers) {
- return headers.firstValue("proxy-authorization")
- .isPresent();
- }
-
- // Determines whether we need to build a new HttpHeader object.
- //
- // Ideally we should pass the filter to OutgoingHeaders refactor the
- // code that creates the HeaderFrame to honor the filter.
- // We're not there yet - so depending on the filter we need to
- // apply and the content of the header we will try to determine
- // whether anything might need to be filtered.
- // If nothing needs filtering then we can just use the
- // original headers.
- private boolean needsFiltering(HttpHeaders headers,
- BiPredicate<String, List<String>> filter) {
- if (filter == Utils.PROXY_TUNNEL_FILTER || filter == Utils.PROXY_FILTER) {
- // we're either connecting or proxying
- // slight optimization: we only need to filter out
- // disabled schemes, so if there are none just
- // pass through.
- return Utils.proxyHasDisabledSchemes(filter == Utils.PROXY_TUNNEL_FILTER)
- && hasProxyAuthorization(headers);
- } else {
- // we're talking to a server, either directly or through
- // a tunnel.
- // Slight optimization: we only need to filter out
- // proxy authorization headers, so if there are none just
- // pass through.
- return hasProxyAuthorization(headers);
- }
- }
-
- private HttpHeaders filter(HttpHeaders headers) {
- HttpConnection conn = connection();
- BiPredicate<String, List<String>> filter =
- conn.headerFilter(request);
- if (needsFiltering(headers, filter)) {
- return ImmutableHeaders.of(headers.map(), filter);
- }
- return headers;
- }
-
- private void setPseudoHeaderFields() {
- HttpHeadersImpl hdrs = requestPseudoHeaders;
- String method = request.method();
- hdrs.setHeader(":method", method);
- URI uri = request.uri();
- hdrs.setHeader(":scheme", uri.getScheme());
- // TODO: userinfo deprecated. Needs to be removed
- hdrs.setHeader(":authority", uri.getAuthority());
- // TODO: ensure header names beginning with : not in user headers
- String query = uri.getQuery();
- String path = uri.getPath();
- if (path == null || path.isEmpty()) {
- if (method.equalsIgnoreCase("OPTIONS")) {
- path = "*";
- } else {
- path = "/";
- }
- }
- if (query != null) {
- path += "?" + query;
- }
- hdrs.setHeader(":path", path);
- }
-
- HttpHeadersImpl getRequestPseudoHeaders() {
- return requestPseudoHeaders;
- }
-
- /** Sets endStreamReceived. Should be called only once. */
- void setEndStreamReceived() {
- assert remotelyClosed == false: "Unexpected endStream already set";
- remotelyClosed = true;
- responseReceived();
- }
-
- /** Tells whether, or not, the END_STREAM Flag has been seen in any frame
- * received on this stream. */
- private boolean endStreamReceived() {
- return remotelyClosed;
- }
-
- @Override
- CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
- debug.log(Level.DEBUG, "sendHeadersOnly()");
- if (Log.requests() && request != null) {
- Log.logRequest(request.toString());
- }
- if (requestPublisher != null) {
- requestContentLen = requestPublisher.contentLength();
- } else {
- requestContentLen = 0;
- }
- OutgoingHeaders<Stream<T>> f = headerFrame(requestContentLen);
- connection.sendFrame(f);
- CompletableFuture<ExchangeImpl<T>> cf = new MinimalFuture<>();
- cf.complete(this); // #### good enough for now
- return cf;
- }
-
- @Override
- void released() {
- if (streamid > 0) {
- debug.log(Level.DEBUG, "Released stream %d", streamid);
- // remove this stream from the Http2Connection map.
- connection.closeStream(streamid);
- } else {
- debug.log(Level.DEBUG, "Can't release stream %d", streamid);
- }
- }
-
- @Override
- void completed() {
- // There should be nothing to do here: the stream should have
- // been already closed (or will be closed shortly after).
- }
-
- void registerStream(int id) {
- this.streamid = id;
- connection.putStream(this, streamid);
- debug.log(Level.DEBUG, "Registered stream %d", id);
- }
-
- void signalWindowUpdate() {
- RequestSubscriber subscriber = requestSubscriber;
- assert subscriber != null;
- debug.log(Level.DEBUG, "Signalling window update");
- subscriber.sendScheduler.runOrSchedule();
- }
-
- static final ByteBuffer COMPLETED = ByteBuffer.allocate(0);
- class RequestSubscriber implements Flow.Subscriber<ByteBuffer> {
- // can be < 0 if the actual length is not known.
- private final long contentLength;
- private volatile long remainingContentLength;
- private volatile Subscription subscription;
-
- // Holds the outgoing data. There will be at most 2 outgoing ByteBuffers.
- // 1) The data that was published by the request body Publisher, and
- // 2) the COMPLETED sentinel, since onComplete can be invoked without demand.
- final ConcurrentLinkedDeque<ByteBuffer> outgoing = new ConcurrentLinkedDeque<>();
-
- private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
- // A scheduler used to honor window updates. Writing must be paused
- // when the window is exhausted, and resumed when the window acquires
- // some space. The sendScheduler makes it possible to implement this
- // behaviour in an asynchronous non-blocking way.
- // See RequestSubscriber::trySend below.
- final SequentialScheduler sendScheduler;
-
- RequestSubscriber(long contentLen) {
- this.contentLength = contentLen;
- this.remainingContentLength = contentLen;
- this.sendScheduler =
- SequentialScheduler.synchronizedScheduler(this::trySend);
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- if (this.subscription != null) {
- throw new IllegalStateException("already subscribed");
- }
- this.subscription = subscription;
- debug.log(Level.DEBUG, "RequestSubscriber: onSubscribe, request 1");
- subscription.request(1);
- }
-
- @Override
- public void onNext(ByteBuffer item) {
- debug.log(Level.DEBUG, "RequestSubscriber: onNext(%d)", item.remaining());
- int size = outgoing.size();
- assert size == 0 : "non-zero size: " + size;
- onNextImpl(item);
- }
-
- private void onNextImpl(ByteBuffer item) {
- // Got some more request body bytes to send.
- if (requestBodyCF.isDone()) {
- // stream already cancelled, probably in timeout
- sendScheduler.stop();
- subscription.cancel();
- return;
- }
- outgoing.add(item);
- sendScheduler.runOrSchedule();
- }
-
- @Override
- public void onError(Throwable throwable) {
- debug.log(Level.DEBUG, () -> "RequestSubscriber: onError: " + throwable);
- // ensure that errors are handled within the flow.
- if (errorRef.compareAndSet(null, throwable)) {
- sendScheduler.runOrSchedule();
- }
- }
-
- @Override
- public void onComplete() {
- debug.log(Level.DEBUG, "RequestSubscriber: onComplete");
- int size = outgoing.size();
- assert size == 0 || size == 1 : "non-zero or one size: " + size;
- // last byte of request body has been obtained.
- // ensure that everything is completed within the flow.
- onNextImpl(COMPLETED);
- }
-
- // Attempts to send the data, if any.
- // Handles errors and completion state.
- // Pause writing if the send window is exhausted, resume it if the
- // send window has some bytes that can be acquired.
- void trySend() {
- try {
- // handle errors raised by onError;
- Throwable t = errorRef.get();
- if (t != null) {
- sendScheduler.stop();
- if (requestBodyCF.isDone()) return;
- subscription.cancel();
- requestBodyCF.completeExceptionally(t);
- return;
- }
-
- do {
- // handle COMPLETED;
- ByteBuffer item = outgoing.peekFirst();
- if (item == null) return;
- else if (item == COMPLETED) {
- sendScheduler.stop();
- complete();
- return;
- }
-
- // handle bytes to send downstream
- while (item.hasRemaining()) {
- debug.log(Level.DEBUG, "trySend: %d", item.remaining());
- assert !endStreamSent : "internal error, send data after END_STREAM flag";
- DataFrame df = getDataFrame(item);
- if (df == null) {
- debug.log(Level.DEBUG, "trySend: can't send yet: %d",
- item.remaining());
- return; // the send window is exhausted: come back later
- }
-
- if (contentLength > 0) {
- remainingContentLength -= df.getDataLength();
- if (remainingContentLength < 0) {
- String msg = connection().getConnectionFlow()
- + " stream=" + streamid + " "
- + "[" + Thread.currentThread().getName() + "] "
- + "Too many bytes in request body. Expected: "
- + contentLength + ", got: "
- + (contentLength - remainingContentLength);
- connection.resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
- throw new IOException(msg);
- } else if (remainingContentLength == 0) {
- df.setFlag(DataFrame.END_STREAM);
- endStreamSent = true;
- }
- }
- debug.log(Level.DEBUG, "trySend: sending: %d", df.getDataLength());
- connection.sendDataFrame(df);
- }
- assert !item.hasRemaining();
- ByteBuffer b = outgoing.removeFirst();
- assert b == item;
- } while (outgoing.peekFirst() != null);
-
- debug.log(Level.DEBUG, "trySend: request 1");
- subscription.request(1);
- } catch (Throwable ex) {
- debug.log(Level.DEBUG, "trySend: ", ex);
- sendScheduler.stop();
- subscription.cancel();
- requestBodyCF.completeExceptionally(ex);
- }
- }
-
- private void complete() throws IOException {
- long remaining = remainingContentLength;
- long written = contentLength - remaining;
- if (remaining > 0) {
- connection.resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
- // let trySend() handle the exception
- throw new IOException(connection().getConnectionFlow()
- + " stream=" + streamid + " "
- + "[" + Thread.currentThread().getName() +"] "
- + "Too few bytes returned by the publisher ("
- + written + "/"
- + contentLength + ")");
- }
- if (!endStreamSent) {
- endStreamSent = true;
- connection.sendDataFrame(getEmptyEndStreamDataFrame());
- }
- requestBodyCF.complete(null);
- }
- }
-
- /**
- * Send a RESET frame to tell server to stop sending data on this stream
- */
- @Override
- public CompletableFuture<Void> ignoreBody() {
- try {
- connection.resetStream(streamid, ResetFrame.STREAM_CLOSED);
- return MinimalFuture.completedFuture(null);
- } catch (Throwable e) {
- Log.logTrace("Error resetting stream {0}", e.toString());
- return MinimalFuture.failedFuture(e);
- }
- }
-
- DataFrame getDataFrame(ByteBuffer buffer) {
- int requestAmount = Math.min(connection.getMaxSendFrameSize(), buffer.remaining());
- // blocks waiting for stream send window, if exhausted
- int actualAmount = windowController.tryAcquire(requestAmount, streamid, this);
- if (actualAmount <= 0) return null;
- ByteBuffer outBuf = Utils.sliceWithLimitedCapacity(buffer, actualAmount);
- DataFrame df = new DataFrame(streamid, 0 , outBuf);
- return df;
- }
-
- private DataFrame getEmptyEndStreamDataFrame() {
- return new DataFrame(streamid, DataFrame.END_STREAM, List.of());
- }
-
- /**
- * A List of responses relating to this stream. Normally there is only
- * one response, but intermediate responses like 100 are allowed
- * and must be passed up to higher level before continuing. Deals with races
- * such as if responses are returned before the CFs get created by
- * getResponseAsync()
- */
-
- final List<CompletableFuture<Response>> response_cfs = new ArrayList<>(5);
-
- @Override
- CompletableFuture<Response> getResponseAsync(Executor executor) {
- CompletableFuture<Response> cf;
- // The code below deals with race condition that can be caused when
- // completeResponse() is being called before getResponseAsync()
- synchronized (response_cfs) {
- if (!response_cfs.isEmpty()) {
- // This CompletableFuture was created by completeResponse().
- // it will be already completed.
- cf = response_cfs.remove(0);
- // if we find a cf here it should be already completed.
- // finding a non completed cf should not happen. just assert it.
- assert cf.isDone() : "Removing uncompleted response: could cause code to hang!";
- } else {
- // getResponseAsync() is called first. Create a CompletableFuture
- // that will be completed by completeResponse() when
- // completeResponse() is called.
- cf = new MinimalFuture<>();
- response_cfs.add(cf);
- }
- }
- if (executor != null && !cf.isDone()) {
- // protect from executing later chain of CompletableFuture operations from SelectorManager thread
- cf = cf.thenApplyAsync(r -> r, executor);
- }
- Log.logTrace("Response future (stream={0}) is: {1}", streamid, cf);
- PushGroup<?> pg = exchange.getPushGroup();
- if (pg != null) {
- // if an error occurs make sure it is recorded in the PushGroup
- cf = cf.whenComplete((t,e) -> pg.pushError(Utils.getCompletionCause(e)));
- }
- return cf;
- }
-
- /**
- * Completes the first uncompleted CF on list, and removes it. If there is no
- * uncompleted CF then creates one (completes it) and adds to list
- */
- void completeResponse(Response resp) {
- synchronized (response_cfs) {
- CompletableFuture<Response> cf;
- int cfs_len = response_cfs.size();
- for (int i=0; i<cfs_len; i++) {
- cf = response_cfs.get(i);
- if (!cf.isDone()) {
- Log.logTrace("Completing response (streamid={0}): {1}",
- streamid, cf);
- cf.complete(resp);
- response_cfs.remove(cf);
- return;
- } // else we found the previous response: just leave it alone.
- }
- cf = MinimalFuture.completedFuture(resp);
- Log.logTrace("Created completed future (streamid={0}): {1}",
- streamid, cf);
- response_cfs.add(cf);
- }
- }
-
- // methods to update state and remove stream when finished
-
- synchronized void requestSent() {
- requestSent = true;
- if (responseReceived) {
- close();
- }
- }
-
- synchronized void responseReceived() {
- responseReceived = true;
- if (requestSent) {
- close();
- }
- }
-
- /**
- * same as above but for errors
- */
- void completeResponseExceptionally(Throwable t) {
- synchronized (response_cfs) {
- // use index to avoid ConcurrentModificationException
- // caused by removing the CF from within the loop.
- for (int i = 0; i < response_cfs.size(); i++) {
- CompletableFuture<Response> cf = response_cfs.get(i);
- if (!cf.isDone()) {
- cf.completeExceptionally(t);
- response_cfs.remove(i);
- return;
- }
- }
- response_cfs.add(MinimalFuture.failedFuture(t));
- }
- }
-
- CompletableFuture<Void> sendBodyImpl() {
- requestBodyCF.whenComplete((v, t) -> requestSent());
- if (requestPublisher != null) {
- final RequestSubscriber subscriber = new RequestSubscriber(requestContentLen);
- requestPublisher.subscribe(requestSubscriber = subscriber);
- } else {
- // there is no request body, therefore the request is complete,
- // END_STREAM has already sent with outgoing headers
- requestBodyCF.complete(null);
- }
- return requestBodyCF;
- }
-
- @Override
- void cancel() {
- cancel(new IOException("Stream " + streamid + " cancelled"));
- }
-
- @Override
- void cancel(IOException cause) {
- cancelImpl(cause);
- }
-
- // This method sends a RST_STREAM frame
- void cancelImpl(Throwable e) {
- debug.log(Level.DEBUG, "cancelling stream {0}: {1}", streamid, e);
- if (Log.trace()) {
- Log.logTrace("cancelling stream {0}: {1}\n", streamid, e);
- }
- boolean closing;
- if (closing = !closed) { // assigning closing to !closed
- synchronized (this) {
- failed = e;
- if (closing = !closed) { // assigning closing to !closed
- closed=true;
- }
- }
- }
- if (closing) { // true if the stream has not been closed yet
- if (responseSubscriber != null)
- sched.runOrSchedule();
- }
- completeResponseExceptionally(e);
- if (!requestBodyCF.isDone()) {
- requestBodyCF.completeExceptionally(e); // we may be sending the body..
- }
- if (responseBodyCF != null) {
- responseBodyCF.completeExceptionally(e);
- }
- try {
- // will send a RST_STREAM frame
- if (streamid != 0) {
- connection.resetStream(streamid, ResetFrame.CANCEL);
- }
- } catch (IOException ex) {
- Log.logError(ex);
- }
- }
-
- // This method doesn't send any frame
- void close() {
- if (closed) return;
- synchronized(this) {
- if (closed) return;
- closed = true;
- }
- Log.logTrace("Closing stream {0}", streamid);
- connection.closeStream(streamid);
- Log.logTrace("Stream {0} closed", streamid);
- }
-
- static class PushedStream<T> extends Stream<T> {
- final PushGroup<T> pushGroup;
- // push streams need the response CF allocated up front as it is
- // given directly to user via the multi handler callback function.
- final CompletableFuture<Response> pushCF;
- CompletableFuture<HttpResponse<T>> responseCF;
- final HttpRequestImpl pushReq;
- HttpResponse.BodyHandler<T> pushHandler;
-
- PushedStream(PushGroup<T> pushGroup,
- Http2Connection connection,
- Exchange<T> pushReq) {
- // ## no request body possible, null window controller
- super(connection, pushReq, null);
- this.pushGroup = pushGroup;
- this.pushReq = pushReq.request();
- this.pushCF = new MinimalFuture<>();
- this.responseCF = new MinimalFuture<>();
-
- }
-
- CompletableFuture<HttpResponse<T>> responseCF() {
- return responseCF;
- }
-
- synchronized void setPushHandler(HttpResponse.BodyHandler<T> pushHandler) {
- this.pushHandler = pushHandler;
- }
-
- synchronized HttpResponse.BodyHandler<T> getPushHandler() {
- // ignored parameters to function can be used as BodyHandler
- return this.pushHandler;
- }
-
- // Following methods call the super class but in case of
- // error record it in the PushGroup. The error method is called
- // with a null value when no error occurred (is a no-op)
- @Override
- CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
- return super.sendBodyAsync()
- .whenComplete((ExchangeImpl<T> v, Throwable t)
- -> pushGroup.pushError(Utils.getCompletionCause(t)));
- }
-
- @Override
- CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
- return super.sendHeadersAsync()
- .whenComplete((ExchangeImpl<T> ex, Throwable t)
- -> pushGroup.pushError(Utils.getCompletionCause(t)));
- }
-
- @Override
- CompletableFuture<Response> getResponseAsync(Executor executor) {
- CompletableFuture<Response> cf = pushCF.whenComplete(
- (v, t) -> pushGroup.pushError(Utils.getCompletionCause(t)));
- if(executor!=null && !cf.isDone()) {
- cf = cf.thenApplyAsync( r -> r, executor);
- }
- return cf;
- }
-
- @Override
- CompletableFuture<T> readBodyAsync(
- HttpResponse.BodyHandler<T> handler,
- boolean returnConnectionToPool,
- Executor executor)
- {
- return super.readBodyAsync(handler, returnConnectionToPool, executor)
- .whenComplete((v, t) -> pushGroup.pushError(t));
- }
-
- @Override
- void completeResponse(Response r) {
- Log.logResponse(r::toString);
- pushCF.complete(r); // not strictly required for push API
- // start reading the body using the obtained BodySubscriber
- CompletableFuture<Void> start = new MinimalFuture<>();
- start.thenCompose( v -> readBodyAsync(getPushHandler(), false, getExchange().executor()))
- .whenComplete((T body, Throwable t) -> {
- if (t != null) {
- responseCF.completeExceptionally(t);
- } else {
- HttpResponseImpl<T> resp =
- new HttpResponseImpl<>(r.request, r, null, body, getExchange());
- responseCF.complete(resp);
- }
- });
- start.completeAsync(() -> null, getExchange().executor());
- }
-
- @Override
- void completeResponseExceptionally(Throwable t) {
- pushCF.completeExceptionally(t);
- }
-
-// @Override
-// synchronized void responseReceived() {
-// super.responseReceived();
-// }
-
- // create and return the PushResponseImpl
- @Override
- protected void handleResponse() {
- responseCode = (int)responseHeaders
- .firstValueAsLong(":status")
- .orElse(-1);
-
- if (responseCode == -1) {
- completeResponseExceptionally(new IOException("No status code"));
- }
-
- this.response = new Response(
- pushReq, exchange, responseHeaders,
- responseCode, HttpClient.Version.HTTP_2);
-
- /* TODO: review if needs to be removed
- the value is not used, but in case `content-length` doesn't parse
- as long, there will be NumberFormatException. If left as is, make
- sure code up the stack handles NFE correctly. */
- responseHeaders.firstValueAsLong("content-length");
-
- if (Log.headers()) {
- StringBuilder sb = new StringBuilder("RESPONSE HEADERS");
- sb.append(" (streamid=").append(streamid).append("): ");
- Log.dumpHeaders(sb, " ", responseHeaders);
- Log.logHeaders(sb.toString());
- }
-
- // different implementations for normal streams and pushed streams
- completeResponse(response);
- }
- }
-
- final class StreamWindowUpdateSender extends WindowUpdateSender {
-
- StreamWindowUpdateSender(Http2Connection connection) {
- super(connection);
- }
-
- @Override
- int getStreamId() {
- return streamid;
- }
- }
-
- /**
- * Returns true if this exchange was canceled.
- * @return true if this exchange was canceled.
- */
- synchronized boolean isCanceled() {
- return failed != null;
- }
-
- /**
- * Returns the cause for which this exchange was canceled, if available.
- * @return the cause for which this exchange was canceled, if available.
- */
- synchronized Throwable getCancelCause() {
- return failed;
- }
-
- final String dbgString() {
- return connection.dbgString() + "/Stream("+streamid+")";
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/TimeoutEvent.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,80 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.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 + "]";
- }
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WindowController.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,320 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, 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.incubator.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.incubator.http.internal.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 boolean DEBUG = Utils.DEBUG; // Revisit: temporary developer's flag
- static final System.Logger DEBUG_LOGGER =
- Utils.getDebugLogger("WindowController"::toString, 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 % 2) == 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
- DEBUG_LOGGER.log(Level.DEBUG,
- "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;
- DEBUG_LOGGER.log(Level.DEBUG,
- "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;
- DEBUG_LOGGER.log(Level.DEBUG, "Connection window size is now %d", size);
-
- // 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)
- throw new InternalError("Expected entry for streamid: " + streamid);
- size += amount;
- if (size < 0)
- return false;
- streams.put(streamid, size);
- DEBUG_LOGGER.log(Level.DEBUG,
- "Stream %s window size is now %s", streamid, size);
-
- 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);
- DEBUG_LOGGER.log(Level.DEBUG,
- "Stream %s window size is now %s", streamid, size);
- }
- }
- } 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();
-// }
-// }
-
-}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WindowUpdateSender.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,90 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.incubator.http;
-
-import java.lang.System.Logger.Level;
-import jdk.incubator.http.internal.frame.SettingsFrame;
-import jdk.incubator.http.internal.frame.WindowUpdateFrame;
-import jdk.incubator.http.internal.common.Utils;
-
-import java.util.concurrent.atomic.AtomicInteger;
-
-abstract class WindowUpdateSender {
-
- final static boolean DEBUG = Utils.DEBUG;
- final System.Logger debug =
- Utils.getDebugLogger(this::dbgString, 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);
- debug.log(Level.DEBUG, "maxFrameSize=%d, initWindowSize=%d, limit=%d",
- maxFrameSize, initWindowSize, limit);
- }
-
- abstract int getStreamId();
-
- void update(int delta) {
- debug.log(Level.DEBUG, "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) {
- debug.log(Level.DEBUG, "sending window update: %d", delta);
- connection.sendUnorderedFrame(new WindowUpdateFrame(getStreamId(), delta));
- }
-
- String dbgString() {
- return "WindowUpdateSender(stream: " + getStreamId() + ")";
- }
-
-}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/AbstractAsyncSSLConnection.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,188 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+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.SSLEngineResult;
+import javax.net.ssl.SSLParameters;
+
+import jdk.incubator.http.internal.common.SSLTube;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.Utils;
+
+
+/**
+ * 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;
+
+ AbstractAsyncSSLConnection(InetSocketAddress addr,
+ HttpClientImpl client,
+ String serverName,
+ String[] alpn) {
+ super(addr, client);
+ this.serverName = serverName;
+ SSLContext context = client.theSSLContext();
+ sslParameters = createSSLParameters(client, serverName, alpn);
+ Log.logParams(sslParameters);
+ engine = createEngine(context, 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,
+ String serverName,
+ String[] alpn) {
+ SSLParameters sslp = client.sslParameters();
+ SSLParameters sslParameters = Utils.copySSLParameters(sslp);
+ 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 != null) {
+ sslParameters.setServerNames(List.of(new SNIHostName(serverName)));
+ }
+ return sslParameters;
+ }
+
+ private static SSLEngine createEngine(SSLContext context,
+ SSLParameters sslParameters) {
+ SSLEngine engine = context.createSSLEngine();
+ engine.setUseClientMode(true);
+ engine.setSSLParameters(sslParameters);
+ return engine;
+ }
+
+ @Override
+ final boolean isSecure() {
+ return true;
+ }
+
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ static final class SSLConnectionChannel extends DetachedConnectionChannel {
+ final DetachedConnectionChannel delegate;
+ final SSLDelegate sslDelegate;
+ SSLConnectionChannel(DetachedConnectionChannel delegate, SSLDelegate sslDelegate) {
+ this.delegate = delegate;
+ this.sslDelegate = sslDelegate;
+ }
+
+ SocketChannel channel() {
+ return delegate.channel();
+ }
+
+ @Override
+ ByteBuffer read() throws IOException {
+ SSLDelegate.WrapperResult r = sslDelegate.recvData(ByteBuffer.allocate(8192));
+ // TODO: check for closure
+ int n = r.result.bytesProduced();
+ if (n > 0) {
+ return r.buf;
+ } else if (n == 0) {
+ return Utils.EMPTY_BYTEBUFFER;
+ } else {
+ return null;
+ }
+ }
+ @Override
+ long write(ByteBuffer[] buffers, int start, int number) throws IOException {
+ long l = SSLDelegate.countBytes(buffers, start, number);
+ SSLDelegate.WrapperResult r = sslDelegate.sendData(buffers, start, number);
+ if (r.result.getStatus() == SSLEngineResult.Status.CLOSED) {
+ if (l > 0) {
+ throw new IOException("SSLHttpConnection closed");
+ }
+ }
+ return l;
+ }
+ @Override
+ public void shutdownInput() throws IOException {
+ delegate.shutdownInput();
+ }
+ @Override
+ public void shutdownOutput() throws IOException {
+ delegate.shutdownOutput();
+ }
+ @Override
+ public void close() {
+ delegate.close();
+ }
+ }
+
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ @Override
+ DetachedConnectionChannel detachChannel() {
+ assert client() != null;
+ DetachedConnectionChannel detachedChannel = plainConnection().detachChannel();
+ SSLDelegate sslDelegate = new SSLDelegate(engine,
+ detachedChannel.channel());
+ return new SSLConnectionChannel(detachedChannel, sslDelegate);
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/AbstractSubscription.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,45 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.util.concurrent.Flow;
+import jdk.incubator.http.internal.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; }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/AsyncEvent.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+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;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/AsyncSSLConnection.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,115 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.net.InetSocketAddress;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.CompletableFuture;
+import jdk.incubator.http.internal.common.SSLTube;
+import jdk.incubator.http.internal.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), 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
+ void shutdownInput() throws IOException {
+ debug.log(Level.DEBUG, "plainConnection.channel().shutdownInput()");
+ plainConnection.channel().shutdownInput();
+ }
+
+ @Override
+ void shutdownOutput() throws IOException {
+ debug.log(Level.DEBUG, "plainConnection.channel().shutdownOutput()");
+ plainConnection.channel().shutdownOutput();
+ }
+
+ @Override
+ SSLTube getConnectionFlow() {
+ return flow;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/AsyncSSLTunnelConnection.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,129 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.net.InetSocketAddress;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.CompletableFuture;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.internal.common.SSLTube;
+import jdk.incubator.http.internal.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), alpn);
+ this.plainConnection = new PlainTunnelingConnection(addr, proxy, client, proxyHeaders);
+ this.writePublisher = new PlainHttpPublisher();
+ }
+
+ @Override
+ public CompletableFuture<Void> connectAsync() {
+ debug.log(Level.DEBUG, "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 -> {
+ debug.log(Level.DEBUG, "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
+ void shutdownInput() throws IOException {
+ plainConnection.channel().shutdownInput();
+ }
+
+ @Override
+ void shutdownOutput() throws IOException {
+ plainConnection.channel().shutdownOutput();
+ }
+
+ @Override
+ SocketChannel channel() {
+ return plainConnection.channel();
+ }
+
+ @Override
+ boolean isProxied() {
+ return true;
+ }
+
+ @Override
+ SSLTube getConnectionFlow() {
+ return flow;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/AsyncTriggerEvent.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+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; }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/AuthenticationFilter.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,398 @@
+/*
+ * 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.incubator.http.internal;
+
+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 jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.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);
+
+ // needs to be instance method in Authenticator
+ return auth.requestPasswordAuthenticationInstance(uri.getHost(),
+ null,
+ uri.getPort(),
+ uri.getScheme(),
+ 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,
+ 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;
+ cache.store(au.scheme, req.uri(), false, 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;
+ }
+ 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;
+ }
+ addBasicCredentials(req, proxy, au.credentials);
+ au.retries++;
+ return req;
+ }
+ }
+
+ // Use a WeakHashMap to make it possible for the HttpClient to
+ // be garbaged 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 class Cache {
+ final LinkedList<CacheEntry> entries = new LinkedList<>();
+
+ 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 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 = uri.resolve(".").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 = uri.toString();
+ return other.startsWith(root);
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/ConnectionPool.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,490 @@
+/*
+ * 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.incubator.http.internal;
+
+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.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.Utils;
+
+/**
+ * Http 1.1 connection pool.
+ */
+final class ConnectionPool {
+
+ static final long KEEP_ALIVE = Utils.getIntegerNetProperty(
+ "jdk.httpclient.keepalive.timeout", 1200); // seconds
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, 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();
+ debug.log(Level.DEBUG, "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) {
+ debug.log(Level.DEBUG,
+ "%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() + ")";
+ }
+
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/CookieFilter.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+import java.io.IOException;
+import java.net.CookieHandler;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.internal.common.HttpHeadersImpl;
+import jdk.incubator.http.internal.common.Log;
+
+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 (String hdrname : cookies.keySet()) {
+ List<String> vals = cookies.get(hdrname);
+ for (String val : vals) {
+ 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;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/Exchange.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,574 @@
+/*
+ * 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.incubator.http.internal;
+
+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 jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.HttpTimeoutException;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import jdk.incubator.http.internal.common.Utils;
+import jdk.incubator.http.internal.common.Log;
+
+import static jdk.incubator.http.internal.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> {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, 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
+ debug.log(Level.DEBUG, "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.
+ debug.log(Level.DEBUG, "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.isLoggable(Level.DEBUG)) {
+ debug.log(Level.DEBUG,
+ "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;
+ Response syntheticResponse = new Response(request, this,
+ proxyResponse.headers, 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 tunelling through HTTP/1.1 proxy
+ // or by sendHeaderAsync (case of HTTP/1.1 SSL tunelling 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(int status, HttpHeaders hdrs) {
+ return HttpResponse.BodySubscriber.discard((T)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?
+ debug.log(Level.DEBUG, "Upgrading async %s", e.connection());
+ return e.readBodyAsync(this::ignoreBody, false, parentExecutor)
+ .thenCompose((T v) -> {// v is null
+ debug.log(Level.DEBUG, "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);
+ }
+ debug.log(Level.DEBUG, "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;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/ExchangeImpl.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,211 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.function.Function;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
+import jdk.incubator.http.internal.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> {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ private static final System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("ExchangeImpl"::toString, 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) {
+ DEBUG_LOGGER.log(Level.DEBUG, "get: HTTP/1.1: new Http1Exchange");
+ return createHttp1Exchange(exchange, connection);
+ } else {
+ Http2ClientImpl c2 = exchange.client().client2(); // TODO: improve
+ HttpRequestImpl request = exchange.request();
+ CompletableFuture<Http2Connection> c2f = c2.getConnectionFor(request);
+ DEBUG_LOGGER.log(Level.DEBUG, "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)
+ {
+ DEBUG_LOGGER.log(Level.DEBUG, "handling HTTP/2 connection creation result");
+ boolean secure = exchange.request().secure();
+ if (t != null) {
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "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();
+ DEBUG_LOGGER.log(Level.DEBUG, "downgrading to HTTP/1.1 with: %s", as);
+ CompletableFuture<? extends ExchangeImpl<U>> ex =
+ createHttp1Exchange(exchange, as);
+ return ex;
+ } else {
+ DEBUG_LOGGER.log(Level.DEBUG, "HTTP/2 connection creation failed "
+ + "with unexpected exception: %s", (Object)t);
+ return MinimalFuture.failedFuture(t);
+ }
+ }
+ if (secure && c== null) {
+ DEBUG_LOGGER.log(Level.DEBUG, "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
+ DEBUG_LOGGER.log(Level.DEBUG, "new Http1Exchange, try to upgrade");
+ return createHttp1Exchange(exchange, connection)
+ .thenApply((e) -> {
+ exchange.h2Upgrade();
+ return e;
+ });
+ } else {
+ DEBUG_LOGGER.log(Level.DEBUG, "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();
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/FilterFactory.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+import java.util.LinkedList;
+import java.util.List;
+
+class FilterFactory {
+
+ final LinkedList<Class<? extends HeaderFilter>> filterClasses = new LinkedList<>();
+
+ public void addFilter(Class<? extends HeaderFilter> type) {
+ filterClasses.add(type);
+ }
+
+ List<HeaderFilter> getFilterChain() {
+ List<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;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/HeaderFilter.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+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;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/HeaderParser.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+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();
+ }
+ } 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();
+ }
+ }
+ 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;
+// }
+// }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/Http1AsyncReceiver.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,651 @@
+/*
+ * 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.incubator.http.internal;
+
+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.stream.Collectors;
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.FlowTube.TubeSubscriber;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.ConnectionExpiredException;
+import jdk.incubator.http.internal.common.Utils;
+
+
+/**
+ * A helper class that will queue up incoming data until the receiving
+ * side is ready to handle it.
+ */
+class Http1AsyncReceiver {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, 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();
+
+ }
+
+ /**
+ * 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 SequentialScheduler scheduler;
+ Http1AsyncDelegateSubscription(SequentialScheduler scheduler,
+ Runnable onCancel) {
+ this.scheduler = scheduler;
+ this.onCancel = onCancel;
+ }
+ @Override
+ public void request(long n) {
+ final Demand demand = demand();
+ if (demand.increase(n)) {
+ scheduler.runOrSchedule();
+ }
+ }
+ @Override
+ public void cancel() { 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;
+
+ 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) {
+ Http1AsyncDelegate delegate = this.delegate;
+ debug.log(Level.DEBUG, "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;
+ debug.log(Level.DEBUG, "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();
+ debug.log(Level.DEBUG, () -> {
+ // 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());
+ 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
+ debug.log(Level.DEBUG, "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 && queue.isEmpty()) {
+ // forward error only after emptying the queue.
+ final Object captured = delegate;
+ debug.log(Level.DEBUG, () -> "flushing " + x
+ + "\n\t delegate: " + captured
+ + "\t\t queue.isEmpty: " + queue.isEmpty());
+ scheduler.stop();
+ delegate.onReadError(x);
+ }
+ }
+
+ /**
+ * 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);
+ debug.log(Level.DEBUG, () -> "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();
+ debug.log(Level.DEBUG, "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);
+ Runnable cancel = () -> {
+ debug.log(Level.DEBUG, "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);
+ pending.onSubscribe(subscription);
+ this.delegate = delegate = pending;
+ final Object captured = delegate;
+ debug.log(Level.DEBUG, () -> "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() {
+ debug.log(Level.DEBUG, "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);
+ }
+ debug.log(Level.DEBUG, () ->
+ "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) {
+ debug.log(Level.DEBUG, "Unsubscribed %s", delegate);
+ this.delegate = null;
+ }
+ }
+ }
+
+ // Callback: Consumer of ByteBuffer
+ private void asyncReceive(ByteBuffer buf) {
+ debug.log(Level.DEBUG, "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;
+ debug.log(Level.DEBUG, "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);
+ debug.log(Level.DEBUG, () -> "recorded " + t
+ + "\n\t delegate: " + delegate
+ + "\t\t queue.isEmpty: " + queue.isEmpty(), ex);
+ }
+ if (queue.isEmpty() || pendingDelegateRef.get() != null) {
+ // 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() {
+ debug.log(Level.DEBUG, "stopping");
+ scheduler.stop();
+ 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) {
+ debug.log(Level.DEBUG,
+ "Http1TubeSubscriber: requesting one more from upstream");
+ s.request(1);
+ return;
+ }
+ }
+ debug.log(Level.DEBUG, "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() {
+ debug.log(Level.DEBUG, "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()]);
+ 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;
+ debug.log(Level.DEBUG,
+ "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) {
+ debug.log(Level.DEBUG,
+ "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;
+ debug.log(Level.DEBUG, () -> "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 = flowTag + " Http1AsyncReceiver";
+ } else {
+ tag = "Http1AsyncReceiver";
+ }
+ }
+ return tag;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/Http1Exchange.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,616 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.net.InetSocketAddress;
+import jdk.incubator.http.HttpResponse.BodyHandler;
+import jdk.incubator.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.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import jdk.incubator.http.internal.common.Utils;
+import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
+
+/**
+ * Encapsulates one HTTP/1.1 request/response exchange.
+ */
+class Http1Exchange<T> extends ExchangeImpl<T> {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ private static final System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("Http1Exchange"::toString, 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> {
+ protected volatile Flow.Subscription subscription;
+ protected volatile boolean complete;
+
+ /** Final sentinel in the stream of request body. */
+ static final List<ByteBuffer> COMPLETED = List.of(ByteBuffer.allocate(0));
+
+ void request(long n) {
+ DEBUG_LOGGER.log(Level.DEBUG, () ->
+ "Http1BodySubscriber requesting " + n + ", from " + subscription);
+ subscription.request(n);
+ }
+
+ static Http1BodySubscriber completeSubscriber() {
+ return new Http1BodySubscriber() {
+ @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;
+ }
+
+ public AbstractSubscription subscription() {
+ return s;
+ }
+ }
+
+ @Override
+ HttpConnection connection() {
+ return connection;
+ }
+
+ private void connectFlows(HttpConnection connection) {
+ FlowTube tube = connection.getConnectionFlow();
+ debug.log(Level.DEBUG, "%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.
+ debug.log(Level.DEBUG, "Sending headers only");
+ if (response == null) {
+ response = new Http1Response<>(connection, this, asyncReceiver);
+ }
+
+ debug.log(Level.DEBUG, "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()) {
+ debug.log(Level.DEBUG, "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);
+
+ debug.log(Level.DEBUG, "requestAction.headers");
+ List<ByteBuffer> data = requestAction.headers();
+ synchronized (lock) {
+ state = State.HEADERS;
+ }
+ debug.log(Level.DEBUG, "setting outgoing with headers");
+ assert outgoing.isEmpty() : "Unexpected outgoing:" + outgoing;
+ appendToOutgoing(data);
+ cf.complete(null);
+ return cf;
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG, "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();
+ appendToOutgoing(Http1BodySubscriber.COMPLETED);
+ } else {
+ bodySubscriber.request(1); // start
+ }
+ } catch (Throwable t) {
+ connection.close();
+ 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);
+ debug.log(Level.DEBUG,
+ () -> 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(response.responseCode(),
+ response.responseHeaders());
+ 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;
+ synchronized (lock) {
+ if (failed == null)
+ failed = 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;
+ while (!toComplete.isEmpty()) {
+ CompletableFuture<?> cf = toComplete.poll();
+ exec.execute(() -> {
+ if (cf.completeExceptionally(cause)) {
+ debug.log(Level.DEBUG, "completed cf with %s",
+ (Object) cause);
+ }
+ });
+ }
+ }
+ }
+
+ 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) {
+ debug.log(Level.DEBUG, "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();
+ }
+
+ // 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(() -> {
+ connection.close();
+ headersSentCF.completeExceptionally(dp.throwable);
+ bodySentCF.completeExceptionally(dp.throwable);
+ });
+ return dp;
+ }
+
+ switch (state) {
+ case HEADERS:
+ state = State.BODY;
+ // completeAsync, since dependent tasks should run in another thread
+ debug.log(Level.DEBUG, "initiating completion of headersSentCF");
+ headersSentCF.completeAsync(() -> this, exec);
+ break;
+ case BODY:
+ if (dp.data == Http1BodySubscriber.COMPLETED) {
+ state = State.COMPLETING;
+ debug.log(Level.DEBUG, "initiating completion of bodySentCF");
+ bodySentCF.completeAsync(() -> this, exec);
+ } else {
+ debug.log(Level.DEBUG, "requesting more body from the subscriber");
+ exec.execute(() -> bodySubscriber.request(1));
+ }
+ 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 System.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;
+ debug.log(Level.DEBUG, "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;
+ debug.log(Level.DEBUG, "WriteTask");
+ if (subscriber == null) {
+ debug.log(Level.DEBUG, "no subscriber yet");
+ return;
+ }
+ debug.log(Level.DEBUG, () -> "hasOutgoing = " + hasOutgoing());
+ while (hasOutgoing() && demand.tryDecrement()) {
+ DataPair dp = getOutgoing();
+
+ if (dp.throwable != null) {
+ debug.log(Level.DEBUG, "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;
+ }
+ debug.log(Level.DEBUG,
+ "completed, stopping %s", writeScheduler);
+ writeScheduler.stop();
+ // Do nothing more. Just do not publish anything further.
+ // The next Subscriber will eventually take over.
+
+ } else {
+ debug.log(Level.DEBUG, () ->
+ "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);
+ debug.log(Level.DEBUG,
+ "subscription request(%d), demand=%s", n, demand);
+ writeScheduler.runOrSchedule(client.theExecutor());
+ }
+
+ @Override
+ public void cancel() {
+ debug.log(Level.DEBUG, "subscription cancelled");
+ if (cancelled)
+ return; //no-op
+ cancelled = true;
+ writeScheduler.stop();
+ }
+ }
+ }
+
+ String dbgString() {
+ return "Http1Exchange";
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/Http1HeaderParser.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+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 jdk.incubator.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));
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/Http1Request.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,390 @@
+/*
+ * 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.incubator.http.internal;
+
+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 jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.internal.Http1Exchange.Http1BodySubscriber;
+import jdk.incubator.http.internal.common.HttpHeadersImpl;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.Utils;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+/**
+ * An HTTP/1.1 request.
+ */
+class Http1Request {
+ 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");
+ Log.logHeaders("REQUEST HEADERS:\n" + s);
+ }
+ }
+
+
+ private void collectHeaders0(StringBuilder sb) {
+ BiPredicate<String,List<String>> filter =
+ connection.headerFilter(request);
+
+ // 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, filter);
+
+ // If we're sending this request through a tunnel,
+ // don't send any user-supplied proxy-* headers
+ // to the target server.
+ collectHeaders1(sb, userHeaders, filter);
+ sb.append("\r\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(value).append("\r\n");
+ }
+ }
+ }
+
+ private String getPathAndQuery(URI uri) {
+ String path = uri.getPath();
+ String query = uri.getQuery();
+ 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;
+ }
+
+ class StreamSubscriber extends Http1BodySubscriber {
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ if (this.subscription != null) {
+ Throwable t = new IllegalStateException("already subscribed");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ this.subscription = 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;
+
+ subscription.cancel();
+ 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?
+
+ }
+ }
+ }
+
+ class FixedContentSubscriber extends Http1BodySubscriber {
+
+ private volatile long contentWritten;
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ if (this.subscription != null) {
+ Throwable t = new IllegalStateException("already subscribed");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ this.subscription = subscription;
+ }
+ }
+
+ @Override
+ public void onNext(ByteBuffer item) {
+ debug.log(Level.DEBUG, "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) {
+ subscription.cancel();
+ 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) {
+ debug.log(Level.DEBUG, "onError");
+ if (complete) // TODO: error?
+ return;
+
+ subscription.cancel();
+ http1Exchange.appendToOutgoing(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ debug.log(Level.DEBUG, "onComplete");
+ if (complete) {
+ Throwable t = new IllegalStateException("subscription already completed");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ complete = true;
+ long written = contentWritten;
+ if (contentLength > written) {
+ subscription.cancel();
+ 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);
+ }
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::toString, DEBUG);
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/Http1Response.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,518 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.io.EOFException;
+import java.lang.System.Logger.Level;
+import java.nio.ByteBuffer;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.internal.ResponseContent.BodyParser;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import jdk.incubator.http.internal.common.Utils;
+import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
+
+/**
+ * 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;
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this.getClass()::getSimpleName, DEBUG);
+
+
+ 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);
+ }
+
+ public CompletableFuture<Response> readHeadersAsync(Executor executor) {
+ debug.log(Level.DEBUG, () -> "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;
+ debug.log(Level.DEBUG, () ->
+ "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,
+ responseCode,
+ HTTP_1_1);
+ 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(HttpResponse.BodySubscriber.discard((Void)null), true, executor);
+ }
+ }
+
+ public <U> CompletableFuture<U> readBody(HttpResponse.BodySubscriber<U> p,
+ boolean return2Cache,
+ Executor executor) {
+ this.return2Cache = return2Cache;
+ final HttpResponse.BodySubscriber<U> pusher = p;
+ final CompletionStage<U> bodyCF = p.getBody();
+ final CompletableFuture<U> cf = MinimalFuture.of(bodyCF);
+
+ 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();
+
+ executor.execute(() -> {
+ try {
+ HttpClientImpl client = connection.client();
+ content = new ResponseContent(
+ connection, clen, headers, pusher,
+ 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.
+ client.reference();
+ bodyReader.start(content.getBodyParser(
+ (t) -> {
+ try {
+ if (t != null) {
+ pusher.onError(t);
+ connection.close();
+ if (!cf.isDone())
+ cf.completeExceptionally(t);
+ }
+ } finally {
+ // 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.
+ client.unreference();
+ 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) {
+ debug.log(Level.DEBUG, () ->
+ "Finished reading body: " + s);
+ assert s == State.READING_BODY;
+ }
+ if (t != null && !cf.isDone()) {
+ pusher.onError(t);
+ cf.completeExceptionally(t);
+ }
+ } catch (Throwable x) {
+ // not supposed to happen
+ asyncReceiver.onReadError(x);
+ }
+ });
+ connection.addTrailingOperation(trailingOp);
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG, () -> "Failed reading body: " + t);
+ try {
+ if (!cf.isDone()) {
+ pusher.onError(t);
+ cf.completeExceptionally(t);
+ }
+ } finally {
+ asyncReceiver.onReadError(t);
+ }
+ }
+ });
+ return cf;
+ }
+
+
+ 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.
+ debug.log(Level.DEBUG, () -> 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.isDone()) 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();
+ debug.log(Level.DEBUG, () -> "Sending " + b.remaining()
+ + "/" + b.capacity() + " bytes to header parser");
+ if (parser.parse(b)) {
+ count -= b.remaining();
+ debug.log(Level.DEBUG, () ->
+ "Parsing headers completed. bytes=" + count);
+ onComplete.accept(State.READING_HEADERS);
+ cf.complete(State.READING_HEADERS);
+ }
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG,
+ () -> "Header parser failed to handle buffer: " + t);
+ cf.completeExceptionally(t);
+ }
+ }
+ }
+
+ // 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;
+ parser.onSubscribe(s);
+ }
+
+ @Override
+ final void handle(ByteBuffer b,
+ BodyParser parser,
+ CompletableFuture<State> cf) {
+ assert cf != null : "parsing not started";
+ assert parser != null : "no parser";
+ try {
+ debug.log(Level.DEBUG, () -> "Sending " + b.remaining()
+ + "/" + b.capacity() + " bytes to body parser");
+ parser.accept(b);
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG,
+ () -> "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 String toString() {
+ return super.toString() + "/parser=" + String.valueOf(parser);
+ }
+
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/Http2ClientImpl.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,231 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.lang.System.Logger.Level;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CompletableFuture;
+
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import jdk.incubator.http.internal.common.Utils;
+import jdk.incubator.http.internal.frame.SettingsFrame;
+import static jdk.incubator.http.internal.frame.SettingsFrame.INITIAL_WINDOW_SIZE;
+import static jdk.incubator.http.internal.frame.SettingsFrame.ENABLE_PUSH;
+import static jdk.incubator.http.internal.frame.SettingsFrame.HEADER_TABLE_SIZE;
+import static jdk.incubator.http.internal.frame.SettingsFrame.MAX_CONCURRENT_STREAMS;
+import static jdk.incubator.http.internal.frame.SettingsFrame.MAX_FRAME_SIZE;
+
+/**
+ * Http2 specific aspects of HttpClientImpl
+ */
+class Http2ClientImpl {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final static System.Logger debug =
+ Utils.getDebugLogger("Http2ClientImpl"::toString, 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) { // fast path if connection already exists
+ 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
+ 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) {
+ String key = c.key();
+ Http2Connection c1 = connections.putIfAbsent(key, c);
+ if (c1 != null) {
+ c.setSingleStream(true);
+ return false;
+ }
+ return true;
+ }
+
+ void deleteConnection(Http2Connection c) {
+ connections.remove(c.key());
+ }
+
+ void stop() {
+ debug.log(Level.DEBUG, "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;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/Http2Connection.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,1290 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Flow;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.internal.HttpConnection.HttpPublisher;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.FlowTube.TubeSubscriber;
+import jdk.incubator.http.internal.common.HttpHeadersImpl;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.Utils;
+import jdk.incubator.http.internal.frame.ContinuationFrame;
+import jdk.incubator.http.internal.frame.DataFrame;
+import jdk.incubator.http.internal.frame.ErrorFrame;
+import jdk.incubator.http.internal.frame.FramesDecoder;
+import jdk.incubator.http.internal.frame.FramesEncoder;
+import jdk.incubator.http.internal.frame.GoAwayFrame;
+import jdk.incubator.http.internal.frame.HeaderFrame;
+import jdk.incubator.http.internal.frame.HeadersFrame;
+import jdk.incubator.http.internal.frame.Http2Frame;
+import jdk.incubator.http.internal.frame.MalformedFrame;
+import jdk.incubator.http.internal.frame.OutgoingHeaders;
+import jdk.incubator.http.internal.frame.PingFrame;
+import jdk.incubator.http.internal.frame.PushPromiseFrame;
+import jdk.incubator.http.internal.frame.ResetFrame;
+import jdk.incubator.http.internal.frame.SettingsFrame;
+import jdk.incubator.http.internal.frame.WindowUpdateFrame;
+import jdk.incubator.http.internal.hpack.Encoder;
+import jdk.incubator.http.internal.hpack.Decoder;
+import jdk.incubator.http.internal.hpack.DecodingCallback;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static jdk.incubator.http.internal.frame.SettingsFrame.*;
+
+
+/**
+ * An Http2Connection. Encapsulates the socket(channel) and any SSLEngine used
+ * over it. Contains an HttpConnection which hides the SocketChannel SSL stuff.
+ *
+ * Http2Connections belong to a Http2ClientImpl, (one of) which belongs
+ * to a HttpClientImpl.
+ *
+ * Creation cases:
+ * 1) upgraded HTTP/1.1 plain tcp connection
+ * 2) prior knowledge directly created plain tcp connection
+ * 3) directly created HTTP/2 SSL connection which uses ALPN.
+ *
+ * Sending is done by writing directly to underlying HttpConnection object which
+ * is operating in async mode. No flow control applies on output at this level
+ * and all writes are just executed as puts to an output Q belonging to HttpConnection
+ * Flow control is implemented by HTTP/2 protocol itself.
+ *
+ * Hpack header compression
+ * and outgoing stream creation is also done here, because these operations
+ * must be synchronized at the socket level. Stream objects send frames simply
+ * by placing them on the connection's output Queue. sendFrame() is called
+ * from a higher level (Stream) thread.
+ *
+ * asyncReceive(ByteBuffer) is always called from the selector thread. It assembles
+ * incoming Http2Frames, and directs them to the appropriate Stream.incoming()
+ * or handles them directly itself. This thread performs hpack decompression
+ * and incoming stream creation (Server push). Incoming frames destined for a
+ * stream are provided by calling Stream.incoming().
+ */
+class Http2Connection {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ static final boolean DEBUG_HPACK = Utils.DEBUG_HPACK; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ final static System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("Http2Connection"::toString, DEBUG);
+ private final System.Logger debugHpack =
+ Utils.getHpackLogger(this::dbgString, DEBUG_HPACK);
+ static final ByteBuffer EMPTY_TRIGGER = ByteBuffer.allocate(0);
+
+ private boolean singleStream; // used only for stream 1, then closed
+
+ /*
+ * ByteBuffer pooling strategy for HTTP/2 protocol:
+ *
+ * In general there are 4 points where ByteBuffers are used:
+ * - incoming/outgoing frames from/to ByteBuffers plus incoming/outgoing encrypted data
+ * in case of SSL connection.
+ *
+ * 1. Outgoing frames encoded to ByteBuffers.
+ * Outgoing ByteBuffers are created with requited size and frequently small (except DataFrames, etc)
+ * At this place no pools at all. All outgoing buffers should be collected by GC.
+ *
+ * 2. Incoming ByteBuffers (decoded to frames).
+ * Here, total elimination of BB pool is not a good idea.
+ * We don't know how many bytes we will receive through network.
+ * So here we allocate buffer of reasonable size. The following life of the BB:
+ * - If all frames decoded from the BB are other than DataFrame and HeaderFrame (and HeaderFrame subclasses)
+ * BB is returned to pool,
+ * - If we decoded DataFrame from the BB. In that case DataFrame refers to subbuffer obtained by slice() method.
+ * Such BB is never returned to pool and will be GCed.
+ * - If we decoded HeadersFrame from the BB. Then header decoding is performed inside processFrame method and
+ * the buffer could be release to pool.
+ *
+ * 3. SLL encrypted buffers. Here another pool was introduced and all net buffers are to/from the pool,
+ * because of we can't predict size encrypted packets.
+ *
+ */
+
+
+ // A small class that allows to control frames with respect to the state of
+ // the connection preface. Any data received before the connection
+ // preface is sent will be buffered.
+ private final class FramesController {
+ volatile boolean prefaceSent;
+ volatile List<ByteBuffer> pending;
+
+ boolean processReceivedData(FramesDecoder decoder, ByteBuffer buf)
+ throws IOException
+ {
+ // if preface is not sent, buffers data in the pending list
+ if (!prefaceSent) {
+ debug.log(Level.DEBUG, "Preface is not sent: buffering %d",
+ buf.remaining());
+ synchronized (this) {
+ if (!prefaceSent) {
+ if (pending == null) pending = new ArrayList<>();
+ pending.add(buf);
+ debug.log(Level.DEBUG, () -> "there are now "
+ + Utils.remaining(pending)
+ + " bytes buffered waiting for preface to be sent");
+ return false;
+ }
+ }
+ }
+
+ // Preface is sent. Checks for pending data and flush it.
+ // We rely on this method being called from within the Http2TubeSubscriber
+ // scheduler, so we know that no other thread could execute this method
+ // concurrently while we're here.
+ // This ensures that later incoming buffers will not
+ // be processed before we have flushed the pending queue.
+ // No additional synchronization is therefore necessary here.
+ List<ByteBuffer> pending = this.pending;
+ this.pending = null;
+ if (pending != null) {
+ // flush pending data
+ debug.log(Level.DEBUG, () -> "Processing buffered data: "
+ + Utils.remaining(pending));
+ for (ByteBuffer b : pending) {
+ decoder.decode(b);
+ }
+ }
+ // push the received buffer to the frames decoder.
+ if (buf != EMPTY_TRIGGER) {
+ debug.log(Level.DEBUG, "Processing %d", buf.remaining());
+ decoder.decode(buf);
+ }
+ return true;
+ }
+
+ // Mark that the connection preface is sent
+ void markPrefaceSent() {
+ assert !prefaceSent;
+ synchronized (this) {
+ prefaceSent = true;
+ }
+ }
+ }
+
+ volatile boolean closed;
+
+ //-------------------------------------
+ final HttpConnection connection;
+ private final Http2ClientImpl client2;
+ private final Map<Integer,Stream<?>> streams = new ConcurrentHashMap<>();
+ private int nextstreamid;
+ private int nextPushStream = 2;
+ private final Encoder hpackOut;
+ private final Decoder hpackIn;
+ final SettingsFrame clientSettings;
+ private volatile SettingsFrame serverSettings;
+ private final String key; // for HttpClientImpl.connections map
+ private final FramesDecoder framesDecoder;
+ private final FramesEncoder framesEncoder = new FramesEncoder();
+
+ /**
+ * Send Window controller for both connection and stream windows.
+ * Each of this connection's Streams MUST use this controller.
+ */
+ private final WindowController windowController = new WindowController();
+ private final FramesController framesController = new FramesController();
+ private final Http2TubeSubscriber subscriber = new Http2TubeSubscriber();
+ final ConnectionWindowUpdateSender windowUpdater;
+ private volatile Throwable cause;
+ private volatile Supplier<ByteBuffer> initial;
+
+ static final int DEFAULT_FRAME_SIZE = 16 * 1024;
+
+
+ // TODO: need list of control frames from other threads
+ // that need to be sent
+
+ private Http2Connection(HttpConnection connection,
+ Http2ClientImpl client2,
+ int nextstreamid,
+ String key) {
+ this.connection = connection;
+ this.client2 = client2;
+ this.nextstreamid = nextstreamid;
+ this.key = key;
+ this.clientSettings = this.client2.getClientSettings();
+ this.framesDecoder = new FramesDecoder(this::processFrame,
+ clientSettings.getParameter(SettingsFrame.MAX_FRAME_SIZE));
+ // serverSettings will be updated by server
+ this.serverSettings = SettingsFrame.getDefaultSettings();
+ this.hpackOut = new Encoder(serverSettings.getParameter(HEADER_TABLE_SIZE));
+ this.hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE));
+ debugHpack.log(Level.DEBUG, () -> "For the record:" + super.toString());
+ debugHpack.log(Level.DEBUG, "Decoder created: %s", hpackIn);
+ debugHpack.log(Level.DEBUG, "Encoder created: %s", hpackOut);
+ this.windowUpdater = new ConnectionWindowUpdateSender(this,
+ client2.getConnectionWindowSize(clientSettings));
+ }
+
+ /**
+ * Case 1) Create from upgraded HTTP/1.1 connection.
+ * Is ready to use. Can be SSL. exchange is the Exchange
+ * that initiated the connection, whose response will be delivered
+ * on a Stream.
+ */
+ private Http2Connection(HttpConnection connection,
+ Http2ClientImpl client2,
+ Exchange<?> exchange,
+ Supplier<ByteBuffer> initial)
+ throws IOException, InterruptedException
+ {
+ this(connection,
+ client2,
+ 3, // stream 1 is registered during the upgrade
+ keyFor(connection));
+ Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
+
+ Stream<?> initialStream = createStream(exchange);
+ initialStream.registerStream(1);
+ windowController.registerStream(1, getInitialSendWindowSize());
+ initialStream.requestSent();
+ // Upgrading:
+ // set callbacks before sending preface - makes sure anything that
+ // might be sent by the server will come our way.
+ this.initial = initial;
+ connectFlows(connection);
+ sendConnectionPreface();
+ }
+
+ // Used when upgrading an HTTP/1.1 connection to HTTP/2 after receiving
+ // agreement from the server. Async style but completes immediately, because
+ // the connection is already connected.
+ static CompletableFuture<Http2Connection> createAsync(HttpConnection connection,
+ Http2ClientImpl client2,
+ Exchange<?> exchange,
+ Supplier<ByteBuffer> initial)
+ {
+ return MinimalFuture.supply(() -> new Http2Connection(connection, client2, exchange, initial));
+ }
+
+ // Requires TLS handshake. So, is really async
+ static CompletableFuture<Http2Connection> createAsync(HttpRequestImpl request,
+ Http2ClientImpl h2client) {
+ assert request.secure();
+ AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection)
+ HttpConnection.getConnection(request.getAddress(),
+ h2client.client(),
+ request,
+ HttpClient.Version.HTTP_2);
+
+ return connection.connectAsync()
+ .thenCompose(unused -> checkSSLConfig(connection))
+ .thenCompose(notused-> {
+ CompletableFuture<Http2Connection> cf = new MinimalFuture<>();
+ try {
+ Http2Connection hc = new Http2Connection(request, h2client, connection);
+ cf.complete(hc);
+ } catch (IOException e) {
+ cf.completeExceptionally(e);
+ }
+ return cf; } );
+ }
+
+ /**
+ * Cases 2) 3)
+ *
+ * request is request to be sent.
+ */
+ private Http2Connection(HttpRequestImpl request,
+ Http2ClientImpl h2client,
+ HttpConnection connection)
+ throws IOException
+ {
+ this(connection,
+ h2client,
+ 1,
+ keyFor(request.uri(), request.proxy()));
+
+ Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
+
+ // safe to resume async reading now.
+ connectFlows(connection);
+ sendConnectionPreface();
+ }
+
+ private void connectFlows(HttpConnection connection) {
+ FlowTube tube = connection.getConnectionFlow();
+ // Connect the flow to our Http2TubeSubscriber:
+ tube.connectFlows(connection.publisher(), subscriber);
+ }
+
+ final HttpClientImpl client() {
+ return client2.client();
+ }
+
+ /**
+ * Throws an IOException if h2 was not negotiated
+ */
+ private static CompletableFuture<?> checkSSLConfig(AbstractAsyncSSLConnection aconn) {
+ assert aconn.isSecure();
+
+ Function<String, CompletableFuture<Void>> checkAlpnCF = (alpn) -> {
+ CompletableFuture<Void> cf = new MinimalFuture<>();
+ SSLEngine engine = aconn.getEngine();
+ assert Objects.equals(alpn, engine.getApplicationProtocol());
+
+ DEBUG_LOGGER.log(Level.DEBUG, "checkSSLConfig: alpn: %s", alpn );
+
+ if (alpn == null || !alpn.equals("h2")) {
+ String msg;
+ if (alpn == null) {
+ Log.logSSL("ALPN not supported");
+ msg = "ALPN not supported";
+ } else {
+ switch (alpn) {
+ case "":
+ Log.logSSL(msg = "No ALPN negotiated");
+ break;
+ case "http/1.1":
+ Log.logSSL( msg = "HTTP/1.1 ALPN returned");
+ break;
+ default:
+ Log.logSSL(msg = "Unexpected ALPN: " + alpn);
+ cf.completeExceptionally(new IOException(msg));
+ }
+ }
+ cf.completeExceptionally(new ALPNException(msg, aconn));
+ return cf;
+ }
+ cf.complete(null);
+ return cf;
+ };
+
+ return aconn.getALPN()
+ .whenComplete((r,t) -> {
+ if (t != null && t instanceof SSLException) {
+ // something went wrong during the initial handshake
+ // close the connection
+ aconn.close();
+ }
+ })
+ .thenCompose(checkAlpnCF);
+ }
+
+ synchronized boolean singleStream() {
+ return singleStream;
+ }
+
+ synchronized void setSingleStream(boolean use) {
+ singleStream = use;
+ }
+
+ static String keyFor(HttpConnection connection) {
+ boolean isProxy = connection.isProxied();
+ boolean isSecure = connection.isSecure();
+ InetSocketAddress addr = connection.address();
+
+ return keyString(isSecure, isProxy, addr.getHostString(), addr.getPort());
+ }
+
+ static String keyFor(URI uri, InetSocketAddress proxy) {
+ boolean isSecure = uri.getScheme().equalsIgnoreCase("https");
+ boolean isProxy = proxy != null;
+
+ String host;
+ int port;
+
+ if (proxy != null) {
+ host = proxy.getHostString();
+ port = proxy.getPort();
+ } else {
+ host = uri.getHost();
+ port = uri.getPort();
+ }
+ return keyString(isSecure, isProxy, host, port);
+ }
+
+ // {C,S}:{H:P}:host:port
+ // C indicates clear text connection "http"
+ // S indicates secure "https"
+ // H indicates host (direct) connection
+ // P indicates proxy
+ // Eg: "S:H:foo.com:80"
+ static String keyString(boolean secure, boolean proxy, String host, int port) {
+ if (secure && port == -1)
+ port = 443;
+ else if (!secure && port == -1)
+ port = 80;
+ return (secure ? "S:" : "C:") + (proxy ? "P:" : "H:") + host + ":" + port;
+ }
+
+ String key() {
+ return this.key;
+ }
+
+ boolean offerConnection() {
+ return client2.offerConnection(this);
+ }
+
+ private HttpPublisher publisher() {
+ return connection.publisher();
+ }
+
+ private void decodeHeaders(HeaderFrame frame, DecodingCallback decoder)
+ throws IOException
+ {
+ debugHpack.log(Level.DEBUG, "decodeHeaders(%s)", decoder);
+
+ boolean endOfHeaders = frame.getFlag(HeaderFrame.END_HEADERS);
+
+ List<ByteBuffer> buffers = frame.getHeaderBlock();
+ int len = buffers.size();
+ for (int i = 0; i < len; i++) {
+ ByteBuffer b = buffers.get(i);
+ hpackIn.decode(b, endOfHeaders && (i == len - 1), decoder);
+ }
+ }
+
+ final int getInitialSendWindowSize() {
+ return serverSettings.getParameter(INITIAL_WINDOW_SIZE);
+ }
+
+ void close() {
+ Log.logTrace("Closing HTTP/2 connection: to {0}", connection.address());
+ GoAwayFrame f = new GoAwayFrame(0,
+ ErrorFrame.NO_ERROR,
+ "Requested by user".getBytes(UTF_8));
+ // TODO: set last stream. For now zero ok.
+ sendFrame(f);
+ }
+
+ long count;
+ final void asyncReceive(ByteBuffer buffer) {
+ // We don't need to read anything and
+ // we don't want to send anything back to the server
+ // until the connection preface has been sent.
+ // Therefore we're going to wait if needed before reading
+ // (and thus replying) to anything.
+ // Starting to reply to something (e.g send an ACK to a
+ // SettingsFrame sent by the server) before the connection
+ // preface is fully sent might result in the server
+ // sending a GOAWAY frame with 'invalid_preface'.
+ //
+ // Note: asyncReceive is only called from the Http2TubeSubscriber
+ // sequential scheduler.
+ try {
+ Supplier<ByteBuffer> bs = initial;
+ // ensure that we always handle the initial buffer first,
+ // if any.
+ if (bs != null) {
+ initial = null;
+ ByteBuffer b = bs.get();
+ if (b.hasRemaining()) {
+ long c = ++count;
+ debug.log(Level.DEBUG, () -> "H2 Receiving Initial("
+ + c +"): " + b.remaining());
+ framesController.processReceivedData(framesDecoder, b);
+ }
+ }
+ ByteBuffer b = buffer;
+ // the Http2TubeSubscriber scheduler ensures that the order of incoming
+ // buffers is preserved.
+ if (b == EMPTY_TRIGGER) {
+ debug.log(Level.DEBUG, "H2 Received EMPTY_TRIGGER");
+ boolean prefaceSent = framesController.prefaceSent;
+ assert prefaceSent;
+ // call framesController.processReceivedData to potentially
+ // trigger the processing of all the data buffered there.
+ framesController.processReceivedData(framesDecoder, buffer);
+ debug.log(Level.DEBUG, "H2 processed buffered data");
+ } else {
+ long c = ++count;
+ debug.log(Level.DEBUG, "H2 Receiving(%d): %d", c, b.remaining());
+ framesController.processReceivedData(framesDecoder, buffer);
+ debug.log(Level.DEBUG, "H2 processed(%d)", c);
+ }
+ } catch (Throwable e) {
+ String msg = Utils.stackTrace(e);
+ Log.logTrace(msg);
+ shutdown(e);
+ }
+ }
+
+ Throwable getRecordedCause() {
+ return cause;
+ }
+
+ void shutdown(Throwable t) {
+ debug.log(Level.DEBUG, () -> "Shutting down h2c (closed="+closed+"): " + t);
+ if (closed == true) return;
+ synchronized (this) {
+ if (closed == true) return;
+ closed = true;
+ }
+ Log.logError(t);
+ Throwable initialCause = this.cause;
+ if (initialCause == null) this.cause = t;
+ client2.deleteConnection(this);
+ List<Stream<?>> c = new LinkedList<>(streams.values());
+ for (Stream<?> s : c) {
+ s.cancelImpl(t);
+ }
+ connection.close();
+ }
+
+ /**
+ * Streams initiated by a client MUST use odd-numbered stream
+ * identifiers; those initiated by the server MUST use even-numbered
+ * stream identifiers.
+ */
+ private static final boolean isSeverInitiatedStream(int streamid) {
+ return (streamid & 0x1) == 0;
+ }
+
+ /**
+ * Handles stream 0 (common) frames that apply to whole connection and passes
+ * other stream specific frames to that Stream object.
+ *
+ * Invokes Stream.incoming() which is expected to process frame without
+ * blocking.
+ */
+ void processFrame(Http2Frame frame) throws IOException {
+ Log.logFrames(frame, "IN");
+ int streamid = frame.streamid();
+ if (frame instanceof MalformedFrame) {
+ Log.logError(((MalformedFrame) frame).getMessage());
+ if (streamid == 0) {
+ framesDecoder.close("Malformed frame on stream 0");
+ protocolError(((MalformedFrame) frame).getErrorCode(),
+ ((MalformedFrame) frame).getMessage());
+ } else {
+ debug.log(Level.DEBUG, () -> "Reset stream: "
+ + ((MalformedFrame) frame).getMessage());
+ resetStream(streamid, ((MalformedFrame) frame).getErrorCode());
+ }
+ return;
+ }
+ if (streamid == 0) {
+ handleConnectionFrame(frame);
+ } else {
+ if (frame instanceof SettingsFrame) {
+ // The stream identifier for a SETTINGS frame MUST be zero
+ framesDecoder.close(
+ "The stream identifier for a SETTINGS frame MUST be zero");
+ protocolError(GoAwayFrame.PROTOCOL_ERROR);
+ return;
+ }
+
+ Stream<?> stream = getStream(streamid);
+ if (stream == null) {
+ // Should never receive a frame with unknown stream id
+
+ if (frame instanceof HeaderFrame) {
+ // always decode the headers as they may affect
+ // connection-level HPACK decoding state
+ HeaderDecoder decoder = new LoggingHeaderDecoder(new HeaderDecoder());
+ decodeHeaders((HeaderFrame) frame, decoder);
+ }
+
+ if (!(frame instanceof ResetFrame)) {
+ if (isSeverInitiatedStream(streamid)) {
+ if (streamid < nextPushStream) {
+ // trailing data on a cancelled push promise stream,
+ // reset will already have been sent, ignore
+ Log.logTrace("Ignoring cancelled push promise frame " + frame);
+ } else {
+ resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
+ }
+ } else if (streamid >= nextstreamid) {
+ // otherwise the stream has already been reset/closed
+ resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
+ }
+ }
+ return;
+ }
+ if (frame instanceof PushPromiseFrame) {
+ PushPromiseFrame pp = (PushPromiseFrame)frame;
+ handlePushPromise(stream, pp);
+ } else if (frame instanceof HeaderFrame) {
+ // decode headers (or continuation)
+ decodeHeaders((HeaderFrame) frame, stream.rspHeadersConsumer());
+ stream.incoming(frame);
+ } else {
+ stream.incoming(frame);
+ }
+ }
+ }
+
+ private <T> void handlePushPromise(Stream<T> parent, PushPromiseFrame pp)
+ throws IOException
+ {
+ // always decode the headers as they may affect connection-level HPACK
+ // decoding state
+ HeaderDecoder decoder = new LoggingHeaderDecoder(new HeaderDecoder());
+ decodeHeaders(pp, decoder);
+
+ HttpRequestImpl parentReq = parent.request;
+ int promisedStreamid = pp.getPromisedStream();
+ if (promisedStreamid != nextPushStream) {
+ resetStream(promisedStreamid, ResetFrame.PROTOCOL_ERROR);
+ return;
+ } else {
+ nextPushStream += 2;
+ }
+
+ HttpHeadersImpl headers = decoder.headers();
+ HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest(parentReq, headers);
+ Exchange<T> pushExch = new Exchange<>(pushReq, parent.exchange.multi);
+ Stream.PushedStream<T> pushStream = createPushStream(parent, pushExch);
+ pushExch.exchImpl = pushStream;
+ pushStream.registerStream(promisedStreamid);
+ parent.incoming_pushPromise(pushReq, pushStream);
+ }
+
+ private void handleConnectionFrame(Http2Frame frame)
+ throws IOException
+ {
+ switch (frame.type()) {
+ case SettingsFrame.TYPE:
+ handleSettings((SettingsFrame)frame);
+ break;
+ case PingFrame.TYPE:
+ handlePing((PingFrame)frame);
+ break;
+ case GoAwayFrame.TYPE:
+ handleGoAway((GoAwayFrame)frame);
+ break;
+ case WindowUpdateFrame.TYPE:
+ handleWindowUpdate((WindowUpdateFrame)frame);
+ break;
+ default:
+ protocolError(ErrorFrame.PROTOCOL_ERROR);
+ }
+ }
+
+ void resetStream(int streamid, int code) throws IOException {
+ Log.logError(
+ "Resetting stream {0,number,integer} with error code {1,number,integer}",
+ streamid, code);
+ ResetFrame frame = new ResetFrame(streamid, code);
+ sendFrame(frame);
+ closeStream(streamid);
+ }
+
+ void closeStream(int streamid) {
+ debug.log(Level.DEBUG, "Closed stream %d", streamid);
+ Stream<?> s = streams.remove(streamid);
+ if (s != null) {
+ // 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.
+ client().unreference();
+ }
+ // ## Remove s != null. It is a hack for delayed cancellation,reset
+ if (s != null && !(s instanceof Stream.PushedStream)) {
+ // Since PushStreams have no request body, then they have no
+ // corresponding entry in the window controller.
+ windowController.removeStream(streamid);
+ }
+ if (singleStream() && streams.isEmpty()) {
+ // should be only 1 stream, but there might be more if server push
+ close();
+ }
+ }
+
+ /**
+ * Increments this connection's send Window by the amount in the given frame.
+ */
+ private void handleWindowUpdate(WindowUpdateFrame f)
+ throws IOException
+ {
+ int amount = f.getUpdate();
+ if (amount <= 0) {
+ // ## temporarily disable to workaround a bug in Jetty where it
+ // ## sends Window updates with a 0 update value.
+ //protocolError(ErrorFrame.PROTOCOL_ERROR);
+ } else {
+ boolean success = windowController.increaseConnectionWindow(amount);
+ if (!success) {
+ protocolError(ErrorFrame.FLOW_CONTROL_ERROR); // overflow
+ }
+ }
+ }
+
+ private void protocolError(int errorCode)
+ throws IOException
+ {
+ protocolError(errorCode, null);
+ }
+
+ private void protocolError(int errorCode, String msg)
+ throws IOException
+ {
+ GoAwayFrame frame = new GoAwayFrame(0, errorCode);
+ sendFrame(frame);
+ shutdown(new IOException("protocol error" + (msg == null?"":(": " + msg))));
+ }
+
+ private void handleSettings(SettingsFrame frame)
+ throws IOException
+ {
+ assert frame.streamid() == 0;
+ if (!frame.getFlag(SettingsFrame.ACK)) {
+ int oldWindowSize = serverSettings.getParameter(INITIAL_WINDOW_SIZE);
+ int newWindowSize = frame.getParameter(INITIAL_WINDOW_SIZE);
+ int diff = newWindowSize - oldWindowSize;
+ if (diff != 0) {
+ windowController.adjustActiveStreams(diff);
+ }
+ serverSettings = frame;
+ sendFrame(new SettingsFrame(SettingsFrame.ACK));
+ }
+ }
+
+ private void handlePing(PingFrame frame)
+ throws IOException
+ {
+ frame.setFlag(PingFrame.ACK);
+ sendUnorderedFrame(frame);
+ }
+
+ private void handleGoAway(GoAwayFrame frame)
+ throws IOException
+ {
+ shutdown(new IOException(
+ String.valueOf(connection.channel().getLocalAddress())
+ +": GOAWAY received"));
+ }
+
+ /**
+ * Max frame size we are allowed to send
+ */
+ public int getMaxSendFrameSize() {
+ int param = serverSettings.getParameter(MAX_FRAME_SIZE);
+ if (param == -1) {
+ param = DEFAULT_FRAME_SIZE;
+ }
+ return param;
+ }
+
+ /**
+ * Max frame size we will receive
+ */
+ public int getMaxReceiveFrameSize() {
+ return clientSettings.getParameter(MAX_FRAME_SIZE);
+ }
+
+ private static final String CLIENT_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
+
+ private static final byte[] PREFACE_BYTES =
+ CLIENT_PREFACE.getBytes(StandardCharsets.ISO_8859_1);
+
+ /**
+ * Sends Connection preface and Settings frame with current preferred
+ * values
+ */
+ private void sendConnectionPreface() throws IOException {
+ Log.logTrace("{0}: start sending connection preface to {1}",
+ connection.channel().getLocalAddress(),
+ connection.address());
+ SettingsFrame sf = new SettingsFrame(clientSettings);
+ int initialWindowSize = sf.getParameter(INITIAL_WINDOW_SIZE);
+ ByteBuffer buf = framesEncoder.encodeConnectionPreface(PREFACE_BYTES, sf);
+ Log.logFrames(sf, "OUT");
+ // send preface bytes and SettingsFrame together
+ HttpPublisher publisher = publisher();
+ publisher.enqueue(List.of(buf));
+ publisher.signalEnqueued();
+ // mark preface sent.
+ framesController.markPrefaceSent();
+ Log.logTrace("PREFACE_BYTES sent");
+ Log.logTrace("Settings Frame sent");
+
+ // send a Window update for the receive buffer we are using
+ // minus the initial 64 K specified in protocol
+ final int len = windowUpdater.initialWindowSize - initialWindowSize;
+ if (len > 0) {
+ windowUpdater.sendWindowUpdate(len);
+ }
+ // there will be an ACK to the windows update - which should
+ // cause any pending data stored before the preface was sent to be
+ // flushed (see PrefaceController).
+ Log.logTrace("finished sending connection preface");
+ debug.log(Level.DEBUG, "Triggering processing of buffered data"
+ + " after sending connection preface");
+ subscriber.onNext(List.of(EMPTY_TRIGGER));
+ }
+
+ /**
+ * Returns an existing Stream with given id, or null if doesn't exist
+ */
+ @SuppressWarnings("unchecked")
+ <T> Stream<T> getStream(int streamid) {
+ return (Stream<T>)streams.get(streamid);
+ }
+
+ /**
+ * Creates Stream with given id.
+ */
+ final <T> Stream<T> createStream(Exchange<T> exchange) {
+ Stream<T> stream = new Stream<>(this, exchange, windowController);
+ return stream;
+ }
+
+ <T> Stream.PushedStream<T> createPushStream(Stream<T> parent, Exchange<T> pushEx) {
+ PushGroup<T> pg = parent.exchange.getPushGroup();
+ return new Stream.PushedStream<>(pg, this, pushEx);
+ }
+
+ <T> void putStream(Stream<T> stream, int streamid) {
+ // increment the reference count on the HttpClientImpl
+ // to prevent the SelectorManager thread from exiting until
+ // the stream is closed.
+ client().reference();
+ streams.put(streamid, stream);
+ }
+
+ /**
+ * Encode the headers into a List<ByteBuffer> and then create HEADERS
+ * and CONTINUATION frames from the list and return the List<Http2Frame>.
+ */
+ private List<HeaderFrame> encodeHeaders(OutgoingHeaders<Stream<?>> frame) {
+ List<ByteBuffer> buffers = encodeHeadersImpl(
+ getMaxSendFrameSize(),
+ frame.getAttachment().getRequestPseudoHeaders(),
+ frame.getUserHeaders(),
+ frame.getSystemHeaders());
+
+ List<HeaderFrame> frames = new ArrayList<>(buffers.size());
+ Iterator<ByteBuffer> bufIterator = buffers.iterator();
+ HeaderFrame oframe = new HeadersFrame(frame.streamid(), frame.getFlags(), bufIterator.next());
+ frames.add(oframe);
+ while(bufIterator.hasNext()) {
+ oframe = new ContinuationFrame(frame.streamid(), bufIterator.next());
+ frames.add(oframe);
+ }
+ oframe.setFlag(HeaderFrame.END_HEADERS);
+ return frames;
+ }
+
+ // Dedicated cache for headers encoding ByteBuffer.
+ // There can be no concurrent access to this buffer as all access to this buffer
+ // and its content happen within a single critical code block section protected
+ // by the sendLock. / (see sendFrame())
+ // private final ByteBufferPool headerEncodingPool = new ByteBufferPool();
+
+ private ByteBuffer getHeaderBuffer(int maxFrameSize) {
+ ByteBuffer buf = ByteBuffer.allocate(maxFrameSize);
+ buf.limit(maxFrameSize);
+ return buf;
+ }
+
+ /*
+ * Encodes all the headers from the given HttpHeaders into the given List
+ * of buffers.
+ *
+ * From https://tools.ietf.org/html/rfc7540#section-8.1.2 :
+ *
+ * ...Just as in HTTP/1.x, header field names are strings of ASCII
+ * characters that are compared in a case-insensitive fashion. However,
+ * header field names MUST be converted to lowercase prior to their
+ * encoding in HTTP/2...
+ */
+ private List<ByteBuffer> encodeHeadersImpl(int maxFrameSize, HttpHeaders... headers) {
+ ByteBuffer buffer = getHeaderBuffer(maxFrameSize);
+ List<ByteBuffer> buffers = new ArrayList<>();
+ for(HttpHeaders header : headers) {
+ for (Map.Entry<String, List<String>> e : header.map().entrySet()) {
+ String lKey = e.getKey().toLowerCase();
+ List<String> values = e.getValue();
+ for (String value : values) {
+ hpackOut.header(lKey, value);
+ while (!hpackOut.encode(buffer)) {
+ buffer.flip();
+ buffers.add(buffer);
+ buffer = getHeaderBuffer(maxFrameSize);
+ }
+ }
+ }
+ }
+ buffer.flip();
+ buffers.add(buffer);
+ return buffers;
+ }
+
+ private List<ByteBuffer> encodeHeaders(OutgoingHeaders<Stream<?>> oh, Stream<?> stream) {
+ oh.streamid(stream.streamid);
+ if (Log.headers()) {
+ StringBuilder sb = new StringBuilder("HEADERS FRAME (stream=");
+ sb.append(stream.streamid).append(")\n");
+ Log.dumpHeaders(sb, " ", oh.getAttachment().getRequestPseudoHeaders());
+ Log.dumpHeaders(sb, " ", oh.getSystemHeaders());
+ Log.dumpHeaders(sb, " ", oh.getUserHeaders());
+ Log.logHeaders(sb.toString());
+ }
+ List<HeaderFrame> frames = encodeHeaders(oh);
+ return encodeFrames(frames);
+ }
+
+ private List<ByteBuffer> encodeFrames(List<HeaderFrame> frames) {
+ if (Log.frames()) {
+ frames.forEach(f -> Log.logFrames(f, "OUT"));
+ }
+ return framesEncoder.encodeFrames(frames);
+ }
+
+ private Stream<?> registerNewStream(OutgoingHeaders<Stream<?>> oh) {
+ Stream<?> stream = oh.getAttachment();
+ int streamid = nextstreamid;
+ nextstreamid += 2;
+ stream.registerStream(streamid);
+ // set outgoing window here. This allows thread sending
+ // body to proceed.
+ windowController.registerStream(streamid, getInitialSendWindowSize());
+ return stream;
+ }
+
+ private final Object sendlock = new Object();
+
+ void sendFrame(Http2Frame frame) {
+ try {
+ HttpPublisher publisher = publisher();
+ synchronized (sendlock) {
+ if (frame instanceof OutgoingHeaders) {
+ @SuppressWarnings("unchecked")
+ OutgoingHeaders<Stream<?>> oh = (OutgoingHeaders<Stream<?>>) frame;
+ Stream<?> stream = registerNewStream(oh);
+ // provide protection from inserting unordered frames between Headers and Continuation
+ publisher.enqueue(encodeHeaders(oh, stream));
+ } else {
+ publisher.enqueue(encodeFrame(frame));
+ }
+ }
+ publisher.signalEnqueued();
+ } catch (IOException e) {
+ if (!closed) {
+ Log.logError(e);
+ shutdown(e);
+ }
+ }
+ }
+
+ private List<ByteBuffer> encodeFrame(Http2Frame frame) {
+ Log.logFrames(frame, "OUT");
+ return framesEncoder.encodeFrame(frame);
+ }
+
+ void sendDataFrame(DataFrame frame) {
+ try {
+ HttpPublisher publisher = publisher();
+ publisher.enqueue(encodeFrame(frame));
+ publisher.signalEnqueued();
+ } catch (IOException e) {
+ if (!closed) {
+ Log.logError(e);
+ shutdown(e);
+ }
+ }
+ }
+
+ /*
+ * Direct call of the method bypasses synchronization on "sendlock" and
+ * allowed only of control frames: WindowUpdateFrame, PingFrame and etc.
+ * prohibited for such frames as DataFrame, HeadersFrame, ContinuationFrame.
+ */
+ void sendUnorderedFrame(Http2Frame frame) {
+ try {
+ HttpPublisher publisher = publisher();
+ publisher.enqueueUnordered(encodeFrame(frame));
+ publisher.signalEnqueued();
+ } catch (IOException e) {
+ if (!closed) {
+ Log.logError(e);
+ shutdown(e);
+ }
+ }
+ }
+
+ /**
+ * A simple tube subscriber for reading from the connection flow.
+ */
+ final class Http2TubeSubscriber implements TubeSubscriber {
+ volatile Flow.Subscription subscription;
+ volatile boolean completed;
+ volatile boolean dropped;
+ volatile Throwable error;
+ final ConcurrentLinkedQueue<ByteBuffer> queue
+ = new ConcurrentLinkedQueue<>();
+ final SequentialScheduler scheduler =
+ SequentialScheduler.synchronizedScheduler(this::processQueue);
+
+ final void processQueue() {
+ try {
+ while (!queue.isEmpty() && !scheduler.isStopped()) {
+ ByteBuffer buffer = queue.poll();
+ debug.log(Level.DEBUG,
+ "sending %d to Http2Connection.asyncReceive",
+ buffer.remaining());
+ asyncReceive(buffer);
+ }
+ } catch (Throwable t) {
+ Throwable x = error;
+ if (x == null) error = t;
+ } finally {
+ Throwable x = error;
+ if (x != null) {
+ debug.log(Level.DEBUG, "Stopping scheduler", x);
+ scheduler.stop();
+ Http2Connection.this.shutdown(x);
+ }
+ }
+ }
+
+ @Override
+ 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;
+ // TODO FIXME: request(1) should be done by the delegate.
+ if (!completed) {
+ debug.log(Level.DEBUG, "onSubscribe: requesting Long.MAX_VALUE for reading");
+ subscription.request(Long.MAX_VALUE);
+ } else {
+ debug.log(Level.DEBUG, "onSubscribe: already completed");
+ }
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ debug.log(Level.DEBUG, () -> "onNext: got " + Utils.remaining(item)
+ + " bytes in " + item.size() + " buffers");
+ queue.addAll(item);
+ scheduler.runOrSchedule(client().theExecutor());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ debug.log(Level.DEBUG, () -> "onError: " + throwable);
+ error = throwable;
+ completed = true;
+ scheduler.runOrSchedule(client().theExecutor());
+ }
+
+ @Override
+ public void onComplete() {
+ debug.log(Level.DEBUG, "EOF");
+ error = new EOFException("EOF reached while reading");
+ completed = true;
+ scheduler.runOrSchedule(client().theExecutor());
+ }
+
+ @Override
+ public void dropSubscription() {
+ debug.log(Level.DEBUG, "dropSubscription");
+ // we could probably set subscription to null here...
+ // then we might not need the 'dropped' boolean?
+ dropped = true;
+ }
+ }
+
+ @Override
+ public final String toString() {
+ return dbgString();
+ }
+
+ final String dbgString() {
+ return "Http2Connection("
+ + connection.getConnectionFlow() + ")";
+ }
+
+ final class LoggingHeaderDecoder extends HeaderDecoder {
+
+ private final HeaderDecoder delegate;
+ private final System.Logger debugHpack =
+ Utils.getHpackLogger(this::dbgString, DEBUG_HPACK);
+
+ LoggingHeaderDecoder(HeaderDecoder delegate) {
+ this.delegate = delegate;
+ }
+
+ String dbgString() {
+ return Http2Connection.this.dbgString() + "/LoggingHeaderDecoder";
+ }
+
+ @Override
+ public void onDecoded(CharSequence name, CharSequence value) {
+ delegate.onDecoded(name, value);
+ }
+
+ @Override
+ public void onIndexed(int index,
+ CharSequence name,
+ CharSequence value) {
+ debugHpack.log(Level.DEBUG, "onIndexed(%s, %s, %s)%n",
+ index, name, value);
+ delegate.onIndexed(index, name, value);
+ }
+
+ @Override
+ public void onLiteral(int index,
+ CharSequence name,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteral(%s, %s, %s, %s)%n",
+ index, name, value, valueHuffman);
+ delegate.onLiteral(index, name, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteral(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteral(%s, %s, %s, %s)%n",
+ name, nameHuffman, value, valueHuffman);
+ delegate.onLiteral(name, nameHuffman, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteralNeverIndexed(int index,
+ CharSequence name,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteralNeverIndexed(%s, %s, %s, %s)%n",
+ index, name, value, valueHuffman);
+ delegate.onLiteralNeverIndexed(index, name, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteralNeverIndexed(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteralNeverIndexed(%s, %s, %s, %s)%n",
+ name, nameHuffman, value, valueHuffman);
+ delegate.onLiteralNeverIndexed(name, nameHuffman, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteralWithIndexing(int index,
+ CharSequence name,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteralWithIndexing(%s, %s, %s, %s)%n",
+ index, name, value, valueHuffman);
+ delegate.onLiteralWithIndexing(index, name, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteralWithIndexing(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteralWithIndexing(%s, %s, %s, %s)%n",
+ name, nameHuffman, value, valueHuffman);
+ delegate.onLiteralWithIndexing(name, nameHuffman, value, valueHuffman);
+ }
+
+ @Override
+ public void onSizeUpdate(int capacity) {
+ debugHpack.log(Level.DEBUG, "onSizeUpdate(%s)%n", capacity);
+ delegate.onSizeUpdate(capacity);
+ }
+
+ @Override
+ HttpHeadersImpl headers() {
+ return delegate.headers();
+ }
+ }
+
+ static class HeaderDecoder implements DecodingCallback {
+ HttpHeadersImpl headers;
+
+ HeaderDecoder() {
+ this.headers = new HttpHeadersImpl();
+ }
+
+ @Override
+ public void onDecoded(CharSequence name, CharSequence value) {
+ headers.addHeader(name.toString(), value.toString());
+ }
+
+ HttpHeadersImpl headers() {
+ return headers;
+ }
+ }
+
+ static final class ConnectionWindowUpdateSender extends WindowUpdateSender {
+
+ final int initialWindowSize;
+ public ConnectionWindowUpdateSender(Http2Connection connection,
+ int initialWindowSize) {
+ super(connection, initialWindowSize);
+ this.initialWindowSize = initialWindowSize;
+ }
+
+ @Override
+ int getStreamId() {
+ return 0;
+ }
+ }
+
+ /**
+ * Thrown when https handshake negotiates http/1.1 alpn instead of h2
+ */
+ static final class ALPNException extends IOException {
+ private static final long serialVersionUID = 0L;
+ final transient AbstractAsyncSSLConnection connection;
+
+ ALPNException(String msg, AbstractAsyncSSLConnection connection) {
+ super(msg);
+ this.connection = connection;
+ }
+
+ AbstractAsyncSSLConnection getConnection() {
+ return connection;
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/HttpClientBuilderImpl.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+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 jdk.incubator.http.HttpClient;
+import jdk.incubator.http.internal.common.Utils;
+import static java.util.Objects.requireNonNull;
+
+public class HttpClientBuilderImpl extends 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);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/HttpClientFacade.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,148 @@
+/*
+ * 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.incubator.http.internal;
+
+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 jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.HttpResponse.BodyHandler;
+import jdk.incubator.http.HttpResponse.PushPromiseHandler;
+import jdk.incubator.http.WebSocket;
+
+/**
+ * An HttpClientFacade is a simple class that wraps an HttpClient implementation
+ * and delegates everything to its implementation delegate.
+ */
+final class HttpClientFacade extends HttpClient {
+
+ final HttpClientImpl impl;
+
+ /**
+ * Creates an HttpClientFacade.
+ */
+ HttpClientFacade(HttpClientImpl impl) {
+ this.impl = impl;
+ }
+
+ @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();
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/HttpClientImpl.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,1017 @@
+/*
+ * 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.incubator.http.internal;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.lang.ref.WeakReference;
+import java.net.Authenticator;
+import java.net.CookieHandler;
+import java.net.ProxySelector;
+import java.nio.channels.CancelledKeyException;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivilegedAction;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Stream;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.HttpResponse.BodyHandler;
+import jdk.incubator.http.HttpResponse.PushPromiseHandler;
+import jdk.incubator.http.WebSocket;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.Pair;
+import jdk.incubator.http.internal.common.Utils;
+import jdk.incubator.http.internal.websocket.BuilderImpl;
+import jdk.internal.misc.InnocuousThread;
+
+/**
+ * Client implementation. Contains all configuration information and also
+ * the selector manager thread which allows async events to be registered
+ * and delivered when they occur. See AsyncEvent.
+ */
+class HttpClientImpl extends HttpClient {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ static final boolean DEBUGELAPSED = Utils.TESTING || DEBUG; // Revisit: temporary dev flag.
+ static final boolean DEBUGTIMEOUT = false; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ final System.Logger debugelapsed = Utils.getDebugLogger(this::dbgString, DEBUGELAPSED);
+ final System.Logger debugtimeout = Utils.getDebugLogger(this::dbgString, DEBUGTIMEOUT);
+ static final AtomicLong CLIENT_IDS = new AtomicLong();
+
+ // Define the default factory as a static inner class
+ // that embeds all the necessary logic to avoid
+ // the risk of using a lambda that might keep a reference on the
+ // HttpClient instance from which it was created (helps with
+ // heapdump analysis).
+ private static final class DefaultThreadFactory implements ThreadFactory {
+ private final String namePrefix;
+ private final AtomicInteger nextId = new AtomicInteger();
+
+ DefaultThreadFactory(long clientID) {
+ namePrefix = "HttpClient-" + clientID + "-Worker-";
+ }
+
+ @Override
+ public Thread newThread(Runnable r) {
+ String name = namePrefix + nextId.getAndIncrement();
+ Thread t;
+ if (System.getSecurityManager() == null) {
+ t = new Thread(null, r, name, 0, false);
+ } else {
+ t = InnocuousThread.newThread(name, r);
+ }
+ t.setDaemon(true);
+ return t;
+ }
+ }
+
+ private final CookieHandler cookieHandler;
+ private final Redirect followRedirects;
+ private final Optional<ProxySelector> userProxySelector;
+ private final ProxySelector proxySelector;
+ private final Authenticator authenticator;
+ private final Version version;
+ private final ConnectionPool connections;
+ private final Executor executor;
+ private final boolean isDefaultExecutor;
+ // Security parameters
+ private final SSLContext sslContext;
+ private final SSLParameters sslParams;
+ private final SelectorManager selmgr;
+ private final FilterFactory filters;
+ private final Http2ClientImpl client2;
+ private final long id;
+ private final String dbgTag;
+
+ // This reference is used to keep track of the facade HttpClient
+ // that was returned to the application code.
+ // It makes it possible to know when the application no longer
+ // holds any reference to the HttpClient.
+ // Unfortunately, this information is not enough to know when
+ // to exit the SelectorManager thread. Because of the asynchronous
+ // nature of the API, we also need to wait until all pending operations
+ // have completed.
+ private final WeakReference<HttpClientFacade> facadeRef;
+
+ // This counter keeps track of the number of operations pending
+ // on the HttpClient. The SelectorManager thread will wait
+ // until there are no longer any pending operations and the
+ // facadeRef is cleared before exiting.
+ //
+ // The pendingOperationCount is incremented every time a send/sendAsync
+ // operation is invoked on the HttpClient, and is decremented when
+ // the HttpResponse<T> object is returned to the user.
+ // However, at this point, the body may not have been fully read yet.
+ // This is the case when the response T is implemented as a streaming
+ // subscriber (such as an InputStream).
+ //
+ // To take care of this issue the pendingOperationCount will additionally
+ // be incremented/decremented in the following cases:
+ //
+ // 1. For HTTP/2 it is incremented when a stream is added to the
+ // Http2Connection streams map, and decreased when the stream is removed
+ // from the map. This should also take care of push promises.
+ // 2. For WebSocket the count is increased when creating a
+ // DetachedConnectionChannel for the socket, and decreased
+ // when the the channel is closed.
+ // In addition, the HttpClient facade is passed to the WebSocket builder,
+ // (instead of the client implementation delegate).
+ // 3. For HTTP/1.1 the count is incremented before starting to parse the body
+ // response, and decremented when the parser has reached the end of the
+ // response body flow.
+ //
+ // This should ensure that the selector manager thread remains alive until
+ // the response has been fully received or the web socket is closed.
+ private final AtomicLong pendingOperationCount = new AtomicLong();
+ private final AtomicLong pendingWebSocketCount = new AtomicLong();
+ private final AtomicLong pendingHttpRequestCount = new AtomicLong();
+
+ /** A Set of, deadline first, ordered timeout events. */
+ private final TreeSet<TimeoutEvent> timeouts;
+
+ /**
+ * This is a bit tricky:
+ * 1. an HttpClientFacade has a final HttpClientImpl field.
+ * 2. an HttpClientImpl has a final WeakReference<HttpClientFacade> field,
+ * where the referent is the facade created for that instance.
+ * 3. We cannot just create the HttpClientFacade in the HttpClientImpl
+ * constructor, because it would be only weakly referenced and could
+ * be GC'ed before we can return it.
+ * The solution is to use an instance of SingleFacadeFactory which will
+ * allow the caller of new HttpClientImpl(...) to retrieve the facade
+ * after the HttpClientImpl has been created.
+ */
+ private static final class SingleFacadeFactory {
+ HttpClientFacade facade;
+ HttpClientFacade createFacade(HttpClientImpl impl) {
+ assert facade == null;
+ return (facade = new HttpClientFacade(impl));
+ }
+ }
+
+ static HttpClientFacade create(HttpClientBuilderImpl builder) {
+ SingleFacadeFactory facadeFactory = new SingleFacadeFactory();
+ HttpClientImpl impl = new HttpClientImpl(builder, facadeFactory);
+ impl.start();
+ assert facadeFactory.facade != null;
+ assert impl.facadeRef.get() == facadeFactory.facade;
+ return facadeFactory.facade;
+ }
+
+ private HttpClientImpl(HttpClientBuilderImpl builder,
+ SingleFacadeFactory facadeFactory) {
+ id = CLIENT_IDS.incrementAndGet();
+ dbgTag = "HttpClientImpl(" + id +")";
+ if (builder.sslContext == null) {
+ try {
+ sslContext = SSLContext.getDefault();
+ } catch (NoSuchAlgorithmException ex) {
+ throw new InternalError(ex);
+ }
+ } else {
+ sslContext = builder.sslContext;
+ }
+ Executor ex = builder.executor;
+ if (ex == null) {
+ ex = Executors.newCachedThreadPool(new DefaultThreadFactory(id));
+ isDefaultExecutor = true;
+ } else {
+ ex = builder.executor;
+ isDefaultExecutor = false;
+ }
+ facadeRef = new WeakReference<>(facadeFactory.createFacade(this));
+ client2 = new Http2ClientImpl(this);
+ executor = ex;
+ cookieHandler = builder.cookieHandler;
+ followRedirects = builder.followRedirects == null ?
+ Redirect.NEVER : builder.followRedirects;
+ this.userProxySelector = Optional.ofNullable(builder.proxy);
+ this.proxySelector = userProxySelector
+ .orElseGet(HttpClientImpl::getDefaultProxySelector);
+ debug.log(Level.DEBUG, "proxySelector is %s (user-supplied=%s)",
+ this.proxySelector, userProxySelector.isPresent());
+ authenticator = builder.authenticator;
+ if (builder.version == null) {
+ version = HttpClient.Version.HTTP_2;
+ } else {
+ version = builder.version;
+ }
+ if (builder.sslParams == null) {
+ sslParams = getDefaultParams(sslContext);
+ } else {
+ sslParams = builder.sslParams;
+ }
+ connections = new ConnectionPool(id);
+ connections.start();
+ timeouts = new TreeSet<>();
+ try {
+ selmgr = new SelectorManager(this);
+ } catch (IOException e) {
+ // unlikely
+ throw new InternalError(e);
+ }
+ selmgr.setDaemon(true);
+ filters = new FilterFactory();
+ initFilters();
+ assert facadeRef.get() != null;
+ }
+
+ private void start() {
+ selmgr.start();
+ }
+
+ // Called from the SelectorManager thread, just before exiting.
+ // Clears the HTTP/1.1 and HTTP/2 cache, ensuring that the connections
+ // that may be still lingering there are properly closed (and their
+ // possibly still opened SocketChannel released).
+ private void stop() {
+ // Clears HTTP/1.1 cache and close its connections
+ connections.stop();
+ // Clears HTTP/2 cache and close its connections.
+ client2.stop();
+ }
+
+ private static SSLParameters getDefaultParams(SSLContext ctx) {
+ SSLParameters params = ctx.getSupportedSSLParameters();
+ params.setProtocols(new String[]{"TLSv1.2"});
+ return params;
+ }
+
+ private static ProxySelector getDefaultProxySelector() {
+ PrivilegedAction<ProxySelector> action = ProxySelector::getDefault;
+ return AccessController.doPrivileged(action);
+ }
+
+ // Returns the facade that was returned to the application code.
+ // May be null if that facade is no longer referenced.
+ final HttpClientFacade facade() {
+ return facadeRef.get();
+ }
+
+ // Increments the pendingOperationCount.
+ final long reference() {
+ pendingHttpRequestCount.incrementAndGet();
+ return pendingOperationCount.incrementAndGet();
+ }
+
+ // Decrements the pendingOperationCount.
+ final long unreference() {
+ final long count = pendingOperationCount.decrementAndGet();
+ final long httpCount = pendingHttpRequestCount.decrementAndGet();
+ final long webSocketCount = pendingWebSocketCount.get();
+ if (count == 0 && facade() == null) {
+ selmgr.wakeupSelector();
+ }
+ assert httpCount >= 0 : "count of HTTP operations < 0";
+ assert webSocketCount >= 0 : "count of WS operations < 0";
+ assert count >= 0 : "count of pending operations < 0";
+ return count;
+ }
+
+ // Increments the pendingOperationCount.
+ final long webSocketOpen() {
+ pendingWebSocketCount.incrementAndGet();
+ return pendingOperationCount.incrementAndGet();
+ }
+
+ // Decrements the pendingOperationCount.
+ final long webSocketClose() {
+ final long count = pendingOperationCount.decrementAndGet();
+ final long webSocketCount = pendingWebSocketCount.decrementAndGet();
+ final long httpCount = pendingHttpRequestCount.get();
+ if (count == 0 && facade() == null) {
+ selmgr.wakeupSelector();
+ }
+ assert httpCount >= 0 : "count of HTTP operations < 0";
+ assert webSocketCount >= 0 : "count of WS operations < 0";
+ assert count >= 0 : "count of pending operations < 0";
+ return count;
+ }
+
+ // Returns the pendingOperationCount.
+ final long referenceCount() {
+ return pendingOperationCount.get();
+ }
+
+ // Called by the SelectorManager thread to figure out whether it's time
+ // to terminate.
+ final boolean isReferenced() {
+ HttpClient facade = facade();
+ return facade != null || referenceCount() > 0;
+ }
+
+ /**
+ * Wait for activity on given exchange.
+ * The following occurs in the SelectorManager thread.
+ *
+ * 1) add to selector
+ * 2) If selector fires for this exchange then
+ * call AsyncEvent.handle()
+ *
+ * If exchange needs to change interest ops, then call registerEvent() again.
+ */
+ void registerEvent(AsyncEvent exchange) throws IOException {
+ selmgr.register(exchange);
+ }
+
+ /**
+ * Only used from RawChannel to disconnect the channel from
+ * the selector
+ */
+ void cancelRegistration(SocketChannel s) {
+ selmgr.cancel(s);
+ }
+
+ /**
+ * Allows an AsyncEvent to modify its interestOps.
+ * @param event The modified event.
+ */
+ void eventUpdated(AsyncEvent event) throws ClosedChannelException {
+ assert !(event instanceof AsyncTriggerEvent);
+ selmgr.eventUpdated(event);
+ }
+
+ boolean isSelectorThread() {
+ return Thread.currentThread() == selmgr;
+ }
+
+ Http2ClientImpl client2() {
+ return client2;
+ }
+
+ private void debugCompleted(String tag, long startNanos, HttpRequest req) {
+ if (debugelapsed.isLoggable(Level.DEBUG)) {
+ debugelapsed.log(Level.DEBUG, () -> tag + " elapsed "
+ + (System.nanoTime() - startNanos)/1000_000L
+ + " millis for " + req.method()
+ + " to " + req.uri());
+ }
+ }
+
+ @Override
+ public <T> HttpResponse<T>
+ send(HttpRequest req, BodyHandler<T> responseHandler)
+ throws IOException, InterruptedException
+ {
+ try {
+ return sendAsync(req, responseHandler).get();
+ } catch (ExecutionException e) {
+ Throwable t = e.getCause();
+ if (t instanceof Error)
+ throw (Error)t;
+ if (t instanceof RuntimeException)
+ throw (RuntimeException)t;
+ else if (t instanceof IOException)
+ throw Utils.getIOException(t);
+ else
+ throw new InternalError("Unexpected exception", t);
+ }
+ }
+
+ @Override
+ public <T> CompletableFuture<HttpResponse<T>>
+ sendAsync(HttpRequest userRequest, BodyHandler<T> responseHandler)
+ {
+ return sendAsync(userRequest, responseHandler, null);
+ }
+
+
+ @Override
+ public <T> CompletableFuture<HttpResponse<T>>
+ sendAsync(HttpRequest userRequest,
+ BodyHandler<T> responseHandler,
+ PushPromiseHandler<T> pushPromiseHandler)
+ {
+ AccessControlContext acc = null;
+ if (System.getSecurityManager() != null)
+ acc = AccessController.getContext();
+
+ // Clone the, possibly untrusted, HttpRequest
+ HttpRequestImpl requestImpl = new HttpRequestImpl(userRequest, proxySelector, acc);
+ if (requestImpl.method().equals("CONNECT"))
+ throw new IllegalArgumentException("Unsupported method CONNECT");
+
+ long start = DEBUGELAPSED ? System.nanoTime() : 0;
+ reference();
+ try {
+ debugelapsed.log(Level.DEBUG, "ClientImpl (async) send %s", userRequest);
+
+ MultiExchange<T> mex = new MultiExchange<>(userRequest,
+ requestImpl,
+ this,
+ responseHandler,
+ pushPromiseHandler,
+ acc);
+ CompletableFuture<HttpResponse<T>> res =
+ mex.responseAsync().whenComplete((b,t) -> unreference());
+ if (DEBUGELAPSED) {
+ res = res.whenComplete(
+ (b,t) -> debugCompleted("ClientImpl (async)", start, userRequest));
+ }
+ // makes sure that any dependent actions happen in the executor
+ if (acc != null) {
+ res.whenCompleteAsync((r, t) -> { /* do nothing */},
+ new PrivilegedExecutor(executor, acc));
+ }
+
+ return res;
+ } catch(Throwable t) {
+ unreference();
+ debugCompleted("ClientImpl (async)", start, userRequest);
+ throw t;
+ }
+ }
+
+ // Main loop for this client's selector
+ private final static class SelectorManager extends Thread {
+
+ // For testing purposes we have an internal System property that
+ // can control the frequency at which the selector manager will wake
+ // up when there are no pending operations.
+ // Increasing the frequency (shorter delays) might allow the selector
+ // to observe that the facade is no longer referenced and might allow
+ // the selector thread to terminate more timely - for when nothing is
+ // ongoing it will only check for that condition every NODEADLINE ms.
+ // To avoid misuse of the property, the delay that can be specified
+ // is comprised between [MIN_NODEADLINE, MAX_NODEADLINE], and its default
+ // value if unspecified (or <= 0) is DEF_NODEADLINE = 3000ms
+ // The property is -Djdk.httpclient.internal.selector.timeout=<millis>
+ private static final int MIN_NODEADLINE = 1000; // ms
+ private static final int MAX_NODEADLINE = 1000 * 1200; // ms
+ private static final int DEF_NODEADLINE = 3000; // ms
+ private static final long NODEADLINE; // default is DEF_NODEADLINE ms
+ static {
+ // ensure NODEADLINE is initialized with some valid value.
+ long deadline = Utils.getIntegerNetProperty(
+ "jdk.httpclient.internal.selector.timeout",
+ DEF_NODEADLINE); // millis
+ if (deadline <= 0) deadline = DEF_NODEADLINE;
+ deadline = Math.max(deadline, MIN_NODEADLINE);
+ NODEADLINE = Math.min(deadline, MAX_NODEADLINE);
+ }
+
+ private final Selector selector;
+ private volatile boolean closed;
+ private final List<AsyncEvent> registrations;
+ private final System.Logger debug;
+ private final System.Logger debugtimeout;
+ HttpClientImpl owner;
+ ConnectionPool pool;
+
+ SelectorManager(HttpClientImpl ref) throws IOException {
+ super(null, null, "HttpClient-" + ref.id + "-SelectorManager", 0, false);
+ owner = ref;
+ debug = ref.debug;
+ debugtimeout = ref.debugtimeout;
+ pool = ref.connectionPool();
+ registrations = new ArrayList<>();
+ selector = Selector.open();
+ }
+
+ void eventUpdated(AsyncEvent e) throws ClosedChannelException {
+ if (Thread.currentThread() == this) {
+ SelectionKey key = e.channel().keyFor(selector);
+ SelectorAttachment sa = (SelectorAttachment) key.attachment();
+ if (sa != null) sa.register(e);
+ } else {
+ register(e);
+ }
+ }
+
+ // This returns immediately. So caller not allowed to send/receive
+ // on connection.
+ synchronized void register(AsyncEvent e) {
+ registrations.add(e);
+ selector.wakeup();
+ }
+
+ synchronized void cancel(SocketChannel e) {
+ SelectionKey key = e.keyFor(selector);
+ if (key != null) {
+ key.cancel();
+ }
+ selector.wakeup();
+ }
+
+ void wakeupSelector() {
+ selector.wakeup();
+ }
+
+ synchronized void shutdown() {
+ debug.log(Level.DEBUG, "SelectorManager shutting down");
+ closed = true;
+ try {
+ selector.close();
+ } catch (IOException ignored) {
+ } finally {
+ owner.stop();
+ }
+ }
+
+ @Override
+ public void run() {
+ List<Pair<AsyncEvent,IOException>> errorList = new ArrayList<>();
+ List<AsyncEvent> readyList = new ArrayList<>();
+ try {
+ while (!Thread.currentThread().isInterrupted()) {
+ synchronized (this) {
+ assert errorList.isEmpty();
+ assert readyList.isEmpty();
+ for (AsyncEvent event : registrations) {
+ if (event instanceof AsyncTriggerEvent) {
+ readyList.add(event);
+ continue;
+ }
+ SelectableChannel chan = event.channel();
+ SelectionKey key = null;
+ try {
+ key = chan.keyFor(selector);
+ SelectorAttachment sa;
+ if (key == null || !key.isValid()) {
+ if (key != null) {
+ // key is canceled.
+ // invoke selectNow() to purge it
+ // before registering the new event.
+ selector.selectNow();
+ }
+ sa = new SelectorAttachment(chan, selector);
+ } else {
+ sa = (SelectorAttachment) key.attachment();
+ }
+ // may throw IOE if channel closed: that's OK
+ sa.register(event);
+ if (!chan.isOpen()) {
+ throw new IOException("Channel closed");
+ }
+ } catch (IOException e) {
+ Log.logTrace("HttpClientImpl: " + e);
+ debug.log(Level.DEBUG, () ->
+ "Got " + e.getClass().getName()
+ + " while handling"
+ + " registration events");
+ chan.close();
+ // let the event abort deal with it
+ errorList.add(new Pair<>(event, e));
+ if (key != null) {
+ key.cancel();
+ selector.selectNow();
+ }
+ }
+ }
+ registrations.clear();
+ selector.selectedKeys().clear();
+ }
+
+ for (AsyncEvent event : readyList) {
+ assert event instanceof AsyncTriggerEvent;
+ event.handle();
+ }
+ readyList.clear();
+
+ for (Pair<AsyncEvent,IOException> error : errorList) {
+ // an IOException was raised and the channel closed.
+ handleEvent(error.first, error.second);
+ }
+ errorList.clear();
+
+ // Check whether client is still alive, and if not,
+ // gracefully stop this thread
+ if (!owner.isReferenced()) {
+ Log.logTrace("HttpClient no longer referenced. Exiting...");
+ return;
+ }
+
+ // Timeouts will have milliseconds granularity. It is important
+ // to handle them in a timely fashion.
+ long nextTimeout = owner.purgeTimeoutsAndReturnNextDeadline();
+ debugtimeout.log(Level.DEBUG, "next timeout: %d", nextTimeout);
+
+ // Keep-alive have seconds granularity. It's not really an
+ // issue if we keep connections linger a bit more in the keep
+ // alive cache.
+ long nextExpiry = pool.purgeExpiredConnectionsAndReturnNextDeadline();
+ debugtimeout.log(Level.DEBUG, "next expired: %d", nextExpiry);
+
+ assert nextTimeout >= 0;
+ assert nextExpiry >= 0;
+
+ // Don't wait for ever as it might prevent the thread to
+ // stop gracefully. millis will be 0 if no deadline was found.
+ if (nextTimeout <= 0) nextTimeout = NODEADLINE;
+
+ // Clip nextExpiry at NODEADLINE limit. The default
+ // keep alive is 1200 seconds (half an hour) - we don't
+ // want to wait that long.
+ if (nextExpiry <= 0) nextExpiry = NODEADLINE;
+ else nextExpiry = Math.min(NODEADLINE, nextExpiry);
+
+ // takes the least of the two.
+ long millis = Math.min(nextExpiry, nextTimeout);
+
+ debugtimeout.log(Level.DEBUG, "Next deadline is %d",
+ (millis == 0 ? NODEADLINE : millis));
+ //debugPrint(selector);
+ int n = selector.select(millis == 0 ? NODEADLINE : millis);
+ if (n == 0) {
+ // Check whether client is still alive, and if not,
+ // gracefully stop this thread
+ if (!owner.isReferenced()) {
+ Log.logTrace("HttpClient no longer referenced. Exiting...");
+ return;
+ }
+ owner.purgeTimeoutsAndReturnNextDeadline();
+ continue;
+ }
+ Set<SelectionKey> keys = selector.selectedKeys();
+
+ assert errorList.isEmpty();
+ for (SelectionKey key : keys) {
+ SelectorAttachment sa = (SelectorAttachment) key.attachment();
+ if (!key.isValid()) {
+ IOException ex = sa.chan.isOpen()
+ ? new IOException("Invalid key")
+ : new ClosedChannelException();
+ sa.pending.forEach(e -> errorList.add(new Pair<>(e,ex)));
+ sa.pending.clear();
+ continue;
+ }
+
+ int eventsOccurred;
+ try {
+ eventsOccurred = key.readyOps();
+ } catch (CancelledKeyException ex) {
+ IOException io = Utils.getIOException(ex);
+ sa.pending.forEach(e -> errorList.add(new Pair<>(e,io)));
+ sa.pending.clear();
+ continue;
+ }
+ sa.events(eventsOccurred).forEach(readyList::add);
+ sa.resetInterestOps(eventsOccurred);
+ }
+ selector.selectNow(); // complete cancellation
+ selector.selectedKeys().clear();
+
+ for (AsyncEvent event : readyList) {
+ handleEvent(event, null); // will be delegated to executor
+ }
+ readyList.clear();
+ errorList.forEach((p) -> handleEvent(p.first, p.second));
+ errorList.clear();
+ }
+ } catch (Throwable e) {
+ //e.printStackTrace();
+ if (!closed) {
+ // This terminates thread. So, better just print stack trace
+ String err = Utils.stackTrace(e);
+ Log.logError("HttpClientImpl: fatal error: " + err);
+ }
+ debug.log(Level.DEBUG, "shutting down", e);
+ if (Utils.ASSERTIONSENABLED && !debug.isLoggable(Level.DEBUG)) {
+ e.printStackTrace(System.err); // always print the stack
+ }
+ } finally {
+ shutdown();
+ }
+ }
+
+// void debugPrint(Selector selector) {
+// System.err.println("Selector: debugprint start");
+// Set<SelectionKey> keys = selector.keys();
+// for (SelectionKey key : keys) {
+// SelectableChannel c = key.channel();
+// int ops = key.interestOps();
+// System.err.printf("selector chan:%s ops:%d\n", c, ops);
+// }
+// System.err.println("Selector: debugprint end");
+// }
+
+ /** Handles the given event. The given ioe may be null. */
+ void handleEvent(AsyncEvent event, IOException ioe) {
+ if (closed || ioe != null) {
+ event.abort(ioe);
+ } else {
+ event.handle();
+ }
+ }
+ }
+
+ /**
+ * Tracks multiple user level registrations associated with one NIO
+ * registration (SelectionKey). In this implementation, registrations
+ * are one-off and when an event is posted the registration is cancelled
+ * until explicitly registered again.
+ *
+ * <p> No external synchronization required as this class is only used
+ * by the SelectorManager thread. One of these objects required per
+ * connection.
+ */
+ private static class SelectorAttachment {
+ private final SelectableChannel chan;
+ private final Selector selector;
+ private final Set<AsyncEvent> pending;
+ private final static System.Logger debug =
+ Utils.getDebugLogger("SelectorAttachment"::toString, DEBUG);
+ private int interestOps;
+
+ SelectorAttachment(SelectableChannel chan, Selector selector) {
+ this.pending = new HashSet<>();
+ this.chan = chan;
+ this.selector = selector;
+ }
+
+ void register(AsyncEvent e) throws ClosedChannelException {
+ int newOps = e.interestOps();
+ boolean reRegister = (interestOps & newOps) != newOps;
+ interestOps |= newOps;
+ pending.add(e);
+ if (reRegister) {
+ // first time registration happens here also
+ chan.register(selector, interestOps, this);
+ }
+ }
+
+ /**
+ * Returns a Stream<AsyncEvents> containing only events that are
+ * registered with the given {@code interestOps}.
+ */
+ Stream<AsyncEvent> events(int interestOps) {
+ return pending.stream()
+ .filter(ev -> (ev.interestOps() & interestOps) != 0);
+ }
+
+ /**
+ * Removes any events with the given {@code interestOps}, and if no
+ * events remaining, cancels the associated SelectionKey.
+ */
+ void resetInterestOps(int interestOps) {
+ int newOps = 0;
+
+ Iterator<AsyncEvent> itr = pending.iterator();
+ while (itr.hasNext()) {
+ AsyncEvent event = itr.next();
+ int evops = event.interestOps();
+ if (event.repeating()) {
+ newOps |= evops;
+ continue;
+ }
+ if ((evops & interestOps) != 0) {
+ itr.remove();
+ } else {
+ newOps |= evops;
+ }
+ }
+
+ this.interestOps = newOps;
+ SelectionKey key = chan.keyFor(selector);
+ if (newOps == 0 && pending.isEmpty()) {
+ key.cancel();
+ } else {
+ try {
+ key.interestOps(newOps);
+ } catch (CancelledKeyException x) {
+ // channel may have been closed
+ debug.log(Level.DEBUG, "key cancelled for " + chan);
+ abortPending(x);
+ }
+ }
+ }
+
+ void abortPending(Throwable x) {
+ if (!pending.isEmpty()) {
+ AsyncEvent[] evts = pending.toArray(new AsyncEvent[0]);
+ pending.clear();
+ IOException io = Utils.getIOException(x);
+ for (AsyncEvent event : evts) {
+ event.abort(io);
+ }
+ }
+ }
+ }
+
+ /*package-private*/ SSLContext theSSLContext() {
+ return sslContext;
+ }
+
+ @Override
+ public SSLContext sslContext() {
+ return sslContext;
+ }
+
+ @Override
+ public SSLParameters sslParameters() {
+ return Utils.copySSLParameters(sslParams);
+ }
+
+ @Override
+ public Optional<Authenticator> authenticator() {
+ return Optional.ofNullable(authenticator);
+ }
+
+ /*package-private*/ final Executor theExecutor() {
+ return executor;
+ }
+
+ @Override
+ public final Optional<Executor> executor() {
+ return isDefaultExecutor ? Optional.empty() : Optional.of(executor);
+ }
+
+ ConnectionPool connectionPool() {
+ return connections;
+ }
+
+ @Override
+ public Redirect followRedirects() {
+ return followRedirects;
+ }
+
+
+ @Override
+ public Optional<CookieHandler> cookieHandler() {
+ return Optional.ofNullable(cookieHandler);
+ }
+
+ @Override
+ public Optional<ProxySelector> proxy() {
+ return this.userProxySelector;
+ }
+
+ // Return the effective proxy that this client uses.
+ ProxySelector proxySelector() {
+ return proxySelector;
+ }
+
+ @Override
+ public WebSocket.Builder newWebSocketBuilder() {
+ // Make sure to pass the HttpClientFacade to the WebSocket builder.
+ // This will ensure that the facade is not released before the
+ // WebSocket has been created, at which point the pendingOperationCount
+ // will have been incremented by the DetachedConnectionChannel
+ // (see PlainHttpConnection.detachChannel())
+ return new BuilderImpl(this.facade(), proxySelector);
+ }
+
+ @Override
+ public Version version() {
+ return version;
+ }
+
+ String dbgString() {
+ return dbgTag;
+ }
+
+ @Override
+ public String toString() {
+ // Used by tests to get the client's id and compute the
+ // name of the SelectorManager thread.
+ return super.toString() + ("(" + id + ")");
+ }
+
+ private void initFilters() {
+ addFilter(AuthenticationFilter.class);
+ addFilter(RedirectFilter.class);
+ if (this.cookieHandler != null) {
+ addFilter(CookieFilter.class);
+ }
+ }
+
+ private void addFilter(Class<? extends HeaderFilter> f) {
+ filters.addFilter(f);
+ }
+
+ final List<HeaderFilter> filterChain() {
+ return filters.getFilterChain();
+ }
+
+ // Timer controls.
+ // Timers are implemented through timed Selector.select() calls.
+
+ synchronized void registerTimer(TimeoutEvent event) {
+ Log.logTrace("Registering timer {0}", event);
+ timeouts.add(event);
+ selmgr.wakeupSelector();
+ }
+
+ synchronized void cancelTimer(TimeoutEvent event) {
+ Log.logTrace("Canceling timer {0}", event);
+ timeouts.remove(event);
+ }
+
+ /**
+ * Purges ( handles ) timer events that have passed their deadline, and
+ * returns the amount of time, in milliseconds, until the next earliest
+ * event. A return value of 0 means that there are no events.
+ */
+ private long purgeTimeoutsAndReturnNextDeadline() {
+ long diff = 0L;
+ List<TimeoutEvent> toHandle = null;
+ int remaining = 0;
+ // enter critical section to retrieve the timeout event to handle
+ synchronized(this) {
+ if (timeouts.isEmpty()) return 0L;
+
+ Instant now = Instant.now();
+ Iterator<TimeoutEvent> itr = timeouts.iterator();
+ while (itr.hasNext()) {
+ TimeoutEvent event = itr.next();
+ diff = now.until(event.deadline(), ChronoUnit.MILLIS);
+ if (diff <= 0) {
+ itr.remove();
+ toHandle = (toHandle == null) ? new ArrayList<>() : toHandle;
+ toHandle.add(event);
+ } else {
+ break;
+ }
+ }
+ remaining = timeouts.size();
+ }
+
+ // can be useful for debugging
+ if (toHandle != null && Log.trace()) {
+ Log.logTrace("purgeTimeoutsAndReturnNextDeadline: handling "
+ + toHandle.size() + " events, "
+ + "remaining " + remaining
+ + ", next deadline: " + (diff < 0 ? 0L : diff));
+ }
+
+ // handle timeout events out of critical section
+ if (toHandle != null) {
+ Throwable failed = null;
+ for (TimeoutEvent event : toHandle) {
+ try {
+ Log.logTrace("Firing timer {0}", event);
+ event.handle();
+ } catch (Error | RuntimeException e) {
+ // Not expected. Handle remaining events then throw...
+ // If e is an OOME or SOE it might simply trigger a new
+ // error from here - but in this case there's not much we
+ // could do anyway. Just let it flow...
+ if (failed == null) failed = e;
+ else failed.addSuppressed(e);
+ Log.logTrace("Failed to handle event {0}: {1}", event, e);
+ }
+ }
+ if (failed instanceof Error) throw (Error) failed;
+ if (failed instanceof RuntimeException) throw (RuntimeException) failed;
+ }
+
+ // return time to wait until next event. 0L if there's no more events.
+ return diff < 0 ? 0L : diff;
+ }
+
+ // used for the connection window
+ int getReceiveBufferSize() {
+ return Utils.getIntegerNetProperty(
+ "jdk.httpclient.receiveBufferSize", 2 * 1024 * 1024
+ );
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/HttpConnection.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,493 @@
+/*
+ * 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.incubator.http.internal;
+
+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 jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpClient.Version;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.DeferredCompleter;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.Utils;
+import static jdk.incubator.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 {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ final static System.Logger DEBUG_LOGGER = Utils.getDebugLogger(
+ () -> "HttpConnection(SocketTube(?))", 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. */
+ 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) {
+ HttpConnection c = null;
+ InetSocketAddress proxy = request.proxy();
+ if (proxy != null && proxy.isUnresolved()) {
+ // The default proxy selector may select a proxy whose address is
+ // unresolved. We must resolve the address before connecting to it.
+ proxy = new InetSocketAddress(proxy.getHostString(), proxy.getPort());
+ }
+ 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;
+ DEBUG_LOGGER.log(Level.DEBUG, () -> 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;
+ DEBUG_LOGGER.log(Level.DEBUG, () -> 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 void shutdownInput() throws IOException;
+
+ abstract void shutdownOutput() throws IOException;
+
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ abstract static class DetachedConnectionChannel implements Closeable {
+ DetachedConnectionChannel() {}
+ abstract SocketChannel channel();
+ abstract long write(ByteBuffer[] buffers, int start, int number)
+ throws IOException;
+ abstract void shutdownInput() throws IOException;
+ abstract void shutdownOutput() throws IOException;
+ abstract ByteBuffer read() throws IOException;
+ @Override
+ public abstract void close();
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName() + ": " + channel().toString();
+ }
+ }
+
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ abstract DetachedConnectionChannel detachChannel();
+
+ 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<>();
+ 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);
+ debug.log(Level.DEBUG, () -> "HttpPublisher: got request of "
+ + n + " from "
+ + getConnectionFlow());
+ writeScheduler.runOrSchedule();
+ }
+
+ @Override
+ public void cancel() {
+ debug.log(Level.DEBUG, () -> "HttpPublisher: cancelled by "
+ + getConnectionFlow());
+ }
+
+ void flush() {
+ while (!queue.isEmpty() && demand.tryDecrement()) {
+ List<ByteBuffer> elem = queue.poll();
+ debug.log(Level.DEBUG, () -> "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(Level.DEBUG, "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();
+ queue.addFirst(buffers);
+ debug.log(Level.DEBUG, "inserted %d bytes in the write queue", bytes);
+ }
+
+ @Override
+ public void signalEnqueued() throws IOException {
+ debug.log(Level.DEBUG, "signalling the publisher of the write queue");
+ signal();
+ }
+ }
+
+ String dbgTag = null;
+ 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();
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/HttpRequestBuilderImpl.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,229 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.Optional;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpRequest.BodyPublisher;
+import jdk.incubator.http.internal.common.HttpHeadersImpl;
+import jdk.incubator.http.internal.common.Utils;
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+import static jdk.incubator.http.internal.common.Utils.isValidName;
+import static jdk.incubator.http.internal.common.Utils.isValidValue;
+
+public class HttpRequestBuilderImpl extends 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;
+ }
+
+ private static IllegalArgumentException newIAE(String message, Object... args) {
+ return new IllegalArgumentException(format(message, args));
+ }
+
+ private static void checkURI(URI uri) {
+ String scheme = uri.getScheme();
+ if (scheme == null)
+ throw newIAE("URI with undefined scheme");
+ scheme = scheme.toLowerCase();
+ 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(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(BodyPublisher body) {
+ return method0("DELETE", requireNonNull(body));
+ }
+
+ @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");
+ return method0(method, requireNonNull(body));
+ }
+
+ private HttpRequest.Builder method0(String method, BodyPublisher body) {
+ assert method != null;
+ assert !method.equals("GET") ? body != null : true;
+ 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; }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/HttpRequestImpl.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,333 @@
+/*
+ * 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.incubator.http.internal;
+
+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.Optional;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.internal.common.HttpHeadersImpl;
+import jdk.incubator.http.internal.websocket.WebSocketRequest;
+
+import static jdk.incubator.http.internal.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, AccessControlContext acc) {
+ String method = request.method();
+ this.method = method == null ? "GET" : method;
+ this.userHeaders = request.headers();
+ if (request instanceof HttpRequestImpl) {
+ this.systemHeaders = ((HttpRequestImpl) request).systemHeaders;
+ this.isWebSocket = ((HttpRequestImpl) request).isWebSocket;
+ } else {
+ this.systemHeaders = new HttpHeadersImpl();
+ }
+ this.systemHeaders.setHeader("User-Agent", USER_AGENT);
+ this.uri = request.uri();
+ 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);
+ if (acc != null && requestPublisher instanceof RequestPublishers.FilePublisher) {
+ // Restricts the file publisher with the senders ACC, if any
+ ((RequestPublishers.FilePublisher)requestPublisher).setAccessControlContext(acc);
+ }
+ this.timeout = request.timeout().orElse(null);
+ this.version = request.version();
+ this.authority = null;
+ }
+
+ /** Creates a HttpRequestImpl using fields of an existing request impl. */
+ public HttpRequestImpl(URI uri,
+ String method,
+ HttpRequestImpl other) {
+ this.method = method == null? "GET" : method;
+ this.userHeaders = other.userHeaders;
+ this.isWebSocket = other.isWebSocket;
+ this.systemHeaders = other.systemHeaders;
+ 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);
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/HttpResponseImpl.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+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.SSLParameters;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.internal.websocket.RawChannel;
+
+/**
+ * The implementation class for HttpResponse
+ */
+class HttpResponseImpl<T> extends HttpResponse<T> implements RawChannel.Provider {
+
+ final int responseCode;
+ final Exchange<T> exchange;
+ final HttpRequest initialRequest;
+ final Optional<HttpResponse<T>> previousResponse;
+ final HttpHeaders headers;
+ final SSLParameters sslParameters;
+ 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.sslParameters = exch.client().sslParameters();
+ 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 SSLParameters sslParameters() {
+ return sslParameters;
+ }
+
+ @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 RawChannelImpl(exchange.client(), 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();
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/ImmutableHeaders.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.BiPredicate;
+import java.util.function.Predicate;
+import jdk.incubator.http.HttpHeaders;
+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());
+ }
+
+ 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()
+ .filter(e -> headerAllowed.test(e.getKey(), e.getValue()))
+ .forEach(e ->
+ {
+ List<String> values = new ArrayList<>(e.getValue());
+ m.put(e.getKey(), unmodifiableList(values));
+ }
+ );
+ this.map = unmodifiableMap(m);
+ }
+
+ 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;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/MultiExchange.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,326 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.time.Duration;
+import java.util.List;
+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 jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.HttpResponse.PushPromiseHandler;
+import jdk.incubator.http.HttpTimeoutException;
+import jdk.incubator.http.internal.UntrustedBodyHandler;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import jdk.incubator.http.internal.common.ConnectionExpiredException;
+import jdk.incubator.http.internal.common.Utils;
+import static jdk.incubator.http.internal.common.MinimalFuture.completedFuture;
+import static jdk.incubator.http.internal.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 boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ static final System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("MultiExchange"::toString, 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 async only
+ 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 List<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.client = client;
+ this.filters = client.filterChain();
+ this.acc = acc;
+ this.executor = client.theExecutor();
+ this.responseHandler = responseHandler;
+ if (acc != null) {
+ // Restricts the file publisher with the senders ACC, if any
+ if (responseHandler instanceof UntrustedBodyHandler)
+ ((UntrustedBodyHandler)this.responseHandler).setAccessControlContext(acc);
+ }
+
+ if (pushPromiseHandler != null) {
+ this.pushGroup = new PushGroup<>(pushPromiseHandler, request, acc);
+ } 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");
+ for (HeaderFilter filter : filters) {
+ 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() {
+// cancelled = true;
+// getExchange().cancel();
+// }
+
+ 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<>(userRequest, 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
+ 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) -> {
+ 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;
+ }
+
+ /**
+ * 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) {
+ // allow the retry mechanism to do its work
+ // ####: method (GET,HEAD, not POST?), no bytes written or read ( differentiate? )
+ if (t.getCause() != null) retryCause = t.getCause();
+ if (!expiredOnce) {
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "MultiExchange: ConnectionExpiredException (async): retrying...",
+ t);
+ expiredOnce = true;
+ return null;
+ } else {
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "MultiExchange: 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() {
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Cancelling MultiExchange due to timeout for request %s",
+ request);
+ cancel(new HttpTimeoutException("request timed out"));
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/PlainHttpConnection.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,313 @@
+/*
+ * 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.incubator.http.internal;
+
+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.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import jdk.incubator.http.internal.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 FlowTube tube;
+ 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";
+ debug.log(Level.DEBUG, "ConnectEvent: finishing connect");
+ boolean finished = chan.finishConnect();
+ assert finished : "Expected channel to be connected";
+ debug.log(Level.DEBUG,
+ "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(address);
+ try {
+ finished = AccessController.doPrivileged(pa);
+ } catch (PrivilegedActionException e) {
+ cf.completeExceptionally(e.getCause());
+ }
+ if (finished) {
+ debug.log(Level.DEBUG, "connect finished without blocking");
+ connected = true;
+ cf.complete(null);
+ } else {
+ debug.log(Level.DEBUG, "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);
+ return true;
+ } catch(IOException x) {
+ debug.log(Level.DEBUG,
+ "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 synchronized void close() {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ try {
+ Log.logTrace("Closing: " + toString());
+ chan.close();
+ } catch (IOException e) {}
+ }
+
+ @Override
+ void shutdownInput() throws IOException {
+ debug.log(Level.DEBUG, "Shutting down input");
+ chan.shutdownInput();
+ }
+
+ @Override
+ void shutdownOutput() throws IOException {
+ debug.log(Level.DEBUG, "Shutting down output");
+ chan.shutdownOutput();
+ }
+
+ @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;
+ }
+
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ private static final class PlainDetachedChannel
+ extends DetachedConnectionChannel {
+ final PlainHttpConnection plainConnection;
+ boolean closed;
+ PlainDetachedChannel(PlainHttpConnection conn) {
+ // We're handing the connection channel over to a web socket.
+ // We need the selector manager's thread to stay alive until
+ // the WebSocket is closed.
+ conn.client().webSocketOpen();
+ this.plainConnection = conn;
+ }
+
+ @Override
+ SocketChannel channel() {
+ return plainConnection.channel();
+ }
+
+ @Override
+ ByteBuffer read() throws IOException {
+ ByteBuffer dst = ByteBuffer.allocate(8192);
+ int n = readImpl(dst);
+ if (n > 0) {
+ return dst;
+ } else if (n == 0) {
+ return Utils.EMPTY_BYTEBUFFER;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void close() {
+ HttpClientImpl client = plainConnection.client();
+ try {
+ plainConnection.close();
+ } finally {
+ // notify the HttpClientImpl that the websocket is no
+ // no longer operating.
+ synchronized(this) {
+ if (closed == true) return;
+ closed = true;
+ }
+ client.webSocketClose();
+ }
+ }
+
+ @Override
+ public long write(ByteBuffer[] buffers, int start, int number)
+ throws IOException
+ {
+ return channel().write(buffers, start, number);
+ }
+
+ @Override
+ public void shutdownInput() throws IOException {
+ plainConnection.shutdownInput();
+ }
+
+ @Override
+ public void shutdownOutput() throws IOException {
+ plainConnection.shutdownOutput();
+ }
+
+ private int readImpl(ByteBuffer buf) throws IOException {
+ int mark = buf.position();
+ int n;
+ n = channel().read(buf);
+ if (n == -1) {
+ return -1;
+ }
+ Utils.flipToMark(buf, mark);
+ return n;
+ }
+ }
+
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ @Override
+ DetachedConnectionChannel detachChannel() {
+ client().cancelRegistration(channel());
+ return new PlainDetachedChannel(this);
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/PlainProxyConnection.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,40 @@
+/*
+ * 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.incubator.http.internal;
+
+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);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/PlainTunnelingConnection.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,166 @@
+/*
+ * 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.incubator.http.internal;
+
+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 jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
+
+/**
+ * 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() {
+ debug.log(Level.DEBUG, "Connecting plain connection");
+ return delegate.connectAsync()
+ .thenCompose((Void v) -> {
+ debug.log(Level.DEBUG, "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, discard(null), null, null);
+ Exchange<Void> connectExchange = new Exchange<>(req, mulEx);
+
+ return connectExchange
+ .responseAsyncImpl(delegate)
+ .thenCompose((Response resp) -> {
+ CompletableFuture<Void> cf = new MinimalFuture<>();
+ debug.log(Level.DEBUG, "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
+ void shutdownInput() throws IOException {
+ delegate.shutdownInput();
+ }
+
+ @Override
+ void shutdownOutput() throws IOException {
+ delegate.shutdownOutput();
+ }
+
+ @Override
+ boolean isSecure() {
+ return false;
+ }
+
+ @Override
+ boolean isProxied() {
+ return true;
+ }
+
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ @Override
+ DetachedConnectionChannel detachChannel() {
+ return delegate.detachChannel();
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/PrivilegedExecutor.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+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));
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/ProxyAuthenticationRequired.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+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 ConnectionExpiredException} 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;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/PullPublisher.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+import java.util.Iterator;
+import java.util.concurrent.Flow;
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.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;
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/PushGroup.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,164 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.HttpResponse.BodyHandler;
+import jdk.incubator.http.HttpResponse.PushPromiseHandler;
+import jdk.incubator.http.internal.common.MinimalFuture;
+import jdk.incubator.http.internal.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 AccessControlContext acc;
+
+ int numberOfPushes;
+ int remainingPushes;
+ boolean noMorePushes = false;
+
+ PushGroup(PushPromiseHandler<T> pushPromiseHandler,
+ HttpRequestImpl initiatingRequest,
+ AccessControlContext acc) {
+ this(pushPromiseHandler, initiatingRequest, new MinimalFuture<>(), acc);
+ }
+
+ // Check mainBodyHandler before calling nested constructor.
+ private PushGroup(HttpResponse.PushPromiseHandler<T> pushPromiseHandler,
+ HttpRequestImpl initiatingRequest,
+ CompletableFuture<HttpResponse<T>> mainResponse,
+ AccessControlContext acc) {
+ this.noMorePushesCF = new MinimalFuture<>();
+ this.pushPromiseHandler = pushPromiseHandler;
+ this.initiatingRequest = initiatingRequest;
+ // Restricts the file publisher with the senders ACC, if any
+ if (pushPromiseHandler instanceof UntrustedBodyHandler)
+ ((UntrustedBodyHandler)this.pushPromiseHandler).setAccessControlContext(acc);
+ this.acc = acc;
+ }
+
+ interface Acceptor<T> {
+ BodyHandler<T> bodyHandler();
+ CompletableFuture<HttpResponse<T>> cf();
+ boolean accepted();
+ }
+
+ private static class AcceptorImpl<T> implements Acceptor<T> {
+ private volatile HttpResponse.BodyHandler<T> bodyHandler;
+ private volatile CompletableFuture<HttpResponse<T>> cf;
+
+ 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;
+ }
+
+ @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<>();
+
+ pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, acceptor::accept);
+
+ synchronized (this) {
+ if (acceptor.accepted()) {
+ if (acceptor.bodyHandler instanceof UntrustedBodyHandler) {
+ ((UntrustedBodyHandler) acceptor.bodyHandler).setAccessControlContext(acc);
+ }
+ 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;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/RawChannelImpl.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,158 @@
+/*
+ * 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.incubator.http.internal;
+
+import jdk.incubator.http.internal.common.Utils;
+import jdk.incubator.http.internal.websocket.RawChannel;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SocketChannel;
+import java.util.function.Supplier;
+
+/*
+ * Each RawChannel corresponds to a TCP connection (SocketChannel) but is
+ * connected to a Selector and an ExecutorService for invoking the send and
+ * receive callbacks. Also includes SSL processing.
+ */
+final class RawChannelImpl implements RawChannel {
+
+ private final HttpClientImpl client;
+ private final HttpConnection.DetachedConnectionChannel detachedChannel;
+ private final Object initialLock = new Object();
+ private Supplier<ByteBuffer> initial;
+
+ RawChannelImpl(HttpClientImpl client,
+ HttpConnection connection,
+ Supplier<ByteBuffer> initial)
+ throws IOException
+ {
+ this.client = client;
+ this.detachedChannel = connection.detachChannel();
+ this.initial = initial;
+
+ SocketChannel chan = connection.channel();
+ client.cancelRegistration(chan);
+ // Constructing a RawChannel is supposed to have a "hand over"
+ // semantics, in other words if construction fails, the channel won't be
+ // needed by anyone, in which case someone still needs to close it
+ try {
+ chan.configureBlocking(false);
+ } catch (IOException e) {
+ try {
+ chan.close();
+ } catch (IOException e1) {
+ e.addSuppressed(e1);
+ } finally {
+ detachedChannel.close();
+ }
+ throw e;
+ }
+ }
+
+ private class NonBlockingRawAsyncEvent extends AsyncEvent {
+
+ private final RawEvent re;
+
+ NonBlockingRawAsyncEvent(RawEvent re) {
+ // !BLOCKING & !REPEATING
+ this.re = re;
+ }
+
+ @Override
+ public SelectableChannel channel() {
+ return detachedChannel.channel();
+ }
+
+ @Override
+ public int interestOps() {
+ return re.interestOps();
+ }
+
+ @Override
+ public void handle() {
+ re.handle();
+ }
+
+ @Override
+ public void abort(IOException ioe) { }
+ }
+
+ @Override
+ public void registerEvent(RawEvent event) throws IOException {
+ client.registerEvent(new NonBlockingRawAsyncEvent(event));
+ }
+
+ @Override
+ public ByteBuffer read() throws IOException {
+ assert !detachedChannel.channel().isBlocking();
+ // connection.read() will no longer be available.
+ return detachedChannel.read();
+ }
+
+ @Override
+ public ByteBuffer initialByteBuffer() {
+ synchronized (initialLock) {
+ if (initial == null) {
+ throw new IllegalStateException();
+ }
+ ByteBuffer ref = initial.get();
+ ref = ref.hasRemaining() ? Utils.copy(ref)
+ : Utils.EMPTY_BYTEBUFFER;
+ initial = null;
+ return ref;
+ }
+ }
+
+ @Override
+ public long write(ByteBuffer[] src, int offset, int len) throws IOException {
+ // this makes the whitebox driver test fail.
+ return detachedChannel.write(src, offset, len);
+ }
+
+ @Override
+ public void shutdownInput() throws IOException {
+ detachedChannel.shutdownInput();
+ }
+
+ @Override
+ public void shutdownOutput() throws IOException {
+ detachedChannel.shutdownOutput();
+ }
+
+ @Override
+ public void close() throws IOException {
+ detachedChannel.close();
+ }
+
+ @Override
+ public String toString() {
+ return super.toString()+"("+ detachedChannel.toString() + ")";
+ }
+
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/RedirectFilter.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,119 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.internal.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);
+ }
+
+ /**
+ * checks to see if new request 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());
+ if (canRedirect(redir) && ++exchange.numberOfRedirects < max_redirects) {
+ //System.out.println("Redirecting to: " + redir);
+ return new HttpRequestImpl(redir, method, request);
+ } else {
+ //System.out.println("Redirect: giving up");
+ 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 SECURE:
+ return newScheme.equalsIgnoreCase("https");
+ case SAME_PROTOCOL:
+ return newScheme.equalsIgnoreCase(oldScheme);
+ default:
+ throw new InternalError();
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/RequestPublishers.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,375 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.io.File;
+import java.io.FileInputStream;
+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.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 jdk.incubator.http.HttpRequest.BodyPublisher;
+import jdk.incubator.http.internal.common.Utils;
+
+public class 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);
+ }
+ }
+
+ public static class FilePublisher implements BodyPublisher {
+ private final File file;
+ private volatile AccessControlContext acc;
+
+ public FilePublisher(Path name) {
+ file = name.toFile();
+ }
+
+ void setAccessControlContext(AccessControlContext acc) {
+ this.acc = acc;
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+ if (System.getSecurityManager() != null && acc == null)
+ throw new InternalError(
+ "Unexpected null acc when security manager has been installed");
+
+ InputStream is;
+ try {
+ PrivilegedExceptionAction<FileInputStream> pa =
+ () -> new FileInputStream(file);
+ is = AccessController.doPrivileged(pa, acc);
+ } catch (PrivilegedActionException pae) {
+ throw new UncheckedIOException((IOException)pae.getCause());
+ }
+ PullPublisher<ByteBuffer> publisher =
+ new PullPublisher<>(() -> new StreamIterator(is));
+ publisher.subscribe(subscriber);
+ }
+
+ @Override
+ public long contentLength() {
+ assert System.getSecurityManager() != null ? acc != null: true;
+ PrivilegedAction<Long> pa = () -> file.length();
+ return AccessController.doPrivileged(pa, acc);
+ }
+ }
+
+ /**
+ * 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);
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/Response.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,100 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.net.URI;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpHeaders;
+
+/**
+ * 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;
+
+ Response(HttpRequestImpl req,
+ Exchange<?> exchange,
+ HttpHeaders headers,
+ int statusCode,
+ HttpClient.Version version) {
+ this(req, exchange, headers, statusCode, version,
+ "CONNECT".equalsIgnoreCase(req.method()));
+ }
+
+ Response(HttpRequestImpl req,
+ Exchange<?> exchange,
+ HttpHeaders headers,
+ int statusCode,
+ HttpClient.Version version,
+ boolean isConnectResponse) {
+ this.headers = headers;
+ this.request = req;
+ this.version = version;
+ this.exchange = exchange;
+ this.statusCode = statusCode;
+ this.isConnectResponse = isConnectResponse;
+ }
+
+ HttpRequestImpl request() {
+ return request;
+ }
+
+ HttpClient.Version version() {
+ return version;
+ }
+
+ HttpHeaders headers() {
+ return headers;
+ }
+
+// Exchange<?> exchange() {
+// return exchange;
+// }
+
+ int statusCode() {
+ return statusCode;
+ }
+
+ @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();
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/ResponseBodyHandlers.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,168 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.AccessControlContext;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Function;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.HttpResponse.BodyHandler;
+import jdk.incubator.http.HttpResponse.BodySubscriber;
+import jdk.incubator.http.internal.ResponseSubscribers.PathSubscriber;
+import static jdk.incubator.http.internal.common.Utils.unchecked;
+
+public class ResponseBodyHandlers {
+
+ /**
+ * A Path body handler.
+ *
+ * Note: Exists mainly too allow setting of the senders ACC post creation of
+ * the handler.
+ */
+ public static class PathBodyHandler implements UntrustedBodyHandler<Path> {
+ private final Path file;
+ private final OpenOption[]openOptions;
+ private volatile AccessControlContext acc;
+
+ public PathBodyHandler(Path file, OpenOption... openOptions) {
+ this.file = file;
+ this.openOptions = openOptions;
+ }
+
+ @Override
+ public void setAccessControlContext(AccessControlContext acc) {
+ this.acc = acc;
+ }
+
+ @Override
+ public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
+ PathSubscriber bs = (PathSubscriber) asFileImpl(file, openOptions);
+ bs.setAccessControlContext(acc);
+ return bs;
+ }
+ }
+
+ /** 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. Supports setting ACC.
+ public static class FileDownloadBodyHandler implements UntrustedBodyHandler<Path> {
+ private final Path directory;
+ private final OpenOption[]openOptions;
+ private volatile AccessControlContext acc;
+
+ public FileDownloadBodyHandler(Path directory, OpenOption... openOptions) {
+ this.directory = directory;
+ this.openOptions = openOptions;
+ }
+
+ @Override
+ public void setAccessControlContext(AccessControlContext acc) {
+ this.acc = acc;
+ }
+
+ @Override
+ public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
+ String dispoHeader = headers.firstValue("Content-Disposition")
+ .orElseThrow(() -> unchecked(new IOException("No Content-Disposition")));
+ if (!dispoHeader.startsWith("attachment;")) {
+ throw unchecked(new IOException("Unknown Content-Disposition type"));
+ }
+ int n = dispoHeader.indexOf("filename=");
+ if (n == -1) {
+ throw unchecked(new IOException("Bad Content-Disposition type"));
+ }
+ int lastsemi = dispoHeader.lastIndexOf(';');
+ String disposition;
+ if (lastsemi < n) {
+ disposition = dispoHeader.substring(n + 9);
+ } else {
+ disposition = dispoHeader.substring(n + 9, lastsemi);
+ }
+ Path file = Paths.get(directory.toString(), disposition);
+
+ PathSubscriber bs = (PathSubscriber)asFileImpl(file, openOptions);
+ bs.setAccessControlContext(acc);
+ return bs;
+ }
+ }
+
+ // no security check
+ private static BodySubscriber<Path> asFileImpl(Path file, OpenOption... openOptions) {
+ return new ResponseSubscribers.PathSubscriber(file, openOptions);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/ResponseContent.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,467 @@
+/*
+ * 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.incubator.http.internal;
+
+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 jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.internal.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 {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+
+ 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 System.Logger debug = Utils.getDebugLogger(this::dbgString, 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) {
+ debug.log(Level.DEBUG, () -> "onSubscribe: "
+ + pusher.getClass().getName());
+ pusher.onSubscribe(this.sub = sub);
+ }
+
+ @Override
+ public void accept(ByteBuffer b) {
+ if (closedExceptionally != null) {
+ debug.log(Level.DEBUG, () -> "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));
+ }
+ debug.log(Level.DEBUG, () -> "done!");
+ assert closedExceptionally == null;
+ assert state == ChunkState.DONE;
+ onFinished.run();
+ pusher.onComplete();
+ 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));
+ }
+ assert state == ChunkState.DONE || !b.hasRemaining();
+ } catch(Throwable 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) {
+ debug.log(Level.DEBUG, () -> "Trying to read chunk len"
+ + " (remaining in buffer:"+chunk.remaining()+")");
+ int clen = chunklen = tryReadChunkLen(chunk);
+ if (clen == -1) return READMORE;
+ debug.log(Level.DEBUG, "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) {
+ debug.log(Level.DEBUG,
+ "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;
+ debug.log(Level.DEBUG, "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();
+ debug.log(Level.DEBUG, "Reading chunk: available %d, needed %d",
+ bytesread, unfulfilled);
+
+ int bytes2return = Math.min(bytesread, unfulfilled);
+ debug.log(Level.DEBUG, "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) {
+ debug.log(Level.DEBUG,
+ "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;
+ debug.log(Level.DEBUG, "Ready to read next chunk");
+ }
+ }
+ if (returnBuffer == READMORE) {
+ debug.log(Level.DEBUG, "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()) {
+ debug.log(Level.DEBUG, "Sending chunk to consumer (%d)",
+ b1.remaining());
+ out.add(b1);
+ debug.log(Level.DEBUG, "Chunk sent.");
+ }
+ 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 System.Logger debug = Utils.getDebugLogger(this::dbgString, 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) {
+ debug.log(Level.DEBUG, () -> "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) {
+ debug.log(Level.DEBUG, () -> "already closed: "
+ + closedExceptionally);
+ return;
+ }
+ boolean completed = false;
+ try {
+ int unfulfilled = remaining;
+ debug.log(Level.DEBUG, "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.
+ assert closedExceptionally == null;
+ onFinished.run();
+ pusher.onComplete();
+ completed = true;
+ onComplete.accept(closedExceptionally); // should be null
+ } else {
+ assert b.remaining() == 0;
+ }
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG, "Unexpected exception", t);
+ closedExceptionally = t;
+ if (!completed) {
+ onComplete.accept(t);
+ }
+ }
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/SSLDelegate.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,489 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import javax.net.ssl.SSLEngineResult.Status;
+import javax.net.ssl.*;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.Utils;
+import static javax.net.ssl.SSLEngineResult.HandshakeStatus.*;
+
+/**
+ * Implements the mechanics of SSL by managing an SSLEngine object.
+ * <p>
+ * This class is only used to implement the {@link
+ * AbstractAsyncSSLConnection.SSLConnectionChannel} which is handed of
+ * to RawChannelImpl when creating a WebSocket.
+ */
+class SSLDelegate {
+
+ final SSLEngine engine;
+ final EngineWrapper wrapper;
+ final Lock handshaking = new ReentrantLock();
+ final SocketChannel chan;
+
+ SSLDelegate(SSLEngine eng, SocketChannel chan)
+ {
+ this.engine = eng;
+ this.chan = chan;
+ this.wrapper = new EngineWrapper(chan, engine);
+ }
+
+ // alpn[] may be null
+// SSLDelegate(SocketChannel chan, HttpClientImpl client, String[] alpn, String sn)
+// throws IOException
+// {
+// serverName = sn;
+// SSLContext context = client.sslContext();
+// engine = context.createSSLEngine();
+// engine.setUseClientMode(true);
+// SSLParameters sslp = client.sslParameters();
+// sslParameters = Utils.copySSLParameters(sslp);
+// if (sn != null) {
+// SNIHostName sni = new SNIHostName(sn);
+// sslParameters.setServerNames(List.of(sni));
+// }
+// if (alpn != null) {
+// sslParameters.setApplicationProtocols(alpn);
+// Log.logSSL("SSLDelegate: Setting application protocols: {0}" + Arrays.toString(alpn));
+// } else {
+// Log.logSSL("SSLDelegate: No application protocols proposed");
+// }
+// engine.setSSLParameters(sslParameters);
+// wrapper = new EngineWrapper(chan, engine);
+// this.chan = chan;
+// this.client = client;
+// }
+
+// SSLParameters getSSLParameters() {
+// return sslParameters;
+// }
+
+ static long countBytes(ByteBuffer[] buffers, int start, int number) {
+ long c = 0;
+ for (int i=0; i<number; i++) {
+ c+= buffers[start+i].remaining();
+ }
+ return c;
+ }
+
+
+ static class WrapperResult {
+ static WrapperResult createOK() {
+ WrapperResult r = new WrapperResult();
+ r.buf = null;
+ r.result = new SSLEngineResult(Status.OK, NOT_HANDSHAKING, 0, 0);
+ return r;
+ }
+ SSLEngineResult result;
+
+ ByteBuffer buf; // buffer containing result data
+ }
+
+ int app_buf_size;
+ int packet_buf_size;
+
+ enum BufType {
+ PACKET,
+ APPLICATION
+ }
+
+ ByteBuffer allocate (BufType type) {
+ return allocate (type, -1);
+ }
+
+ // TODO: Use buffer pool for this
+ ByteBuffer allocate (BufType type, int len) {
+ assert engine != null;
+ synchronized (this) {
+ int size;
+ if (type == BufType.PACKET) {
+ if (packet_buf_size == 0) {
+ SSLSession sess = engine.getSession();
+ packet_buf_size = sess.getPacketBufferSize();
+ }
+ if (len > packet_buf_size) {
+ packet_buf_size = len;
+ }
+ size = packet_buf_size;
+ } else {
+ if (app_buf_size == 0) {
+ SSLSession sess = engine.getSession();
+ app_buf_size = sess.getApplicationBufferSize();
+ }
+ if (len > app_buf_size) {
+ app_buf_size = len;
+ }
+ size = app_buf_size;
+ }
+ return ByteBuffer.allocate (size);
+ }
+ }
+
+ /* reallocates the buffer by :-
+ * 1. creating a new buffer double the size of the old one
+ * 2. putting the contents of the old buffer into the new one
+ * 3. set xx_buf_size to the new size if it was smaller than new size
+ *
+ * flip is set to true if the old buffer needs to be flipped
+ * before it is copied.
+ */
+ private ByteBuffer realloc (ByteBuffer b, boolean flip, BufType type) {
+ // TODO: there should be the linear growth, rather than exponential as
+ // we definitely know the maximum amount of space required to unwrap
+ synchronized (this) {
+ int nsize = 2 * b.capacity();
+ ByteBuffer n = allocate (type, nsize);
+ if (flip) {
+ b.flip();
+ }
+ n.put(b);
+ b = n;
+ }
+ return b;
+ }
+
+ /**
+ * This is a thin wrapper over SSLEngine and the SocketChannel, which
+ * guarantees the ordering of wraps/unwraps with respect to the underlying
+ * channel read/writes. It handles the UNDER/OVERFLOW status codes
+ * It does not handle the handshaking status codes, or the CLOSED status code
+ * though once the engine is closed, any attempt to read/write to it
+ * will get an exception. The overall result is returned.
+ * It functions synchronously/blocking
+ */
+ class EngineWrapper {
+
+ SocketChannel chan;
+ SSLEngine engine;
+ final Object wrapLock;
+ final Object unwrapLock;
+ ByteBuffer unwrap_src, wrap_dst;
+ boolean closed = false;
+ int u_remaining; // the number of bytes left in unwrap_src after an unwrap()
+
+ EngineWrapper (SocketChannel chan, SSLEngine engine) {
+ this.chan = chan;
+ this.engine = engine;
+ wrapLock = new Object();
+ unwrapLock = new Object();
+ unwrap_src = allocate(BufType.PACKET);
+ wrap_dst = allocate(BufType.PACKET);
+ }
+
+// void close () throws IOException {
+// }
+
+ WrapperResult wrapAndSend(ByteBuffer src, boolean ignoreClose)
+ throws IOException
+ {
+ ByteBuffer[] buffers = new ByteBuffer[1];
+ buffers[0] = src;
+ return wrapAndSend(buffers, 0, 1, ignoreClose);
+ }
+
+ /* try to wrap and send the data in src. Handles OVERFLOW.
+ * Might block if there is an outbound blockage or if another
+ * thread is calling wrap(). Also, might not send any data
+ * if an unwrap is needed.
+ */
+ WrapperResult wrapAndSend(ByteBuffer[] src,
+ int offset,
+ int len,
+ boolean ignoreClose)
+ throws IOException
+ {
+ if (closed && !ignoreClose) {
+ throw new IOException ("Engine is closed");
+ }
+ Status status;
+ WrapperResult r = new WrapperResult();
+ synchronized (wrapLock) {
+ wrap_dst.clear();
+ do {
+ r.result = engine.wrap (src, offset, len, wrap_dst);
+ status = r.result.getStatus();
+ if (status == Status.BUFFER_OVERFLOW) {
+ wrap_dst = realloc (wrap_dst, true, BufType.PACKET);
+ }
+ } while (status == Status.BUFFER_OVERFLOW);
+ if (status == Status.CLOSED && !ignoreClose) {
+ closed = true;
+ return r;
+ }
+ if (r.result.bytesProduced() > 0) {
+ wrap_dst.flip();
+ int l = wrap_dst.remaining();
+ assert l == r.result.bytesProduced();
+ while (l>0) {
+ l -= chan.write (wrap_dst);
+ }
+ }
+ }
+ return r;
+ }
+
+ /* block until a complete message is available and return it
+ * in dst, together with the Result. dst may have been re-allocated
+ * so caller should check the returned value in Result
+ * If handshaking is in progress then, possibly no data is returned
+ */
+ WrapperResult recvAndUnwrap(ByteBuffer dst) throws IOException {
+ Status status;
+ WrapperResult r = new WrapperResult();
+ r.buf = dst;
+ if (closed) {
+ throw new IOException ("Engine is closed");
+ }
+ boolean needData;
+ if (u_remaining > 0) {
+ unwrap_src.compact();
+ unwrap_src.flip();
+ needData = false;
+ } else {
+ unwrap_src.clear();
+ needData = true;
+ }
+ synchronized (unwrapLock) {
+ int x;
+ do {
+ if (needData) {
+ x = chan.read (unwrap_src);
+ if (x == -1) {
+ throw new IOException ("connection closed for reading");
+ }
+ unwrap_src.flip();
+ }
+ r.result = engine.unwrap (unwrap_src, r.buf);
+ status = r.result.getStatus();
+ if (status == Status.BUFFER_UNDERFLOW) {
+ if (unwrap_src.limit() == unwrap_src.capacity()) {
+ /* buffer not big enough */
+ unwrap_src = realloc (
+ unwrap_src, false, BufType.PACKET
+ );
+ } else {
+ /* Buffer not full, just need to read more
+ * data off the channel. Reset pointers
+ * for reading off SocketChannel
+ */
+ unwrap_src.position (unwrap_src.limit());
+ unwrap_src.limit (unwrap_src.capacity());
+ }
+ needData = true;
+ } else if (status == Status.BUFFER_OVERFLOW) {
+ r.buf = realloc (r.buf, true, BufType.APPLICATION);
+ needData = false;
+ } else if (status == Status.CLOSED) {
+ closed = true;
+ r.buf.flip();
+ return r;
+ }
+ } while (status != Status.OK);
+ }
+ u_remaining = unwrap_src.remaining();
+ return r;
+ }
+ }
+
+// WrapperResult sendData (ByteBuffer src) throws IOException {
+// ByteBuffer[] buffers = new ByteBuffer[1];
+// buffers[0] = src;
+// return sendData(buffers, 0, 1);
+// }
+
+ /**
+ * send the data in the given ByteBuffer. If a handshake is needed
+ * then this is handled within this method. When this call returns,
+ * all of the given user data has been sent and any handshake has been
+ * completed. Caller should check if engine has been closed.
+ */
+ WrapperResult sendData (ByteBuffer[] src, int offset, int len) throws IOException {
+ WrapperResult r = WrapperResult.createOK();
+ while (countBytes(src, offset, len) > 0) {
+ r = wrapper.wrapAndSend(src, offset, len, false);
+ Status status = r.result.getStatus();
+ if (status == Status.CLOSED) {
+ doClosure ();
+ return r;
+ }
+ HandshakeStatus hs_status = r.result.getHandshakeStatus();
+ if (hs_status != HandshakeStatus.FINISHED &&
+ hs_status != HandshakeStatus.NOT_HANDSHAKING)
+ {
+ doHandshake(hs_status);
+ }
+ }
+ return r;
+ }
+
+ /**
+ * read data thru the engine into the given ByteBuffer. If the
+ * given buffer was not large enough, a new one is allocated
+ * and returned. This call handles handshaking automatically.
+ * Caller should check if engine has been closed.
+ */
+ WrapperResult recvData (ByteBuffer dst) throws IOException {
+ /* we wait until some user data arrives */
+ int mark = dst.position();
+ WrapperResult r = null;
+ int pos = dst.position();
+ while (dst.position() == pos) {
+ r = wrapper.recvAndUnwrap (dst);
+ dst = (r.buf != dst) ? r.buf: dst;
+ Status status = r.result.getStatus();
+ if (status == Status.CLOSED) {
+ doClosure ();
+ return r;
+ }
+
+ HandshakeStatus hs_status = r.result.getHandshakeStatus();
+ if (hs_status != HandshakeStatus.FINISHED &&
+ hs_status != HandshakeStatus.NOT_HANDSHAKING)
+ {
+ doHandshake (hs_status);
+ }
+ }
+ Utils.flipToMark(dst, mark);
+ return r;
+ }
+
+ /* we've received a close notify. Need to call wrap to send
+ * the response
+ */
+ void doClosure () throws IOException {
+ try {
+ handshaking.lock();
+ ByteBuffer tmp = allocate(BufType.APPLICATION);
+ WrapperResult r;
+ do {
+ tmp.clear();
+ tmp.flip ();
+ r = wrapper.wrapAndSend(tmp, true);
+ } while (r.result.getStatus() != Status.CLOSED);
+ } finally {
+ handshaking.unlock();
+ }
+ }
+
+ /* do the (complete) handshake after acquiring the handshake lock.
+ * If two threads call this at the same time, then we depend
+ * on the wrapper methods being idempotent. eg. if wrapAndSend()
+ * is called with no data to send then there must be no problem
+ */
+ @SuppressWarnings("fallthrough")
+ void doHandshake (HandshakeStatus hs_status) throws IOException {
+ boolean wasBlocking;
+ try {
+ wasBlocking = chan.isBlocking();
+ handshaking.lock();
+ chan.configureBlocking(true);
+ ByteBuffer tmp = allocate(BufType.APPLICATION);
+ while (hs_status != HandshakeStatus.FINISHED &&
+ hs_status != HandshakeStatus.NOT_HANDSHAKING)
+ {
+ WrapperResult r = null;
+ switch (hs_status) {
+ case NEED_TASK:
+ Runnable task;
+ while ((task = engine.getDelegatedTask()) != null) {
+ /* run in current thread, because we are already
+ * running an external Executor
+ */
+ task.run();
+ }
+ /* fall thru - call wrap again */
+ case NEED_WRAP:
+ tmp.clear();
+ tmp.flip();
+ r = wrapper.wrapAndSend(tmp, false);
+ break;
+
+ case NEED_UNWRAP:
+ tmp.clear();
+ r = wrapper.recvAndUnwrap (tmp);
+ if (r.buf != tmp) {
+ tmp = r.buf;
+ }
+ assert tmp.position() == 0;
+ break;
+ }
+ hs_status = r.result.getHandshakeStatus();
+ }
+ Log.logSSL(getSessionInfo());
+ if (!wasBlocking) {
+ chan.configureBlocking(false);
+ }
+ } finally {
+ handshaking.unlock();
+ }
+ }
+
+// static void printParams(SSLParameters p) {
+// System.out.println("SSLParameters:");
+// if (p == null) {
+// System.out.println("Null params");
+// return;
+// }
+// for (String cipher : p.getCipherSuites()) {
+// System.out.printf("cipher: %s\n", cipher);
+// }
+// // JDK 8 EXCL START
+// for (String approto : p.getApplicationProtocols()) {
+// System.out.printf("application protocol: %s\n", approto);
+// }
+// // JDK 8 EXCL END
+// for (String protocol : p.getProtocols()) {
+// System.out.printf("protocol: %s\n", protocol);
+// }
+// if (p.getServerNames() != null) {
+// for (SNIServerName sname : p.getServerNames()) {
+// System.out.printf("server name: %s\n", sname.toString());
+// }
+// }
+// }
+
+ String getSessionInfo() {
+ StringBuilder sb = new StringBuilder();
+ String application = engine.getApplicationProtocol();
+ SSLSession sess = engine.getSession();
+ String cipher = sess.getCipherSuite();
+ String protocol = sess.getProtocol();
+ sb.append("Handshake complete alpn: ")
+ .append(application)
+ .append(", Cipher: ")
+ .append(cipher)
+ .append(", Protocol: ")
+ .append(protocol);
+ return sb.toString();
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/SocketTube.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,956 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import jdk.incubator.http.internal.common.Demand;
+import jdk.incubator.http.internal.common.FlowTube;
+import jdk.incubator.http.internal.common.SequentialScheduler;
+import jdk.incubator.http.internal.common.SequentialScheduler.DeferredCompleter;
+import jdk.incubator.http.internal.common.SequentialScheduler.RestartableTask;
+import jdk.incubator.http.internal.common.Utils;
+
+/**
+ * A SocketTube is a terminal tube plugged directly into the socket.
+ * The read subscriber should call {@code subscribe} on the SocketTube before
+ * the SocketTube can be subscribed to the write publisher.
+ */
+final class SocketTube implements FlowTube {
+
+ static final boolean DEBUG = Utils.DEBUG; // revisit: temporary developer's flag
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ static final AtomicLong IDS = new AtomicLong();
+
+ private final HttpClientImpl client;
+ private final SocketChannel channel;
+ private final Supplier<ByteBuffer> buffersSource;
+ private final Object lock = new Object();
+ private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+ private final InternalReadPublisher readPublisher;
+ private final InternalWriteSubscriber writeSubscriber;
+ private final long id = IDS.incrementAndGet();
+
+ public SocketTube(HttpClientImpl client, SocketChannel channel,
+ Supplier<ByteBuffer> buffersSource) {
+ this.client = client;
+ this.channel = channel;
+ this.buffersSource = buffersSource;
+ this.readPublisher = new InternalReadPublisher();
+ this.writeSubscriber = new InternalWriteSubscriber();
+ }
+
+// private static Flow.Subscription nopSubscription() {
+// return new Flow.Subscription() {
+// @Override public void request(long n) { }
+// @Override public void cancel() { }
+// };
+// }
+
+ /**
+ * Returns {@code true} if this flow is finished.
+ * This happens when this flow internal read subscription is completed,
+ * either normally (EOF reading) or exceptionally (EOF writing, or
+ * underlying socket closed, or some exception occurred while reading or
+ * writing to the socket).
+ *
+ * @return {@code true} if this flow is finished.
+ */
+ public boolean isFinished() {
+ InternalReadPublisher.InternalReadSubscription subscription =
+ readPublisher.subscriptionImpl;
+ return subscription != null && subscription.completed
+ || subscription == null && errorRef.get() != null;
+ }
+
+ // ===================================================================== //
+ // Flow.Publisher //
+ // ======================================================================//
+
+ /**
+ * {@inheritDoc }
+ * @apiNote This method should be called first. In particular, the caller
+ * must ensure that this method must be called by the read
+ * subscriber before the write publisher can call {@code onSubscribe}.
+ * Failure to adhere to this contract may result in assertion errors.
+ */
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
+ Objects.requireNonNull(s);
+ assert s instanceof TubeSubscriber : "Expected TubeSubscriber, got:" + s;
+ readPublisher.subscribe(s);
+ }
+
+
+ // ===================================================================== //
+ // Flow.Subscriber //
+ // ======================================================================//
+
+ /**
+ * {@inheritDoc }
+ * @apiNote The caller must ensure that {@code subscribe} is called by
+ * the read subscriber before {@code onSubscribe} is called by
+ * the write publisher.
+ * Failure to adhere to this contract may result in assertion errors.
+ */
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ writeSubscriber.onSubscribe(subscription);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ writeSubscriber.onNext(item);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ writeSubscriber.onError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ writeSubscriber.onComplete();
+ }
+
+ // ===================================================================== //
+ // Events //
+ // ======================================================================//
+
+ /**
+ * A restartable task used to process tasks in sequence.
+ */
+ private static class SocketFlowTask implements RestartableTask {
+ final Runnable task;
+ private final Object monitor = new Object();
+ SocketFlowTask(Runnable task) {
+ this.task = task;
+ }
+ @Override
+ public final void run(DeferredCompleter taskCompleter) {
+ try {
+ // non contentious synchronized for visibility.
+ synchronized(monitor) {
+ task.run();
+ }
+ } finally {
+ taskCompleter.complete();
+ }
+ }
+ }
+
+ // This is best effort - there's no guarantee that the printed set
+ // of values is consistent. It should only be considered as
+ // weakly accurate - in particular in what concerns the events states,
+ // especially when displaying a read event state from a write event
+ // callback and conversely.
+ void debugState(String when) {
+ if (debug.isLoggable(Level.DEBUG)) {
+ StringBuilder state = new StringBuilder();
+
+ InternalReadPublisher.InternalReadSubscription sub =
+ readPublisher.subscriptionImpl;
+ InternalReadPublisher.ReadEvent readEvent =
+ sub == null ? null : sub.readEvent;
+ Demand rdemand = sub == null ? null : sub.demand;
+ InternalWriteSubscriber.WriteEvent writeEvent =
+ writeSubscriber.writeEvent;
+ AtomicLong wdemand = writeSubscriber.writeDemand;
+ int rops = readEvent == null ? 0 : readEvent.interestOps();
+ long rd = rdemand == null ? 0 : rdemand.get();
+ int wops = writeEvent == null ? 0 : writeEvent.interestOps();
+ long wd = wdemand == null ? 0 : wdemand.get();
+
+ state.append(when).append(" Reading: [ops=")
+ .append(rops).append(", demand=").append(rd)
+ .append(", stopped=")
+ .append((sub == null ? false : sub.readScheduler.isStopped()))
+ .append("], Writing: [ops=").append(wops)
+ .append(", demand=").append(wd)
+ .append("]");
+ debug.log(Level.DEBUG, state.toString());
+ }
+ }
+
+ /**
+ * A repeatable event that can be paused or resumed by changing
+ * its interestOps.
+ * When the event is fired, it is first paused before being signaled.
+ * It is the responsibility of the code triggered by {@code signalEvent}
+ * to resume the event if required.
+ */
+ private static abstract class SocketFlowEvent extends AsyncEvent {
+ final SocketChannel channel;
+ final int defaultInterest;
+ volatile int interestOps;
+ volatile boolean registered;
+ SocketFlowEvent(int defaultInterest, SocketChannel channel) {
+ super(AsyncEvent.REPEATING);
+ this.defaultInterest = defaultInterest;
+ this.channel = channel;
+ }
+ final boolean registered() {return registered;}
+ final void resume() {
+ interestOps = defaultInterest;
+ registered = true;
+ }
+ final void pause() {interestOps = 0;}
+ @Override
+ public final SelectableChannel channel() {return channel;}
+ @Override
+ public final int interestOps() {return interestOps;}
+
+ @Override
+ public final void handle() {
+ pause(); // pause, then signal
+ signalEvent(); // won't be fired again until resumed.
+ }
+ @Override
+ public final void abort(IOException error) {
+ debug().log(Level.DEBUG, () -> "abort: " + error);
+ pause(); // pause, then signal
+ signalError(error); // should not be resumed after abort (not checked)
+ }
+
+ protected abstract void signalEvent();
+ protected abstract void signalError(Throwable error);
+ abstract System.Logger debug();
+ }
+
+ // ===================================================================== //
+ // Writing //
+ // ======================================================================//
+
+ // This class makes the assumption that the publisher will call
+ // onNext sequentially, and that onNext won't be called if the demand
+ // has not been incremented by request(1).
+ // It has a 'queue of 1' meaning that it will call request(1) in
+ // onSubscribe, and then only after its 'current' buffer list has been
+ // fully written and current set to null;
+ private final class InternalWriteSubscriber
+ implements Flow.Subscriber<List<ByteBuffer>> {
+
+ volatile Flow.Subscription subscription;
+ volatile List<ByteBuffer> current;
+ volatile boolean completed;
+ final WriteEvent writeEvent = new WriteEvent(channel, this);
+ final AtomicLong writeDemand = new AtomicLong();
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ Flow.Subscription previous = this.subscription;
+ this.subscription = subscription;
+ debug.log(Level.DEBUG, "subscribed for writing");
+ if (current == null) {
+ if (previous == subscription || previous == null) {
+ if (writeDemand.compareAndSet(0, 1)) {
+ subscription.request(1);
+ }
+ } else {
+ writeDemand.set(1);
+ subscription.request(1);
+ }
+ }
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> bufs) {
+ assert current == null; // this is a queue of 1.
+ assert subscription != null;
+ current = bufs;
+ tryFlushCurrent(client.isSelectorThread()); // may be in selector thread
+ // For instance in HTTP/2, a received SETTINGS frame might trigger
+ // the sending of a SETTINGS frame in turn which might cause
+ // onNext to be called from within the same selector thread that the
+ // original SETTINGS frames arrived on. If rs is the read-subscriber
+ // and ws is the write-subscriber then the following can occur:
+ // ReadEvent -> rs.onNext(bytes) -> process server SETTINGS -> write
+ // client SETTINGS -> ws.onNext(bytes) -> tryFlushCurrent
+ debugState("leaving w.onNext");
+ }
+
+ // we don't use a SequentialScheduler here: we rely on
+ // onNext() being called sequentially, and not being called
+ // if we haven't call request(1)
+ // onNext is usually called from within a user/executor thread.
+ // we will perform the initial writing in that thread.
+ // if for some reason, not all data can be written, the writeEvent
+ // will be resumed, and the rest of the data will be written from
+ // the selector manager thread when the writeEvent is fired.
+ // If we are in the selector manager thread, then we will use the executor
+ // to call request(1), ensuring that onNext() won't be called from
+ // within the selector thread.
+ // If we are not in the selector manager thread, then we don't care.
+ void tryFlushCurrent(boolean inSelectorThread) {
+ List<ByteBuffer> bufs = current;
+ if (bufs == null) return;
+ try {
+ assert inSelectorThread == client.isSelectorThread() :
+ "should " + (inSelectorThread ? "" : "not ")
+ + " be in the selector thread";
+ long remaining = Utils.remaining(bufs);
+ debug.log(Level.DEBUG, "trying to write: %d", remaining);
+ long written = writeAvailable(bufs);
+ debug.log(Level.DEBUG, "wrote: %d", remaining);
+ if (written == -1) {
+ signalError(new EOFException("EOF reached while writing"));
+ return;
+ }
+ assert written <= remaining;
+ if (remaining - written == 0) {
+ current = null;
+ writeDemand.decrementAndGet();
+ Runnable requestMore = this::requestMore;
+ if (inSelectorThread) {
+ assert client.isSelectorThread();
+ client.theExecutor().execute(requestMore);
+ } else {
+ assert !client.isSelectorThread();
+ requestMore.run();
+ }
+ } else {
+ resumeWriteEvent(inSelectorThread);
+ }
+ } catch (Throwable t) {
+ signalError(t);
+ subscription.cancel();
+ }
+ }
+
+ void requestMore() {
+ try {
+ if (completed) return;
+ long d = writeDemand.get();
+ if (writeDemand.compareAndSet(0,1)) {
+ debug.log(Level.DEBUG, "write: requesting more...");
+ subscription.request(1);
+ } else {
+ debug.log(Level.DEBUG, "write: no need to request more: %d", d);
+ }
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG, () ->
+ "write: error while requesting more: " + t);
+ signalError(t);
+ subscription.cancel();
+ } finally {
+ debugState("leaving requestMore: ");
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ signalError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ completed = true;
+ // no need to pause the write event here: the write event will
+ // be paused if there is nothing more to write.
+ List<ByteBuffer> bufs = current;
+ long remaining = bufs == null ? 0 : Utils.remaining(bufs);
+ debug.log(Level.DEBUG, "write completed, %d yet to send", remaining);
+ debugState("InternalWriteSubscriber::onComplete");
+ }
+
+ void resumeWriteEvent(boolean inSelectorThread) {
+ debug.log(Level.DEBUG, "scheduling write event");
+ resumeEvent(writeEvent, this::signalError);
+ }
+
+// void pauseWriteEvent() {
+// debug.log(Level.DEBUG, "pausing write event");
+// pauseEvent(writeEvent, this::signalError);
+// }
+
+ void signalWritable() {
+ debug.log(Level.DEBUG, "channel is writable");
+ tryFlushCurrent(true);
+ }
+
+ void signalError(Throwable error) {
+ debug.log(Level.DEBUG, () -> "write error: " + error);
+ completed = true;
+ readPublisher.signalError(error);
+ }
+
+ // A repeatable WriteEvent which is paused after firing and can
+ // be resumed if required - see SocketFlowEvent;
+ final class WriteEvent extends SocketFlowEvent {
+ final InternalWriteSubscriber sub;
+ WriteEvent(SocketChannel channel, InternalWriteSubscriber sub) {
+ super(SelectionKey.OP_WRITE, channel);
+ this.sub = sub;
+ }
+ @Override
+ protected final void signalEvent() {
+ try {
+ client.eventUpdated(this);
+ sub.signalWritable();
+ } catch(Throwable t) {
+ sub.signalError(t);
+ }
+ }
+
+ @Override
+ protected void signalError(Throwable error) {
+ sub.signalError(error);
+ }
+
+ @Override
+ System.Logger debug() {
+ return debug;
+ }
+
+ }
+
+ }
+
+ // ===================================================================== //
+ // Reading //
+ // ===================================================================== //
+
+ // The InternalReadPublisher uses a SequentialScheduler to ensure that
+ // onNext/onError/onComplete are called sequentially on the caller's
+ // subscriber.
+ // However, it relies on the fact that the only time where
+ // runOrSchedule() is called from a user/executor thread is in signalError,
+ // right after the errorRef has been set.
+ // Because the sequential scheduler's task always checks for errors first,
+ // and always terminate the scheduler on error, then it is safe to assume
+ // that if it reaches the point where it reads from the channel, then
+ // it is running in the SelectorManager thread. This is because all
+ // other invocation of runOrSchedule() are triggered from within a
+ // ReadEvent.
+ //
+ // When pausing/resuming the event, some shortcuts can then be taken
+ // when we know we're running in the selector manager thread
+ // (in that case there's no need to call client.eventUpdated(readEvent);
+ //
+ private final class InternalReadPublisher
+ implements Flow.Publisher<List<ByteBuffer>> {
+ private final InternalReadSubscription subscriptionImpl
+ = new InternalReadSubscription();
+ AtomicReference<ReadSubscription> pendingSubscription = new AtomicReference<>();
+ private volatile ReadSubscription subscription;
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
+ Objects.requireNonNull(s);
+
+ TubeSubscriber sub = FlowTube.asTubeSubscriber(s);
+ ReadSubscription target = new ReadSubscription(subscriptionImpl, sub);
+ ReadSubscription previous = pendingSubscription.getAndSet(target);
+
+ if (previous != null && previous != target) {
+ debug.log(Level.DEBUG,
+ () -> "read publisher: dropping pending subscriber: "
+ + previous.subscriber);
+ previous.errorRef.compareAndSet(null, errorRef.get());
+ previous.signalOnSubscribe();
+ if (subscriptionImpl.completed) {
+ previous.signalCompletion();
+ } else {
+ previous.subscriber.dropSubscription();
+ }
+ }
+
+ debug.log(Level.DEBUG, "read publisher got subscriber");
+ subscriptionImpl.signalSubscribe();
+ debugState("leaving read.subscribe: ");
+ }
+
+ void signalError(Throwable error) {
+ if (!errorRef.compareAndSet(null, error)) {
+ return;
+ }
+ subscriptionImpl.handleError();
+ }
+
+ final class ReadSubscription implements Flow.Subscription {
+ final InternalReadSubscription impl;
+ final TubeSubscriber subscriber;
+ final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+ volatile boolean subscribed;
+ volatile boolean cancelled;
+ volatile boolean completed;
+
+ public ReadSubscription(InternalReadSubscription impl,
+ TubeSubscriber subscriber) {
+ this.impl = impl;
+ this.subscriber = subscriber;
+ }
+
+ @Override
+ public void cancel() {
+ cancelled = true;
+ }
+
+ @Override
+ public void request(long n) {
+ if (!cancelled) {
+ impl.request(n);
+ } else {
+ debug.log(Level.DEBUG,
+ "subscription cancelled, ignoring request %d", n);
+ }
+ }
+
+ void signalCompletion() {
+ assert subscribed || cancelled;
+ if (completed || cancelled) return;
+ synchronized (this) {
+ if (completed) return;
+ completed = true;
+ }
+ Throwable error = errorRef.get();
+ if (error != null) {
+ debug.log(Level.DEBUG, () ->
+ "forwarding error to subscriber: "
+ + error);
+ subscriber.onError(error);
+ } else {
+ debug.log(Level.DEBUG, "completing subscriber");
+ subscriber.onComplete();
+ }
+ }
+
+ void signalOnSubscribe() {
+ if (subscribed || cancelled) return;
+ synchronized (this) {
+ if (subscribed || cancelled) return;
+ subscribed = true;
+ }
+ subscriber.onSubscribe(this);
+ debug.log(Level.DEBUG, "onSubscribe called");
+ if (errorRef.get() != null) {
+ signalCompletion();
+ }
+ }
+ }
+
+ final class InternalReadSubscription implements Flow.Subscription {
+
+ private final Demand demand = new Demand();
+ final SequentialScheduler readScheduler;
+ private volatile boolean completed;
+ private final ReadEvent readEvent;
+ private final AsyncEvent subscribeEvent;
+
+ InternalReadSubscription() {
+ readScheduler = new SequentialScheduler(new SocketFlowTask(this::read));
+ subscribeEvent = new AsyncTriggerEvent(this::signalError,
+ this::handleSubscribeEvent);
+ readEvent = new ReadEvent(channel, this);
+ }
+
+ /*
+ * This method must be invoked before any other method of this class.
+ */
+ final void signalSubscribe() {
+ if (readScheduler.isStopped() || completed) {
+ // if already completed or stopped we can handle any
+ // pending connection directly from here.
+ debug.log(Level.DEBUG,
+ "handling pending subscription while completed");
+ handlePending();
+ } else {
+ try {
+ debug.log(Level.DEBUG,
+ "registering subscribe event");
+ client.registerEvent(subscribeEvent);
+ } catch (Throwable t) {
+ signalError(t);
+ handlePending();
+ }
+ }
+ }
+
+ final void handleSubscribeEvent() {
+ assert client.isSelectorThread();
+ debug.log(Level.DEBUG, "subscribe event raised");
+ readScheduler.runOrSchedule();
+ if (readScheduler.isStopped() || completed) {
+ // if already completed or stopped we can handle any
+ // pending connection directly from here.
+ debug.log(Level.DEBUG,
+ "handling pending subscription when completed");
+ handlePending();
+ }
+ }
+
+
+ /*
+ * Although this method is thread-safe, the Reactive-Streams spec seems
+ * to not require it to be as such. It's a responsibility of the
+ * subscriber to signal demand in a thread-safe manner.
+ *
+ * https://github.com/reactive-streams/reactive-streams-jvm/blob/dd24d2ab164d7de6c316f6d15546f957bec29eaa/README.md
+ * (rules 2.7 and 3.4)
+ */
+ @Override
+ public final void request(long n) {
+ if (n > 0L) {
+ boolean wasFulfilled = demand.increase(n);
+ if (wasFulfilled) {
+ debug.log(Level.DEBUG, "got some demand for reading");
+ resumeReadEvent();
+ // if demand has been changed from fulfilled
+ // to unfulfilled register read event;
+ }
+ } else {
+ signalError(new IllegalArgumentException("non-positive request"));
+ }
+ debugState("leaving request("+n+"): ");
+ }
+
+ @Override
+ public final void cancel() {
+ pauseReadEvent();
+ readScheduler.stop();
+ }
+
+ private void resumeReadEvent() {
+ debug.log(Level.DEBUG, "resuming read event");
+ resumeEvent(readEvent, this::signalError);
+ }
+
+ private void pauseReadEvent() {
+ debug.log(Level.DEBUG, "pausing read event");
+ pauseEvent(readEvent, this::signalError);
+ }
+
+
+ final void handleError() {
+ assert errorRef.get() != null;
+ readScheduler.runOrSchedule();
+ }
+
+ final void signalError(Throwable error) {
+ if (!errorRef.compareAndSet(null, error)) {
+ return;
+ }
+ debug.log(Level.DEBUG, () -> "got read error: " + error);
+ readScheduler.runOrSchedule();
+ }
+
+ final void signalReadable() {
+ readScheduler.runOrSchedule();
+ }
+
+ /** The body of the task that runs in SequentialScheduler. */
+ final void read() {
+ // It is important to only call pauseReadEvent() when stopping
+ // the scheduler. The event is automatically paused before
+ // firing, and trying to pause it again could cause a race
+ // condition between this loop, which calls tryDecrementDemand(),
+ // and the thread that calls request(n), which will try to resume
+ // reading.
+ try {
+ while(!readScheduler.isStopped()) {
+ if (completed) return;
+
+ // make sure we have a subscriber
+ if (handlePending()) {
+ debug.log(Level.DEBUG, "pending subscriber subscribed");
+ return;
+ }
+
+ // If an error was signaled, we might not be in the
+ // the selector thread, and that is OK, because we
+ // will just call onError and return.
+ ReadSubscription current = subscription;
+ TubeSubscriber subscriber = current.subscriber;
+ Throwable error = errorRef.get();
+ if (error != null) {
+ completed = true;
+ // safe to pause here because we're finished anyway.
+ pauseReadEvent();
+ debug.log(Level.DEBUG, () -> "Sending error " + error
+ + " to subscriber " + subscriber);
+ current.errorRef.compareAndSet(null, error);
+ current.signalCompletion();
+ readScheduler.stop();
+ debugState("leaving read() loop with error: ");
+ return;
+ }
+
+ // If we reach here then we must be in the selector thread.
+ assert client.isSelectorThread();
+ if (demand.tryDecrement()) {
+ // we have demand.
+ try {
+ List<ByteBuffer> bytes = readAvailable();
+ if (bytes == EOF) {
+ if (!completed) {
+ debug.log(Level.DEBUG, "got read EOF");
+ completed = true;
+ // safe to pause here because we're finished
+ // anyway.
+ pauseReadEvent();
+ current.signalCompletion();
+ readScheduler.stop();
+ }
+ debugState("leaving read() loop after EOF: ");
+ return;
+ } else if (Utils.remaining(bytes) > 0) {
+ // the subscriber is responsible for offloading
+ // to another thread if needed.
+ debug.log(Level.DEBUG, () -> "read bytes: "
+ + Utils.remaining(bytes));
+ assert !current.completed;
+ subscriber.onNext(bytes);
+ // we could continue looping until the demand
+ // reaches 0. However, that would risk starving
+ // other connections (bound to other socket
+ // channels) - as other selected keys activated
+ // by the selector manager thread might be
+ // waiting for this event to terminate.
+ // So resume the read event and return now...
+ resumeReadEvent();
+ debugState("leaving read() loop after onNext: ");
+ return;
+ } else {
+ // nothing available!
+ debug.log(Level.DEBUG, "no more bytes available");
+ // re-increment the demand and resume the read
+ // event. This ensures that this loop is
+ // executed again when the socket becomes
+ // readable again.
+ demand.increase(1);
+ resumeReadEvent();
+ debugState("leaving read() loop with no bytes");
+ return;
+ }
+ } catch (Throwable x) {
+ signalError(x);
+ continue;
+ }
+ } else {
+ debug.log(Level.DEBUG, "no more demand for reading");
+ // the event is paused just after firing, so it should
+ // still be paused here, unless the demand was just
+ // incremented from 0 to n, in which case, the
+ // event will be resumed, causing this loop to be
+ // invoked again when the socket becomes readable:
+ // This is what we want.
+ // Trying to pause the event here would actually
+ // introduce a race condition between this loop and
+ // request(n).
+ debugState("leaving read() loop with no demand");
+ break;
+ }
+ }
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG, "Unexpected exception in read loop", t);
+ signalError(t);
+ } finally {
+ handlePending();
+ }
+ }
+
+ boolean handlePending() {
+ ReadSubscription pending = pendingSubscription.getAndSet(null);
+ if (pending == null) return false;
+ debug.log(Level.DEBUG, "handling pending subscription for %s",
+ pending.subscriber);
+ ReadSubscription current = subscription;
+ if (current != null && current != pending && !completed) {
+ current.subscriber.dropSubscription();
+ }
+ debug.log(Level.DEBUG, "read demand reset to 0");
+ subscriptionImpl.demand.reset(); // subscriber will increase demand if it needs to.
+ pending.errorRef.compareAndSet(null, errorRef.get());
+ if (!readScheduler.isStopped()) {
+ subscription = pending;
+ } else {
+ debug.log(Level.DEBUG, "socket tube is already stopped");
+ }
+ debug.log(Level.DEBUG, "calling onSubscribe");
+ pending.signalOnSubscribe();
+ if (completed) {
+ pending.errorRef.compareAndSet(null, errorRef.get());
+ pending.signalCompletion();
+ }
+ return true;
+ }
+ }
+
+
+ // A repeatable ReadEvent which is paused after firing and can
+ // be resumed if required - see SocketFlowEvent;
+ final class ReadEvent extends SocketFlowEvent {
+ final InternalReadSubscription sub;
+ ReadEvent(SocketChannel channel, InternalReadSubscription sub) {
+ super(SelectionKey.OP_READ, channel);
+ this.sub = sub;
+ }
+ @Override
+ protected final void signalEvent() {
+ try {
+ client.eventUpdated(this);
+ sub.signalReadable();
+ } catch(Throwable t) {
+ sub.signalError(t);
+ }
+ }
+
+ @Override
+ protected final void signalError(Throwable error) {
+ sub.signalError(error);
+ }
+
+ @Override
+ System.Logger debug() {
+ return debug;
+ }
+ }
+
+ }
+
+ // ===================================================================== //
+ // Socket Channel Read/Write //
+ // ===================================================================== //
+ static final int MAX_BUFFERS = 3;
+ static final List<ByteBuffer> EOF = List.of();
+
+ private List<ByteBuffer> readAvailable() throws IOException {
+ ByteBuffer buf = buffersSource.get();
+ assert buf.hasRemaining();
+
+ int read;
+ int pos = buf.position();
+ List<ByteBuffer> list = null;
+ while (buf.hasRemaining()) {
+ while ((read = channel.read(buf)) > 0) {
+ if (!buf.hasRemaining()) break;
+ }
+
+ // nothing read;
+ if (buf.position() == pos) {
+ // An empty list signal the end of data, and should only be
+ // returned if read == -1.
+ // If we already read some data, then we must return what we have
+ // read, and -1 will be returned next time the caller attempts to
+ // read something.
+ if (list == null && read == -1) { // eof
+ list = EOF;
+ break;
+ }
+ }
+ buf.limit(buf.position());
+ buf.position(pos);
+ if (list == null) {
+ list = List.of(buf);
+ } else {
+ if (!(list instanceof ArrayList)) {
+ list = new ArrayList<>(list);
+ }
+ list.add(buf);
+ }
+ if (read <= 0 || list.size() == MAX_BUFFERS) break;
+ buf = buffersSource.get();
+ pos = buf.position();
+ assert buf.hasRemaining();
+ }
+ return list;
+ }
+
+ private long writeAvailable(List<ByteBuffer> bytes) throws IOException {
+ ByteBuffer[] srcs = bytes.toArray(Utils.EMPTY_BB_ARRAY);
+ final long remaining = Utils.remaining(srcs);
+ long written = 0;
+ while (remaining > written) {
+ long w = channel.write(srcs);
+ if (w == -1 && written == 0) return -1;
+ if (w == 0) break;
+ written += w;
+ }
+ return written;
+ }
+
+ private void resumeEvent(SocketFlowEvent event,
+ Consumer<Throwable> errorSignaler) {
+ boolean registrationRequired;
+ synchronized(lock) {
+ registrationRequired = !event.registered();
+ event.resume();
+ }
+ try {
+ if (registrationRequired) {
+ client.registerEvent(event);
+ } else {
+ client.eventUpdated(event);
+ }
+ } catch(Throwable t) {
+ errorSignaler.accept(t);
+ }
+ }
+
+ private void pauseEvent(SocketFlowEvent event,
+ Consumer<Throwable> errorSignaler) {
+ synchronized(lock) {
+ event.pause();
+ }
+ try {
+ client.eventUpdated(event);
+ } catch(Throwable t) {
+ errorSignaler.accept(t);
+ }
+ }
+
+ @Override
+ public void connectFlows(TubePublisher writePublisher,
+ TubeSubscriber readSubscriber) {
+ debug.log(Level.DEBUG, "connecting flows");
+ this.subscribe(readSubscriber);
+ writePublisher.subscribe(this);
+ }
+
+
+ @Override
+ public String toString() {
+ return dbgString();
+ }
+
+ final String dbgString() {
+ return "SocketTube("+id+")";
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/Stream.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,1170 @@
+/*
+ * 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.incubator.http.internal;
+
+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.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Flow;
+import java.util.concurrent.Flow.Subscription;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiPredicate;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.HttpResponse.BodySubscriber;
+import jdk.incubator.http.internal.common.*;
+import jdk.incubator.http.internal.frame.*;
+import jdk.incubator.http.internal.hpack.DecodingCallback;
+
+/**
+ * Http/2 Stream handling.
+ *
+ * REQUESTS
+ *
+ * sendHeadersOnly() -- assembles HEADERS frame and puts on connection outbound Q
+ *
+ * sendRequest() -- sendHeadersOnly() + sendBody()
+ *
+ * sendBodyAsync() -- calls sendBody() in an executor thread.
+ *
+ * sendHeadersAsync() -- calls sendHeadersOnly() which does not block
+ *
+ * sendRequestAsync() -- calls sendRequest() in an executor thread
+ *
+ * RESPONSES
+ *
+ * Multiple responses can be received per request. Responses are queued up on
+ * a LinkedList of CF<HttpResponse> and the the first one on the list is completed
+ * with the next response
+ *
+ * getResponseAsync() -- queries list of response CFs and returns first one
+ * if one exists. Otherwise, creates one and adds it to list
+ * and returns it. Completion is achieved through the
+ * incoming() upcall from connection reader thread.
+ *
+ * getResponse() -- calls getResponseAsync() and waits for CF to complete
+ *
+ * responseBodyAsync() -- calls responseBody() in an executor thread.
+ *
+ * incoming() -- entry point called from connection reader thread. Frames are
+ * either handled immediately without blocking or for data frames
+ * placed on the stream's inputQ which is consumed by the stream's
+ * reader thread.
+ *
+ * PushedStream sub class
+ * ======================
+ * Sending side methods are not used because the request comes from a PUSH_PROMISE
+ * frame sent by the server. When a PUSH_PROMISE is received the PushedStream
+ * is created. PushedStream does not use responseCF list as there can be only
+ * one response. The CF is created when the object created and when the response
+ * HEADERS frame is received the object is completed.
+ */
+class Stream<T> extends ExchangeImpl<T> {
+
+ final static boolean DEBUG = Utils.DEBUG; // Revisit: temporary developer's flag
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+
+ final ConcurrentLinkedQueue<Http2Frame> inputQ = new ConcurrentLinkedQueue<>();
+ final SequentialScheduler sched =
+ SequentialScheduler.synchronizedScheduler(this::schedule);
+ final SubscriptionBase userSubscription = new SubscriptionBase(sched, this::cancel);
+
+ /**
+ * This stream's identifier. Assigned lazily by the HTTP2Connection before
+ * the stream's first frame is sent.
+ */
+ protected volatile int streamid;
+
+ long requestContentLen;
+
+ final Http2Connection connection;
+ final HttpRequestImpl request;
+ final DecodingCallback rspHeadersConsumer;
+ HttpHeadersImpl responseHeaders;
+ final HttpHeadersImpl requestPseudoHeaders;
+ volatile HttpResponse.BodySubscriber<T> responseSubscriber;
+ final HttpRequest.BodyPublisher requestPublisher;
+ volatile RequestSubscriber requestSubscriber;
+ volatile int responseCode;
+ volatile Response response;
+ volatile Throwable failed; // The exception with which this stream was canceled.
+ final CompletableFuture<Void> requestBodyCF = new MinimalFuture<>();
+ volatile CompletableFuture<T> responseBodyCF;
+
+ /** True if END_STREAM has been seen in a frame received on this stream. */
+ private volatile boolean remotelyClosed;
+ private volatile boolean closed;
+ private volatile boolean endStreamSent;
+
+ // state flags
+ private boolean requestSent, responseReceived;
+
+ /**
+ * A reference to this Stream's connection Send Window controller. The
+ * stream MUST acquire the appropriate amount of Send Window before
+ * sending any data. Will be null for PushStreams, as they cannot send data.
+ */
+ private final WindowController windowController;
+ private final WindowUpdateSender windowUpdater;
+
+ @Override
+ HttpConnection connection() {
+ return connection.connection;
+ }
+
+ /**
+ * Invoked either from incoming() -> {receiveDataFrame() or receiveResetFrame() }
+ * of after user subscription window has re-opened, from SubscriptionBase.request()
+ */
+ private void schedule() {
+ if (responseSubscriber == null)
+ // can't process anything yet
+ return;
+
+ try {
+ while (!inputQ.isEmpty()) {
+ Http2Frame frame = inputQ.peek();
+ if (frame instanceof ResetFrame) {
+ inputQ.remove();
+ handleReset((ResetFrame)frame);
+ return;
+ }
+ DataFrame df = (DataFrame)frame;
+ boolean finished = df.getFlag(DataFrame.END_STREAM);
+
+ List<ByteBuffer> buffers = df.getData();
+ List<ByteBuffer> dsts = Collections.unmodifiableList(buffers);
+ int size = Utils.remaining(dsts, Integer.MAX_VALUE);
+ if (size == 0 && finished) {
+ inputQ.remove();
+ Log.logTrace("responseSubscriber.onComplete");
+ debug.log(Level.DEBUG, "incoming: onComplete");
+ sched.stop();
+ responseSubscriber.onComplete();
+ setEndStreamReceived();
+ return;
+ } else if (userSubscription.tryDecrement()) {
+ inputQ.remove();
+ Log.logTrace("responseSubscriber.onNext {0}", size);
+ debug.log(Level.DEBUG, "incoming: onNext(%d)", size);
+ responseSubscriber.onNext(dsts);
+ if (consumed(df)) {
+ Log.logTrace("responseSubscriber.onComplete");
+ debug.log(Level.DEBUG, "incoming: onComplete");
+ sched.stop();
+ responseSubscriber.onComplete();
+ setEndStreamReceived();
+ return;
+ }
+ } else {
+ return;
+ }
+ }
+ } catch (Throwable throwable) {
+ failed = throwable;
+ }
+
+ Throwable t = failed;
+ if (t != null) {
+ sched.stop();
+ responseSubscriber.onError(t);
+ close();
+ }
+ }
+
+ // Callback invoked after the Response BodySubscriber has consumed the
+ // buffers contained in a DataFrame.
+ // Returns true if END_STREAM is reached, false otherwise.
+ private boolean consumed(DataFrame df) {
+ // RFC 7540 6.1:
+ // The entire DATA frame payload is included in flow control,
+ // including the Pad Length and Padding fields if present
+ int len = df.payloadLength();
+ connection.windowUpdater.update(len);
+
+ if (!df.getFlag(DataFrame.END_STREAM)) {
+ // Don't send window update on a stream which is
+ // closed or half closed.
+ windowUpdater.update(len);
+ return false; // more data coming
+ }
+ return true; // end of stream
+ }
+
+ @Override
+ CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
+ boolean returnConnectionToPool,
+ Executor executor)
+ {
+ Log.logTrace("Reading body on stream {0}", streamid);
+ BodySubscriber<T> bodySubscriber = handler.apply(responseCode, responseHeaders);
+ CompletableFuture<T> cf = receiveData(bodySubscriber);
+
+ PushGroup<?> pg = exchange.getPushGroup();
+ if (pg != null) {
+ // if an error occurs make sure it is recorded in the PushGroup
+ cf = cf.whenComplete((t,e) -> pg.pushError(e));
+ }
+ return cf;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("streamid: ")
+ .append(streamid);
+ return sb.toString();
+ }
+
+ private void receiveDataFrame(DataFrame df) {
+ inputQ.add(df);
+ sched.runOrSchedule();
+ }
+
+ /** Handles a RESET frame. RESET is always handled inline in the queue. */
+ private void receiveResetFrame(ResetFrame frame) {
+ inputQ.add(frame);
+ sched.runOrSchedule();
+ }
+
+ // pushes entire response body into response subscriber
+ // blocking when required by local or remote flow control
+ CompletableFuture<T> receiveData(BodySubscriber<T> bodySubscriber) {
+ responseBodyCF = MinimalFuture.of(bodySubscriber.getBody());
+
+ if (isCanceled()) {
+ Throwable t = getCancelCause();
+ responseBodyCF.completeExceptionally(t);
+ } else {
+ bodySubscriber.onSubscribe(userSubscription);
+ }
+ // Set the responseSubscriber field now that onSubscribe has been called.
+ // This effectively allows the scheduler to start invoking the callbacks.
+ responseSubscriber = bodySubscriber;
+ sched.runOrSchedule(); // in case data waiting already to be processed
+ return responseBodyCF;
+ }
+
+ @Override
+ CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
+ return sendBodyImpl().thenApply( v -> this);
+ }
+
+ @SuppressWarnings("unchecked")
+ Stream(Http2Connection connection,
+ Exchange<T> e,
+ WindowController windowController)
+ {
+ super(e);
+ this.connection = connection;
+ this.windowController = windowController;
+ this.request = e.request();
+ this.requestPublisher = request.requestPublisher; // may be null
+ responseHeaders = new HttpHeadersImpl();
+ rspHeadersConsumer = (name, value) -> {
+ responseHeaders.addHeader(name.toString(), value.toString());
+ if (Log.headers() && Log.trace()) {
+ Log.logTrace("RECEIVED HEADER (streamid={0}): {1}: {2}",
+ streamid, name, value);
+ }
+ };
+ this.requestPseudoHeaders = new HttpHeadersImpl();
+ // NEW
+ this.windowUpdater = new StreamWindowUpdateSender(connection);
+ }
+
+ /**
+ * Entry point from Http2Connection reader thread.
+ *
+ * Data frames will be removed by response body thread.
+ */
+ void incoming(Http2Frame frame) throws IOException {
+ debug.log(Level.DEBUG, "incoming: %s", frame);
+ if ((frame instanceof HeaderFrame)) {
+ HeaderFrame hframe = (HeaderFrame)frame;
+ if (hframe.endHeaders()) {
+ Log.logTrace("handling response (streamid={0})", streamid);
+ handleResponse();
+ if (hframe.getFlag(HeaderFrame.END_STREAM)) {
+ receiveDataFrame(new DataFrame(streamid, DataFrame.END_STREAM, List.of()));
+ }
+ }
+ } else if (frame instanceof DataFrame) {
+ receiveDataFrame((DataFrame)frame);
+ } else {
+ otherFrame(frame);
+ }
+ }
+
+ void otherFrame(Http2Frame frame) throws IOException {
+ switch (frame.type()) {
+ case WindowUpdateFrame.TYPE:
+ incoming_windowUpdate((WindowUpdateFrame) frame);
+ break;
+ case ResetFrame.TYPE:
+ incoming_reset((ResetFrame) frame);
+ break;
+ case PriorityFrame.TYPE:
+ incoming_priority((PriorityFrame) frame);
+ break;
+ default:
+ String msg = "Unexpected frame: " + frame.toString();
+ throw new IOException(msg);
+ }
+ }
+
+ // The Hpack decoder decodes into one of these consumers of name,value pairs
+
+ DecodingCallback rspHeadersConsumer() {
+ return rspHeadersConsumer;
+ }
+
+ protected void handleResponse() throws IOException {
+ responseCode = (int)responseHeaders
+ .firstValueAsLong(":status")
+ .orElseThrow(() -> new IOException("no statuscode in response"));
+
+ response = new Response(
+ request, exchange, responseHeaders,
+ responseCode, HttpClient.Version.HTTP_2);
+
+ /* TODO: review if needs to be removed
+ the value is not used, but in case `content-length` doesn't parse as
+ long, there will be NumberFormatException. If left as is, make sure
+ code up the stack handles NFE correctly. */
+ responseHeaders.firstValueAsLong("content-length");
+
+ if (Log.headers()) {
+ StringBuilder sb = new StringBuilder("RESPONSE HEADERS:\n");
+ Log.dumpHeaders(sb, " ", responseHeaders);
+ Log.logHeaders(sb.toString());
+ }
+
+ completeResponse(response);
+ }
+
+ void incoming_reset(ResetFrame frame) {
+ Log.logTrace("Received RST_STREAM on stream {0}", streamid);
+ if (endStreamReceived()) {
+ Log.logTrace("Ignoring RST_STREAM frame received on remotely closed stream {0}", streamid);
+ } else if (closed) {
+ Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
+ } else {
+ // put it in the input queue in order to read all
+ // pending data frames first. Indeed, a server may send
+ // RST_STREAM after sending END_STREAM, in which case we should
+ // ignore it. However, we won't know if we have received END_STREAM
+ // or not until all pending data frames are read.
+ receiveResetFrame(frame);
+ // RST_STREAM was pushed to the queue. It will be handled by
+ // asyncReceive after all pending data frames have been
+ // processed.
+ Log.logTrace("RST_STREAM pushed in queue for stream {0}", streamid);
+ }
+ }
+
+ void handleReset(ResetFrame frame) {
+ Log.logTrace("Handling RST_STREAM on stream {0}", streamid);
+ if (!closed) {
+ close();
+ int error = frame.getErrorCode();
+ completeResponseExceptionally(new IOException(ErrorFrame.stringForCode(error)));
+ } else {
+ Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
+ }
+ }
+
+ void incoming_priority(PriorityFrame frame) {
+ // TODO: implement priority
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ private void incoming_windowUpdate(WindowUpdateFrame frame)
+ throws IOException
+ {
+ int amount = frame.getUpdate();
+ if (amount <= 0) {
+ Log.logTrace("Resetting stream: {0} %d, Window Update amount: %d\n",
+ streamid, streamid, amount);
+ connection.resetStream(streamid, ResetFrame.FLOW_CONTROL_ERROR);
+ } else {
+ assert streamid != 0;
+ boolean success = windowController.increaseStreamWindow(amount, streamid);
+ if (!success) { // overflow
+ connection.resetStream(streamid, ResetFrame.FLOW_CONTROL_ERROR);
+ }
+ }
+ }
+
+ void incoming_pushPromise(HttpRequestImpl pushRequest,
+ PushedStream<T> pushStream)
+ throws IOException
+ {
+ if (Log.requests()) {
+ Log.logRequest("PUSH_PROMISE: " + pushRequest.toString());
+ }
+ PushGroup<T> pushGroup = exchange.getPushGroup();
+ if (pushGroup == null) {
+ Log.logTrace("Rejecting push promise stream " + streamid);
+ connection.resetStream(pushStream.streamid, ResetFrame.REFUSED_STREAM);
+ pushStream.close();
+ return;
+ }
+
+ PushGroup.Acceptor<T> acceptor = pushGroup.acceptPushRequest(pushRequest);
+
+ if (!acceptor.accepted()) {
+ // cancel / reject
+ IOException ex = new IOException("Stream " + streamid + " cancelled by users handler");
+ if (Log.trace()) {
+ Log.logTrace("No body subscriber for {0}: {1}", pushRequest,
+ ex.getMessage());
+ }
+ pushStream.cancelImpl(ex);
+ return;
+ }
+
+ CompletableFuture<HttpResponse<T>> pushResponseCF = acceptor.cf();
+ HttpResponse.BodyHandler<T> pushHandler = acceptor.bodyHandler();
+ assert pushHandler != null;
+
+ pushStream.requestSent();
+ pushStream.setPushHandler(pushHandler); // TODO: could wrap the handler to throw on acceptPushPromise ?
+ // setup housekeeping for when the push is received
+ // TODO: deal with ignoring of CF anti-pattern
+ CompletableFuture<HttpResponse<T>> cf = pushStream.responseCF();
+ cf.whenComplete((HttpResponse<T> resp, Throwable t) -> {
+ t = Utils.getCompletionCause(t);
+ if (Log.trace()) {
+ Log.logTrace("Push completed on stream {0} for {1}{2}",
+ pushStream.streamid, resp,
+ ((t==null) ? "": " with exception " + t));
+ }
+ if (t != null) {
+ pushGroup.pushError(t);
+ pushResponseCF.completeExceptionally(t);
+ } else {
+ pushResponseCF.complete(resp);
+ }
+ pushGroup.pushCompleted();
+ });
+
+ }
+
+ private OutgoingHeaders<Stream<T>> headerFrame(long contentLength) {
+ HttpHeadersImpl h = request.getSystemHeaders();
+ if (contentLength > 0) {
+ h.setHeader("content-length", Long.toString(contentLength));
+ }
+ setPseudoHeaderFields();
+ HttpHeaders sysh = filter(h);
+ HttpHeaders userh = filter(request.getUserHeaders());
+ OutgoingHeaders<Stream<T>> f = new OutgoingHeaders<>(sysh, userh, this);
+ if (contentLength == 0) {
+ f.setFlag(HeadersFrame.END_STREAM);
+ endStreamSent = true;
+ }
+ return f;
+ }
+
+ private boolean hasProxyAuthorization(HttpHeaders headers) {
+ return headers.firstValue("proxy-authorization")
+ .isPresent();
+ }
+
+ // Determines whether we need to build a new HttpHeader object.
+ //
+ // Ideally we should pass the filter to OutgoingHeaders refactor the
+ // code that creates the HeaderFrame to honor the filter.
+ // We're not there yet - so depending on the filter we need to
+ // apply and the content of the header we will try to determine
+ // whether anything might need to be filtered.
+ // If nothing needs filtering then we can just use the
+ // original headers.
+ private boolean needsFiltering(HttpHeaders headers,
+ BiPredicate<String, List<String>> filter) {
+ if (filter == Utils.PROXY_TUNNEL_FILTER || filter == Utils.PROXY_FILTER) {
+ // we're either connecting or proxying
+ // slight optimization: we only need to filter out
+ // disabled schemes, so if there are none just
+ // pass through.
+ return Utils.proxyHasDisabledSchemes(filter == Utils.PROXY_TUNNEL_FILTER)
+ && hasProxyAuthorization(headers);
+ } else {
+ // we're talking to a server, either directly or through
+ // a tunnel.
+ // Slight optimization: we only need to filter out
+ // proxy authorization headers, so if there are none just
+ // pass through.
+ return hasProxyAuthorization(headers);
+ }
+ }
+
+ private HttpHeaders filter(HttpHeaders headers) {
+ HttpConnection conn = connection();
+ BiPredicate<String, List<String>> filter =
+ conn.headerFilter(request);
+ if (needsFiltering(headers, filter)) {
+ return ImmutableHeaders.of(headers.map(), filter);
+ }
+ return headers;
+ }
+
+ private void setPseudoHeaderFields() {
+ HttpHeadersImpl hdrs = requestPseudoHeaders;
+ String method = request.method();
+ hdrs.setHeader(":method", method);
+ URI uri = request.uri();
+ hdrs.setHeader(":scheme", uri.getScheme());
+ // TODO: userinfo deprecated. Needs to be removed
+ hdrs.setHeader(":authority", uri.getAuthority());
+ // TODO: ensure header names beginning with : not in user headers
+ String query = uri.getQuery();
+ String path = uri.getPath();
+ if (path == null || path.isEmpty()) {
+ if (method.equalsIgnoreCase("OPTIONS")) {
+ path = "*";
+ } else {
+ path = "/";
+ }
+ }
+ if (query != null) {
+ path += "?" + query;
+ }
+ hdrs.setHeader(":path", path);
+ }
+
+ HttpHeadersImpl getRequestPseudoHeaders() {
+ return requestPseudoHeaders;
+ }
+
+ /** Sets endStreamReceived. Should be called only once. */
+ void setEndStreamReceived() {
+ assert remotelyClosed == false: "Unexpected endStream already set";
+ remotelyClosed = true;
+ responseReceived();
+ }
+
+ /** Tells whether, or not, the END_STREAM Flag has been seen in any frame
+ * received on this stream. */
+ private boolean endStreamReceived() {
+ return remotelyClosed;
+ }
+
+ @Override
+ CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
+ debug.log(Level.DEBUG, "sendHeadersOnly()");
+ if (Log.requests() && request != null) {
+ Log.logRequest(request.toString());
+ }
+ if (requestPublisher != null) {
+ requestContentLen = requestPublisher.contentLength();
+ } else {
+ requestContentLen = 0;
+ }
+ OutgoingHeaders<Stream<T>> f = headerFrame(requestContentLen);
+ connection.sendFrame(f);
+ CompletableFuture<ExchangeImpl<T>> cf = new MinimalFuture<>();
+ cf.complete(this); // #### good enough for now
+ return cf;
+ }
+
+ @Override
+ void released() {
+ if (streamid > 0) {
+ debug.log(Level.DEBUG, "Released stream %d", streamid);
+ // remove this stream from the Http2Connection map.
+ connection.closeStream(streamid);
+ } else {
+ debug.log(Level.DEBUG, "Can't release stream %d", streamid);
+ }
+ }
+
+ @Override
+ void completed() {
+ // There should be nothing to do here: the stream should have
+ // been already closed (or will be closed shortly after).
+ }
+
+ void registerStream(int id) {
+ this.streamid = id;
+ connection.putStream(this, streamid);
+ debug.log(Level.DEBUG, "Registered stream %d", id);
+ }
+
+ void signalWindowUpdate() {
+ RequestSubscriber subscriber = requestSubscriber;
+ assert subscriber != null;
+ debug.log(Level.DEBUG, "Signalling window update");
+ subscriber.sendScheduler.runOrSchedule();
+ }
+
+ static final ByteBuffer COMPLETED = ByteBuffer.allocate(0);
+ class RequestSubscriber implements Flow.Subscriber<ByteBuffer> {
+ // can be < 0 if the actual length is not known.
+ private final long contentLength;
+ private volatile long remainingContentLength;
+ private volatile Subscription subscription;
+
+ // Holds the outgoing data. There will be at most 2 outgoing ByteBuffers.
+ // 1) The data that was published by the request body Publisher, and
+ // 2) the COMPLETED sentinel, since onComplete can be invoked without demand.
+ final ConcurrentLinkedDeque<ByteBuffer> outgoing = new ConcurrentLinkedDeque<>();
+
+ private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+ // A scheduler used to honor window updates. Writing must be paused
+ // when the window is exhausted, and resumed when the window acquires
+ // some space. The sendScheduler makes it possible to implement this
+ // behaviour in an asynchronous non-blocking way.
+ // See RequestSubscriber::trySend below.
+ final SequentialScheduler sendScheduler;
+
+ RequestSubscriber(long contentLen) {
+ this.contentLength = contentLen;
+ this.remainingContentLength = contentLen;
+ this.sendScheduler =
+ SequentialScheduler.synchronizedScheduler(this::trySend);
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ if (this.subscription != null) {
+ throw new IllegalStateException("already subscribed");
+ }
+ this.subscription = subscription;
+ debug.log(Level.DEBUG, "RequestSubscriber: onSubscribe, request 1");
+ subscription.request(1);
+ }
+
+ @Override
+ public void onNext(ByteBuffer item) {
+ debug.log(Level.DEBUG, "RequestSubscriber: onNext(%d)", item.remaining());
+ int size = outgoing.size();
+ assert size == 0 : "non-zero size: " + size;
+ onNextImpl(item);
+ }
+
+ private void onNextImpl(ByteBuffer item) {
+ // Got some more request body bytes to send.
+ if (requestBodyCF.isDone()) {
+ // stream already cancelled, probably in timeout
+ sendScheduler.stop();
+ subscription.cancel();
+ return;
+ }
+ outgoing.add(item);
+ sendScheduler.runOrSchedule();
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ debug.log(Level.DEBUG, () -> "RequestSubscriber: onError: " + throwable);
+ // ensure that errors are handled within the flow.
+ if (errorRef.compareAndSet(null, throwable)) {
+ sendScheduler.runOrSchedule();
+ }
+ }
+
+ @Override
+ public void onComplete() {
+ debug.log(Level.DEBUG, "RequestSubscriber: onComplete");
+ int size = outgoing.size();
+ assert size == 0 || size == 1 : "non-zero or one size: " + size;
+ // last byte of request body has been obtained.
+ // ensure that everything is completed within the flow.
+ onNextImpl(COMPLETED);
+ }
+
+ // Attempts to send the data, if any.
+ // Handles errors and completion state.
+ // Pause writing if the send window is exhausted, resume it if the
+ // send window has some bytes that can be acquired.
+ void trySend() {
+ try {
+ // handle errors raised by onError;
+ Throwable t = errorRef.get();
+ if (t != null) {
+ sendScheduler.stop();
+ if (requestBodyCF.isDone()) return;
+ subscription.cancel();
+ requestBodyCF.completeExceptionally(t);
+ return;
+ }
+
+ do {
+ // handle COMPLETED;
+ ByteBuffer item = outgoing.peekFirst();
+ if (item == null) return;
+ else if (item == COMPLETED) {
+ sendScheduler.stop();
+ complete();
+ return;
+ }
+
+ // handle bytes to send downstream
+ while (item.hasRemaining()) {
+ debug.log(Level.DEBUG, "trySend: %d", item.remaining());
+ assert !endStreamSent : "internal error, send data after END_STREAM flag";
+ DataFrame df = getDataFrame(item);
+ if (df == null) {
+ debug.log(Level.DEBUG, "trySend: can't send yet: %d",
+ item.remaining());
+ return; // the send window is exhausted: come back later
+ }
+
+ if (contentLength > 0) {
+ remainingContentLength -= df.getDataLength();
+ if (remainingContentLength < 0) {
+ String msg = connection().getConnectionFlow()
+ + " stream=" + streamid + " "
+ + "[" + Thread.currentThread().getName() + "] "
+ + "Too many bytes in request body. Expected: "
+ + contentLength + ", got: "
+ + (contentLength - remainingContentLength);
+ connection.resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
+ throw new IOException(msg);
+ } else if (remainingContentLength == 0) {
+ df.setFlag(DataFrame.END_STREAM);
+ endStreamSent = true;
+ }
+ }
+ debug.log(Level.DEBUG, "trySend: sending: %d", df.getDataLength());
+ connection.sendDataFrame(df);
+ }
+ assert !item.hasRemaining();
+ ByteBuffer b = outgoing.removeFirst();
+ assert b == item;
+ } while (outgoing.peekFirst() != null);
+
+ debug.log(Level.DEBUG, "trySend: request 1");
+ subscription.request(1);
+ } catch (Throwable ex) {
+ debug.log(Level.DEBUG, "trySend: ", ex);
+ sendScheduler.stop();
+ subscription.cancel();
+ requestBodyCF.completeExceptionally(ex);
+ }
+ }
+
+ private void complete() throws IOException {
+ long remaining = remainingContentLength;
+ long written = contentLength - remaining;
+ if (remaining > 0) {
+ connection.resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
+ // let trySend() handle the exception
+ throw new IOException(connection().getConnectionFlow()
+ + " stream=" + streamid + " "
+ + "[" + Thread.currentThread().getName() +"] "
+ + "Too few bytes returned by the publisher ("
+ + written + "/"
+ + contentLength + ")");
+ }
+ if (!endStreamSent) {
+ endStreamSent = true;
+ connection.sendDataFrame(getEmptyEndStreamDataFrame());
+ }
+ requestBodyCF.complete(null);
+ }
+ }
+
+ /**
+ * Send a RESET frame to tell server to stop sending data on this stream
+ */
+ @Override
+ public CompletableFuture<Void> ignoreBody() {
+ try {
+ connection.resetStream(streamid, ResetFrame.STREAM_CLOSED);
+ return MinimalFuture.completedFuture(null);
+ } catch (Throwable e) {
+ Log.logTrace("Error resetting stream {0}", e.toString());
+ return MinimalFuture.failedFuture(e);
+ }
+ }
+
+ DataFrame getDataFrame(ByteBuffer buffer) {
+ int requestAmount = Math.min(connection.getMaxSendFrameSize(), buffer.remaining());
+ // blocks waiting for stream send window, if exhausted
+ int actualAmount = windowController.tryAcquire(requestAmount, streamid, this);
+ if (actualAmount <= 0) return null;
+ ByteBuffer outBuf = Utils.sliceWithLimitedCapacity(buffer, actualAmount);
+ DataFrame df = new DataFrame(streamid, 0 , outBuf);
+ return df;
+ }
+
+ private DataFrame getEmptyEndStreamDataFrame() {
+ return new DataFrame(streamid, DataFrame.END_STREAM, List.of());
+ }
+
+ /**
+ * A List of responses relating to this stream. Normally there is only
+ * one response, but intermediate responses like 100 are allowed
+ * and must be passed up to higher level before continuing. Deals with races
+ * such as if responses are returned before the CFs get created by
+ * getResponseAsync()
+ */
+
+ final List<CompletableFuture<Response>> response_cfs = new ArrayList<>(5);
+
+ @Override
+ CompletableFuture<Response> getResponseAsync(Executor executor) {
+ CompletableFuture<Response> cf;
+ // The code below deals with race condition that can be caused when
+ // completeResponse() is being called before getResponseAsync()
+ synchronized (response_cfs) {
+ if (!response_cfs.isEmpty()) {
+ // This CompletableFuture was created by completeResponse().
+ // it will be already completed.
+ cf = response_cfs.remove(0);
+ // if we find a cf here it should be already completed.
+ // finding a non completed cf should not happen. just assert it.
+ assert cf.isDone() : "Removing uncompleted response: could cause code to hang!";
+ } else {
+ // getResponseAsync() is called first. Create a CompletableFuture
+ // that will be completed by completeResponse() when
+ // completeResponse() is called.
+ cf = new MinimalFuture<>();
+ response_cfs.add(cf);
+ }
+ }
+ if (executor != null && !cf.isDone()) {
+ // protect from executing later chain of CompletableFuture operations from SelectorManager thread
+ cf = cf.thenApplyAsync(r -> r, executor);
+ }
+ Log.logTrace("Response future (stream={0}) is: {1}", streamid, cf);
+ PushGroup<?> pg = exchange.getPushGroup();
+ if (pg != null) {
+ // if an error occurs make sure it is recorded in the PushGroup
+ cf = cf.whenComplete((t,e) -> pg.pushError(Utils.getCompletionCause(e)));
+ }
+ return cf;
+ }
+
+ /**
+ * Completes the first uncompleted CF on list, and removes it. If there is no
+ * uncompleted CF then creates one (completes it) and adds to list
+ */
+ void completeResponse(Response resp) {
+ synchronized (response_cfs) {
+ CompletableFuture<Response> cf;
+ int cfs_len = response_cfs.size();
+ for (int i=0; i<cfs_len; i++) {
+ cf = response_cfs.get(i);
+ if (!cf.isDone()) {
+ Log.logTrace("Completing response (streamid={0}): {1}",
+ streamid, cf);
+ cf.complete(resp);
+ response_cfs.remove(cf);
+ return;
+ } // else we found the previous response: just leave it alone.
+ }
+ cf = MinimalFuture.completedFuture(resp);
+ Log.logTrace("Created completed future (streamid={0}): {1}",
+ streamid, cf);
+ response_cfs.add(cf);
+ }
+ }
+
+ // methods to update state and remove stream when finished
+
+ synchronized void requestSent() {
+ requestSent = true;
+ if (responseReceived) {
+ close();
+ }
+ }
+
+ synchronized void responseReceived() {
+ responseReceived = true;
+ if (requestSent) {
+ close();
+ }
+ }
+
+ /**
+ * same as above but for errors
+ */
+ void completeResponseExceptionally(Throwable t) {
+ synchronized (response_cfs) {
+ // use index to avoid ConcurrentModificationException
+ // caused by removing the CF from within the loop.
+ for (int i = 0; i < response_cfs.size(); i++) {
+ CompletableFuture<Response> cf = response_cfs.get(i);
+ if (!cf.isDone()) {
+ cf.completeExceptionally(t);
+ response_cfs.remove(i);
+ return;
+ }
+ }
+ response_cfs.add(MinimalFuture.failedFuture(t));
+ }
+ }
+
+ CompletableFuture<Void> sendBodyImpl() {
+ requestBodyCF.whenComplete((v, t) -> requestSent());
+ if (requestPublisher != null) {
+ final RequestSubscriber subscriber = new RequestSubscriber(requestContentLen);
+ requestPublisher.subscribe(requestSubscriber = subscriber);
+ } else {
+ // there is no request body, therefore the request is complete,
+ // END_STREAM has already sent with outgoing headers
+ requestBodyCF.complete(null);
+ }
+ return requestBodyCF;
+ }
+
+ @Override
+ void cancel() {
+ cancel(new IOException("Stream " + streamid + " cancelled"));
+ }
+
+ @Override
+ void cancel(IOException cause) {
+ cancelImpl(cause);
+ }
+
+ // This method sends a RST_STREAM frame
+ void cancelImpl(Throwable e) {
+ debug.log(Level.DEBUG, "cancelling stream {0}: {1}", streamid, e);
+ if (Log.trace()) {
+ Log.logTrace("cancelling stream {0}: {1}\n", streamid, e);
+ }
+ boolean closing;
+ if (closing = !closed) { // assigning closing to !closed
+ synchronized (this) {
+ failed = e;
+ if (closing = !closed) { // assigning closing to !closed
+ closed=true;
+ }
+ }
+ }
+ if (closing) { // true if the stream has not been closed yet
+ if (responseSubscriber != null)
+ sched.runOrSchedule();
+ }
+ completeResponseExceptionally(e);
+ if (!requestBodyCF.isDone()) {
+ requestBodyCF.completeExceptionally(e); // we may be sending the body..
+ }
+ if (responseBodyCF != null) {
+ responseBodyCF.completeExceptionally(e);
+ }
+ try {
+ // will send a RST_STREAM frame
+ if (streamid != 0) {
+ connection.resetStream(streamid, ResetFrame.CANCEL);
+ }
+ } catch (IOException ex) {
+ Log.logError(ex);
+ }
+ }
+
+ // This method doesn't send any frame
+ void close() {
+ if (closed) return;
+ synchronized(this) {
+ if (closed) return;
+ closed = true;
+ }
+ Log.logTrace("Closing stream {0}", streamid);
+ connection.closeStream(streamid);
+ Log.logTrace("Stream {0} closed", streamid);
+ }
+
+ static class PushedStream<T> extends Stream<T> {
+ final PushGroup<T> pushGroup;
+ // push streams need the response CF allocated up front as it is
+ // given directly to user via the multi handler callback function.
+ final CompletableFuture<Response> pushCF;
+ CompletableFuture<HttpResponse<T>> responseCF;
+ final HttpRequestImpl pushReq;
+ HttpResponse.BodyHandler<T> pushHandler;
+
+ PushedStream(PushGroup<T> pushGroup,
+ Http2Connection connection,
+ Exchange<T> pushReq) {
+ // ## no request body possible, null window controller
+ super(connection, pushReq, null);
+ this.pushGroup = pushGroup;
+ this.pushReq = pushReq.request();
+ this.pushCF = new MinimalFuture<>();
+ this.responseCF = new MinimalFuture<>();
+
+ }
+
+ CompletableFuture<HttpResponse<T>> responseCF() {
+ return responseCF;
+ }
+
+ synchronized void setPushHandler(HttpResponse.BodyHandler<T> pushHandler) {
+ this.pushHandler = pushHandler;
+ }
+
+ synchronized HttpResponse.BodyHandler<T> getPushHandler() {
+ // ignored parameters to function can be used as BodyHandler
+ return this.pushHandler;
+ }
+
+ // Following methods call the super class but in case of
+ // error record it in the PushGroup. The error method is called
+ // with a null value when no error occurred (is a no-op)
+ @Override
+ CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
+ return super.sendBodyAsync()
+ .whenComplete((ExchangeImpl<T> v, Throwable t)
+ -> pushGroup.pushError(Utils.getCompletionCause(t)));
+ }
+
+ @Override
+ CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
+ return super.sendHeadersAsync()
+ .whenComplete((ExchangeImpl<T> ex, Throwable t)
+ -> pushGroup.pushError(Utils.getCompletionCause(t)));
+ }
+
+ @Override
+ CompletableFuture<Response> getResponseAsync(Executor executor) {
+ CompletableFuture<Response> cf = pushCF.whenComplete(
+ (v, t) -> pushGroup.pushError(Utils.getCompletionCause(t)));
+ if(executor!=null && !cf.isDone()) {
+ cf = cf.thenApplyAsync( r -> r, executor);
+ }
+ return cf;
+ }
+
+ @Override
+ CompletableFuture<T> readBodyAsync(
+ HttpResponse.BodyHandler<T> handler,
+ boolean returnConnectionToPool,
+ Executor executor)
+ {
+ return super.readBodyAsync(handler, returnConnectionToPool, executor)
+ .whenComplete((v, t) -> pushGroup.pushError(t));
+ }
+
+ @Override
+ void completeResponse(Response r) {
+ Log.logResponse(r::toString);
+ pushCF.complete(r); // not strictly required for push API
+ // start reading the body using the obtained BodySubscriber
+ CompletableFuture<Void> start = new MinimalFuture<>();
+ start.thenCompose( v -> readBodyAsync(getPushHandler(), false, getExchange().executor()))
+ .whenComplete((T body, Throwable t) -> {
+ if (t != null) {
+ responseCF.completeExceptionally(t);
+ } else {
+ HttpResponseImpl<T> resp =
+ new HttpResponseImpl<>(r.request, r, null, body, getExchange());
+ responseCF.complete(resp);
+ }
+ });
+ start.completeAsync(() -> null, getExchange().executor());
+ }
+
+ @Override
+ void completeResponseExceptionally(Throwable t) {
+ pushCF.completeExceptionally(t);
+ }
+
+// @Override
+// synchronized void responseReceived() {
+// super.responseReceived();
+// }
+
+ // create and return the PushResponseImpl
+ @Override
+ protected void handleResponse() {
+ responseCode = (int)responseHeaders
+ .firstValueAsLong(":status")
+ .orElse(-1);
+
+ if (responseCode == -1) {
+ completeResponseExceptionally(new IOException("No status code"));
+ }
+
+ this.response = new Response(
+ pushReq, exchange, responseHeaders,
+ responseCode, HttpClient.Version.HTTP_2);
+
+ /* TODO: review if needs to be removed
+ the value is not used, but in case `content-length` doesn't parse
+ as long, there will be NumberFormatException. If left as is, make
+ sure code up the stack handles NFE correctly. */
+ responseHeaders.firstValueAsLong("content-length");
+
+ if (Log.headers()) {
+ StringBuilder sb = new StringBuilder("RESPONSE HEADERS");
+ sb.append(" (streamid=").append(streamid).append("): ");
+ Log.dumpHeaders(sb, " ", responseHeaders);
+ Log.logHeaders(sb.toString());
+ }
+
+ // different implementations for normal streams and pushed streams
+ completeResponse(response);
+ }
+ }
+
+ final class StreamWindowUpdateSender extends WindowUpdateSender {
+
+ StreamWindowUpdateSender(Http2Connection connection) {
+ super(connection);
+ }
+
+ @Override
+ int getStreamId() {
+ return streamid;
+ }
+ }
+
+ /**
+ * Returns true if this exchange was canceled.
+ * @return true if this exchange was canceled.
+ */
+ synchronized boolean isCanceled() {
+ return failed != null;
+ }
+
+ /**
+ * Returns the cause for which this exchange was canceled, if available.
+ * @return the cause for which this exchange was canceled, if available.
+ */
+ synchronized Throwable getCancelCause() {
+ return failed;
+ }
+
+ final String dbgString() {
+ return connection.dbgString() + "/Stream("+streamid+")";
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/TimeoutEvent.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+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 + "]";
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/UntrustedBodyHandler.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,34 @@
+/*
+ * 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.incubator.http.internal;
+
+import java.security.AccessControlContext;
+import jdk.incubator.http.HttpResponse;
+
+/** A body handler that is further restricted by a given ACC. */
+public interface UntrustedBodyHandler<T> extends HttpResponse.BodyHandler<T> {
+ void setAccessControlContext(AccessControlContext acc);
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/WindowController.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,320 @@
+/*
+ * 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.incubator.http.internal;
+
+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.incubator.http.internal.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 boolean DEBUG = Utils.DEBUG; // Revisit: temporary developer's flag
+ static final System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("WindowController"::toString, 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 % 2) == 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
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "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;
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "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;
+ DEBUG_LOGGER.log(Level.DEBUG, "Connection window size is now %d", size);
+
+ // 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)
+ throw new InternalError("Expected entry for streamid: " + streamid);
+ size += amount;
+ if (size < 0)
+ return false;
+ streams.put(streamid, size);
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Stream %s window size is now %s", streamid, size);
+
+ 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);
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Stream %s window size is now %s", streamid, size);
+ }
+ }
+ } 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();
+// }
+// }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/WindowUpdateSender.java Tue Feb 06 14:10:28 2018 +0000
@@ -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.incubator.http.internal;
+
+import java.lang.System.Logger.Level;
+import jdk.incubator.http.internal.frame.SettingsFrame;
+import jdk.incubator.http.internal.frame.WindowUpdateFrame;
+import jdk.incubator.http.internal.common.Utils;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+abstract class WindowUpdateSender {
+
+ final static boolean DEBUG = Utils.DEBUG;
+ final System.Logger debug =
+ Utils.getDebugLogger(this::dbgString, 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);
+ debug.log(Level.DEBUG, "maxFrameSize=%d, initWindowSize=%d, limit=%d",
+ maxFrameSize, initWindowSize, limit);
+ }
+
+ abstract int getStreamId();
+
+ void update(int delta) {
+ debug.log(Level.DEBUG, "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) {
+ debug.log(Level.DEBUG, "sending window update: %d", delta);
+ connection.sendUnorderedFrame(new WindowUpdateFrame(getStreamId(), delta));
+ }
+
+ String dbgString() {
+ return "WindowUpdateSender(stream: " + getStreamId() + ")";
+ }
+
+}
--- a/test/jdk/java/net/httpclient/whitebox/ConnectionPoolTestDriver.java Tue Feb 06 11:39:55 2018 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/ConnectionPoolTestDriver.java Tue Feb 06 14:10:28 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * 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
@@ -27,6 +27,9 @@
* @summary Verifies that the ConnectionPool correctly handle
* connection deadlines and purges the right connections
* from the cache.
- * @modules jdk.incubator.httpclient java.management
- * @run main/othervm --add-reads jdk.incubator.httpclient=java.management jdk.incubator.httpclient/jdk.incubator.http.ConnectionPoolTest
+ * @modules jdk.incubator.httpclient/jdk.incubator.http.internal
+ * java.management
+ * @run main/othervm
+ * --add-reads jdk.incubator.httpclient=java.management
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.ConnectionPoolTest
*/
--- a/test/jdk/java/net/httpclient/whitebox/Driver.java Tue Feb 06 11:39:55 2018 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/Driver.java Tue Feb 06 14:10:28 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
+ * 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
@@ -24,7 +24,7 @@
/*
* @test
* @bug 8151299 8164704
- * @modules jdk.incubator.httpclient
- * @run testng jdk.incubator.httpclient/jdk.incubator.http.SelectorTest
- * @run testng jdk.incubator.httpclient/jdk.incubator.http.RawChannelTest
+ * @modules jdk.incubator.httpclient/jdk.incubator.http.internal
+ * @run testng jdk.incubator.httpclient/jdk.incubator.http.internal.SelectorTest
+ * @run testng jdk.incubator.httpclient/jdk.incubator.http.internal.RawChannelTest
*/
--- a/test/jdk/java/net/httpclient/whitebox/FramesDecoderTestDriver.java Tue Feb 06 11:39:55 2018 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/FramesDecoderTestDriver.java Tue Feb 06 14:10:28 2018 +0000
@@ -25,6 +25,8 @@
* @test
* @bug 8195823
* @modules jdk.incubator.httpclient/jdk.incubator.http.internal.frame
- * @run testng/othervm -Djdk.internal.httpclient.debug=true jdk.incubator.httpclient/jdk.incubator.http.internal.frame.FramesDecoderTest
+ * @run testng/othervm
+ * -Djdk.internal.httpclient.debug=true
+ * jdk.incubator.httpclient/jdk.incubator.http.internal.frame.FramesDecoderTest
*/
--- a/test/jdk/java/net/httpclient/whitebox/Http1HeaderParserTestDriver.java Tue Feb 06 11:39:55 2018 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/Http1HeaderParserTestDriver.java Tue Feb 06 14:10:28 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * 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
@@ -24,6 +24,6 @@
/*
* @test
* @bug 8195138
- * @modules jdk.incubator.httpclient
- * @run testng jdk.incubator.httpclient/jdk.incubator.http.Http1HeaderParserTest
+ * @modules jdk.incubator.httpclient/jdk.incubator.http.internal
+ * @run testng jdk.incubator.httpclient/jdk.incubator.http.internal.Http1HeaderParserTest
*/
--- a/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/ConnectionPoolTest.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,249 +0,0 @@
-/*
- * Copyright (c) 2017, 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.
- *
- * 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.incubator.http;
-
-import java.io.IOException;
-import java.lang.management.ManagementFactory;
-import java.net.Authenticator;
-import java.net.CookieHandler;
-import java.net.InetSocketAddress;
-import java.net.ProxySelector;
-import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
-import java.util.List;
-import java.util.Optional;
-import java.util.Random;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Flow;
-import java.util.stream.IntStream;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLParameters;
-import jdk.incubator.http.internal.common.FlowTube;
-
-/**
- * @summary Verifies that the ConnectionPool correctly handle
- * connection deadlines and purges the right connections
- * from the cache.
- * @bug 8187044 8187111
- * @author danielfuchs
- */
-public class ConnectionPoolTest {
-
- static long getActiveCleaners() throws ClassNotFoundException {
- // ConnectionPool.ACTIVE_CLEANER_COUNTER.get()
- // ConnectionPoolTest.class.getModule().addReads(
- // Class.forName("java.lang.management.ManagementFactory").getModule());
- return java.util.stream.Stream.of(ManagementFactory.getThreadMXBean()
- .dumpAllThreads(false, false))
- .filter(t -> t.getThreadName().startsWith("HTTP-Cache-cleaner"))
- .count();
- }
-
- public static void main(String[] args) throws Exception {
- testCacheCleaners();
- }
-
- public static void testCacheCleaners() throws Exception {
- ConnectionPool pool = new ConnectionPool(666);
- HttpClient client = new HttpClientStub(pool);
- InetSocketAddress proxy = InetSocketAddress.createUnresolved("bar", 80);
- System.out.println("Adding 10 connections to pool");
- Random random = new Random();
-
- final int count = 20;
- Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
- int[] keepAlives = new int[count];
- HttpConnectionStub[] connections = new HttpConnectionStub[count];
- long purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
- long expected = 0;
- if (purge != expected) {
- throw new RuntimeException("Bad purge delay: " + purge
- + ", expected " + expected);
- }
- expected = Long.MAX_VALUE;
- for (int i=0; i<count; i++) {
- InetSocketAddress addr = InetSocketAddress.createUnresolved("foo"+i, 80);
- keepAlives[i] = random.nextInt(10) * 10 + 10;
- connections[i] = new HttpConnectionStub(client, addr, proxy, true);
- System.out.println("Adding connection: " + now
- + " keepAlive: " + keepAlives[i]
- + " /" + connections[i]);
- pool.returnToPool(connections[i], now, keepAlives[i]);
- expected = Math.min(expected, keepAlives[i] * 1000);
- purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
- if (purge != expected) {
- throw new RuntimeException("Bad purge delay: " + purge
- + ", expected " + expected);
- }
- }
- int min = IntStream.of(keepAlives).min().getAsInt();
- int max = IntStream.of(keepAlives).max().getAsInt();
- int mean = (min + max)/2;
- System.out.println("min=" + min + ", max=" + max + ", mean=" + mean);
- purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
- System.out.println("first purge would be in " + purge + " ms");
- if (Math.abs(purge/1000 - min) > 0) {
- throw new RuntimeException("expected " + min + " got " + purge/1000);
- }
- long opened = java.util.stream.Stream.of(connections)
- .filter(HttpConnectionStub::connected).count();
- if (opened != count) {
- throw new RuntimeException("Opened: expected "
- + count + " got " + opened);
- }
- purge = mean * 1000;
- System.out.println("start purging at " + purge + " ms");
- Instant next = now;
- do {
- System.out.println("next purge is in " + purge + " ms");
- next = next.plus(purge, ChronoUnit.MILLIS);
- purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(next);
- long k = now.until(next, ChronoUnit.SECONDS);
- System.out.println("now is " + k + "s from start");
- for (int i=0; i<count; i++) {
- if (connections[i].connected() != (k < keepAlives[i])) {
- throw new RuntimeException("Bad connection state for "
- + i
- + "\n\t connected=" + connections[i].connected()
- + "\n\t keepAlive=" + keepAlives[i]
- + "\n\t elapsed=" + k);
- }
- }
- } while (purge > 0);
- opened = java.util.stream.Stream.of(connections)
- .filter(HttpConnectionStub::connected).count();
- if (opened != 0) {
- throw new RuntimeException("Closed: expected "
- + count + " got "
- + (count-opened));
- }
- }
-
- static <T> T error() {
- throw new InternalError("Should not reach here: wrong test assumptions!");
- }
-
- static class FlowTubeStub implements FlowTube {
- final HttpConnectionStub conn;
- FlowTubeStub(HttpConnectionStub conn) { this.conn = conn; }
- @Override
- public void onSubscribe(Flow.Subscription subscription) { }
- @Override public void onError(Throwable error) { error(); }
- @Override public void onComplete() { error(); }
- @Override public void onNext(List<ByteBuffer> item) { error();}
- @Override
- public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
- }
- @Override public boolean isFinished() { return conn.closed; }
- }
-
- // Emulates an HttpConnection that has a strong reference to its HttpClient.
- static class HttpConnectionStub extends HttpConnection {
-
- public HttpConnectionStub(HttpClient client,
- InetSocketAddress address,
- InetSocketAddress proxy,
- boolean secured) {
- super(address, null);
- this.key = ConnectionPool.cacheKey(address, proxy);
- this.address = address;
- this.proxy = proxy;
- this.secured = secured;
- this.client = client;
- this.flow = new FlowTubeStub(this);
- }
-
- final InetSocketAddress proxy;
- final InetSocketAddress address;
- final boolean secured;
- final ConnectionPool.CacheKey key;
- final HttpClient client;
- final FlowTubeStub flow;
- volatile boolean closed;
-
- // All these return something
- @Override boolean connected() {return !closed;}
- @Override boolean isSecure() {return secured;}
- @Override boolean isProxied() {return proxy!=null;}
- @Override ConnectionPool.CacheKey cacheKey() {return key;}
- @Override void shutdownInput() throws IOException {}
- @Override void shutdownOutput() throws IOException {}
- @Override
- public void close() {
- closed=true;
- System.out.println("closed: " + this);
- }
- @Override
- public String toString() {
- return "HttpConnectionStub: " + address + " proxy: " + proxy;
- }
-
- // All these throw errors
- @Override public HttpPublisher publisher() {return error();}
- @Override public CompletableFuture<Void> connectAsync() {return error();}
- @Override SocketChannel channel() {return error();}
- @Override
- HttpConnection.DetachedConnectionChannel detachChannel() {
- return error();
- }
- @Override
- FlowTube getConnectionFlow() {return flow;}
- }
- // Emulates an HttpClient that has a strong reference to its connection pool.
- static class HttpClientStub extends HttpClient {
- public HttpClientStub(ConnectionPool pool) {
- this.pool = pool;
- }
- final ConnectionPool pool;
- @Override public Optional<CookieHandler> cookieHandler() {return error();}
- @Override public HttpClient.Redirect followRedirects() {return error();}
- @Override public Optional<ProxySelector> proxy() {return error();}
- @Override public SSLContext sslContext() {return error();}
- @Override public SSLParameters sslParameters() {return error();}
- @Override public Optional<Authenticator> authenticator() {return error();}
- @Override public HttpClient.Version version() {return HttpClient.Version.HTTP_1_1;}
- @Override public Optional<Executor> executor() {return error();}
- @Override
- public <T> HttpResponse<T> send(HttpRequest req,
- HttpResponse.BodyHandler<T> responseBodyHandler)
- throws IOException, InterruptedException {
- return error();
- }
- @Override
- public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest req,
- HttpResponse.BodyHandler<T> responseBodyHandler) {
- return error();
- }
- @Override
- public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest req,
- HttpResponse.BodyHandler<T> bodyHandler,
- HttpResponse.PushPromiseHandler<T> multiHandler) {
- return error();
- }
- }
-
-}
--- a/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/Http1HeaderParserTest.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,379 +0,0 @@
-/*
- * Copyright (c) 2017, 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.
- *
- * 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.incubator.http;
-
-import java.io.ByteArrayInputStream;
-import java.net.ProtocolException;
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.IntStream;
-import sun.net.www.MessageHeader;
-import org.testng.annotations.Test;
-import org.testng.annotations.DataProvider;
-import static java.lang.System.out;
-import static java.lang.String.format;
-import static java.nio.charset.StandardCharsets.US_ASCII;
-import static java.util.stream.Collectors.toList;
-import static org.testng.Assert.*;
-
-// Mostly verifies the "new" Http1HeaderParser returns the same results as the
-// tried and tested sun.net.www.MessageHeader.
-
-public class Http1HeaderParserTest {
-
- @DataProvider(name = "responses")
- public Object[][] responses() {
- List<String> responses = new ArrayList<>();
-
- String[] basic =
- { "HTTP/1.1 200 OK\r\n\r\n",
-
- "HTTP/1.1 200 OK\r\n" +
- "Date: Mon, 15 Jan 2001 12:18:21 GMT\r\n" +
- "Server: Apache/1.3.14 (Unix)\r\n" +
- "Connection: close\r\n" +
- "Content-Type: text/html; charset=iso-8859-1\r\n" +
- "Content-Length: 10\r\n\r\n" +
- "123456789",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 9\r\n" +
- "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
- "XXXXX",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 9\r\n" +
- "Content-Type: text/html; charset=UTF-8\r\n\r\n" + // more than one SP after ':'
- "XXXXX",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length:\t10\r\n" +
- "Content-Type:\ttext/html; charset=UTF-8\r\n\r\n" + // HT separator
- "XXXXX",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length:\t\t10\r\n" +
- "Content-Type:\t\ttext/html; charset=UTF-8\r\n\r\n" + // more than one HT after ':'
- "XXXXX",
-
- "HTTP/1.1 407 Proxy Authorization Required\r\n" +
- "Proxy-Authenticate: Basic realm=\"a fake realm\"\r\n\r\n",
-
- "HTTP/1.1 401 Unauthorized\r\n" +
- "WWW-Authenticate: Digest realm=\"wally land\" domain=/ " +
- "nonce=\"2B7F3A2B\" qop=\"auth\"\r\n\r\n",
-
- "HTTP/1.1 200 OK\r\n" +
- "X-Foo:\r\n\r\n", // no value
-
- "HTTP/1.1 200 OK\r\n" +
- "X-Foo:\r\n\r\n" + // no value, with response body
- "Some Response Body",
-
- "HTTP/1.1 200 OK\r\n" +
- "X-Foo:\r\n" + // no value, followed by another header
- "Content-Length: 10\r\n\r\n" +
- "Some Response Body",
-
- "HTTP/1.1 200 OK\r\n" +
- "X-Foo:\r\n" + // no value, followed by another header, with response body
- "Content-Length: 10\r\n\r\n",
-
- "HTTP/1.1 200 OK\r\n" +
- "X-Foo: chegar\r\n" +
- "X-Foo: dfuchs\r\n" + // same header appears multiple times
- "Content-Length: 0\r\n" +
- "X-Foo: michaelm\r\n" +
- "X-Foo: prappo\r\n\r\n",
-
- "HTTP/1.1 200 OK\r\n" +
- "X-Foo:\r\n" + // no value, same header appears multiple times
- "X-Foo: dfuchs\r\n" +
- "Content-Length: 0\r\n" +
- "X-Foo: michaelm\r\n" +
- "X-Foo: prappo\r\n\r\n",
-
- "HTTP/1.1 200 OK\r\n" +
- "Accept-Ranges: bytes\r\n" +
- "Cache-control: max-age=0, no-cache=\"set-cookie\"\r\n" +
- "Content-Length: 132868\r\n" +
- "Content-Type: text/html; charset=UTF-8\r\n" +
- "Date: Sun, 05 Nov 2017 22:24:03 GMT\r\n" +
- "Server: Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips Communique/4.2.2\r\n" +
- "Set-Cookie: AWSELB=AF7927F5100F4202119876ED2436B5005EE;PATH=/;MAX-AGE=900\r\n" +
- "Vary: Host,Accept-Encoding,User-Agent\r\n" +
- "X-Mod-Pagespeed: 1.12.34.2-0\r\n" +
- "Connection: keep-alive\r\n\r\n"
- };
- Arrays.stream(basic).forEach(responses::add);
-
- String[] foldingTemplate =
- { "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 9\r\n" +
- "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r'
- " charset=UTF-8\r\n" + // one preceding SP
- "Connection: close\r\n\r\n" +
- "XXYYZZAABBCCDDEE",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 19\r\n" +
- "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
- " charset=UTF-8\r\n" + // more than one preceding SP
- "Connection: keep-alive\r\n\r\n" +
- "XXYYZZAABBCCDDEEFFGG",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 999\r\n" +
- "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
- "\tcharset=UTF-8\r\n" + // one preceding HT
- "Connection: close\r\n\r\n" +
- "XXYYZZAABBCCDDEE",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 54\r\n" +
- "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
- "\t\t\tcharset=UTF-8\r\n" + // more than one preceding HT
- "Connection: keep-alive\r\n\r\n" +
- "XXYYZZAABBCCDDEEFFGG",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: -1\r\n" +
- "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
- "\t \t \tcharset=UTF-8\r\n" + // mix of preceding HT and SP
- "Connection: keep-alive\r\n\r\n" +
- "XXYYZZAABBCCDDEEFFGGHH",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 65\r\n" +
- "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
- " \t \t charset=UTF-8\r\n" + // mix of preceding SP and HT
- "Connection: keep-alive\r\n\r\n" +
- "XXYYZZAABBCCDDEEFFGGHHII",
-
- "HTTP/1.1 401 Unauthorized\r\n" +
- "WWW-Authenticate: Digest realm=\"wally land\","
- +"$NEWLINE domain=/,"
- +"$NEWLINE nonce=\"2B7F3A2B\","
- +"$NEWLINE\tqop=\"auth\"\r\n\r\n",
-
- };
- for (String newLineChar : new String[] { "\n", "\r", "\r\n" }) {
- for (String template : foldingTemplate)
- responses.add(template.replace("$NEWLINE", newLineChar));
- }
-
- String[] bad = // much of this is to retain parity with legacy MessageHeaders
- { "HTTP/1.1 200 OK\r\n" +
- "Connection:\r\n\r\n", // empty value, no body
-
- "HTTP/1.1 200 OK\r\n" +
- "Connection:\r\n\r\n" + // empty value, with body
- "XXXXX",
-
- "HTTP/1.1 200 OK\r\n" +
- ": no header\r\n\r\n", // no/empty header-name, no body, no following header
-
- "HTTP/1.1 200 OK\r\n" +
- ": no; header\r\n" + // no/empty header-name, no body, following header
- "Content-Length: 65\r\n\r\n",
-
- "HTTP/1.1 200 OK\r\n" +
- ": no header\r\n" + // no/empty header-name
- "Content-Length: 65\r\n\r\n" +
- "XXXXX",
-
- "HTTP/1.1 200 OK\r\n" +
- ": no header\r\n\r\n" + // no/empty header-name, followed by header
- "XXXXX",
-
- "HTTP/1.1 200 OK\r\n" +
- "Conte\r" +
- " nt-Length: 9\r\n" + // fold/bad header name ???
- "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
- "XXXXX",
-
- "HTTP/1.1 200 OK\r\n" +
- "Conte\r" +
- "nt-Length: 9\r\n" + // fold/bad header name ??? without preceding space
- "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
- "XXXXXYYZZ",
-
- "HTTP/1.0 404 Not Found\r\n" +
- "header-without-colon\r\n\r\n",
-
- "HTTP/1.0 404 Not Found\r\n" +
- "header-without-colon\r\n\r\n" +
- "SOMEBODY",
-
- };
- Arrays.stream(bad).forEach(responses::add);
-
- return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
- }
-
- @Test(dataProvider = "responses")
- public void verifyHeaders(String respString) throws Exception {
- byte[] bytes = respString.getBytes(US_ASCII);
- ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
- MessageHeader m = new MessageHeader(bais);
- Map<String,List<String>> messageHeaderMap = m.getHeaders();
- int available = bais.available();
-
- Http1HeaderParser decoder = new Http1HeaderParser();
- ByteBuffer b = ByteBuffer.wrap(bytes);
- decoder.parse(b);
- Map<String,List<String>> decoderMap1 = decoder.headers().map();
- assertEquals(available, b.remaining(),
- "stream available not equal to remaining");
-
- // assert status-line
- String statusLine1 = messageHeaderMap.get(null).get(0);
- String statusLine2 = decoder.statusLine();
- if (statusLine1.startsWith("HTTP")) {// skip the case where MH's messes up the status-line
- assertEquals(statusLine1, statusLine2, "Status-line not equal");
- } else {
- assertTrue(statusLine2.startsWith("HTTP/1."), "Status-line not HTTP/1.");
- }
-
- // remove the null'th entry with is the status-line
- Map<String,List<String>> map = new HashMap<>();
- for (Map.Entry<String,List<String>> e : messageHeaderMap.entrySet()) {
- if (e.getKey() != null) {
- map.put(e.getKey(), e.getValue());
- }
- }
- messageHeaderMap = map;
-
- assertHeadersEqual(messageHeaderMap, decoderMap1,
- "messageHeaderMap not equal to decoderMap1");
-
- // byte at a time
- decoder = new Http1HeaderParser();
- List<ByteBuffer> buffers = IntStream.range(0, bytes.length)
- .mapToObj(i -> ByteBuffer.wrap(bytes, i, 1))
- .collect(toList());
- while (decoder.parse(buffers.remove(0)) != true);
- Map<String,List<String>> decoderMap2 = decoder.headers().map();
- assertEquals(available, buffers.size(),
- "stream available not equals to remaining buffers");
- assertEquals(decoderMap1, decoderMap2, "decoder maps not equal");
- }
-
- @DataProvider(name = "errors")
- public Object[][] errors() {
- List<String> responses = new ArrayList<>();
-
- // These responses are parsed, somewhat, by MessageHeaders but give
- // nonsensible results. They, correctly, fail with the Http1HeaderParser.
- String[] bad =
- {// "HTTP/1.1 402 Payment Required\r\n" +
- // "Content-Length: 65\r\n\r", // missing trailing LF //TODO: incomplete
-
- "HTTP/1.1 402 Payment Required\r\n" +
- "Content-Length: 65\r\n\rT\r\n\r\nGGGGGG",
-
- "HTTP/1.1 200OK\r\n\rT",
-
- "HTTP/1.1 200OK\rT",
- };
- Arrays.stream(bad).forEach(responses::add);
-
- return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
- }
-
- @Test(dataProvider = "errors", expectedExceptions = ProtocolException.class)
- public void errors(String respString) throws ProtocolException {
- byte[] bytes = respString.getBytes(US_ASCII);
- Http1HeaderParser decoder = new Http1HeaderParser();
- ByteBuffer b = ByteBuffer.wrap(bytes);
- decoder.parse(b);
- }
-
- void assertHeadersEqual(Map<String,List<String>> expected,
- Map<String,List<String>> actual,
- String msg) {
-
- if (expected.equals(actual))
- return;
-
- assertEquals(expected.size(), actual.size(),
- format("%s. Expected size %d, actual size %s. %nexpected= %s,%n actual=%s.",
- msg, expected.size(), actual.size(), mapToString(expected), mapToString(actual)));
-
- for (Map.Entry<String,List<String>> e : expected.entrySet()) {
- String key = e.getKey();
- List<String> values = e.getValue();
-
- boolean found = false;
- for (Map.Entry<String,List<String>> other: actual.entrySet()) {
- if (key.equalsIgnoreCase(other.getKey())) {
- found = true;
- List<String> otherValues = other.getValue();
- assertEquals(values.size(), otherValues.size(),
- format("%s. Expected list size %d, actual size %s",
- msg, values.size(), otherValues.size()));
- if (!(values.containsAll(otherValues) && otherValues.containsAll(values)))
- assertTrue(false, format("Lists are unequal [%s] [%s]", values, otherValues));
- break;
- }
- }
- assertTrue(found, format("header name, %s, not found in %s", key, actual));
- }
- }
-
- static String mapToString(Map<String,List<String>> map) {
- StringBuilder sb = new StringBuilder();
- List<String> sortedKeys = new ArrayList(map.keySet());
- Collections.sort(sortedKeys);
- for (String key : sortedKeys) {
- List<String> values = map.get(key);
- sb.append("\n\t" + key + " | " + values);
- }
- return sb.toString();
- }
-
- // ---
-
- /* Main entry point for standalone testing of the main functional test. */
- public static void main(String... args) throws Exception {
- Http1HeaderParserTest test = new Http1HeaderParserTest();
- int count = 0;
- for (Object[] objs : test.responses()) {
- out.println("Testing " + count++ + ", " + objs[0]);
- test.verifyHeaders((String) objs[0]);
- }
- for (Object[] objs : test.errors()) {
- out.println("Testing " + count++ + ", " + objs[0]);
- try {
- test.errors((String) objs[0]);
- throw new RuntimeException("Expected ProtocolException for " + objs[0]);
- } catch (ProtocolException expected) { /* Ok */ }
- }
- }
-}
--- a/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/RawChannelTest.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,332 +0,0 @@
-/*
- * Copyright (c) 2017, 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.
- *
- * 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.incubator.http;
-
-import jdk.incubator.http.internal.websocket.RawChannel;
-import jdk.incubator.http.internal.websocket.WebSocketRequest;
-import org.testng.annotations.Test;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.UncheckedIOException;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.net.URI;
-import java.nio.ByteBuffer;
-import java.nio.channels.SelectionKey;
-import java.util.Random;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicLong;
-
-import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
-import static org.testng.Assert.assertEquals;
-
-/*
- * This test exercises mechanics of _independent_ reads and writes on the
- * RawChannel. It verifies that the underlying implementation can manage more
- * than a single type of notifications at the same time.
- */
-public class RawChannelTest {
-
- private final AtomicLong clientWritten = new AtomicLong();
- private final AtomicLong serverWritten = new AtomicLong();
- private final AtomicLong clientRead = new AtomicLong();
- private final AtomicLong serverRead = new AtomicLong();
-
- /*
- * Since at this level we don't have any control over the low level socket
- * parameters, this latch ensures a write to the channel will stall at least
- * once (socket's send buffer filled up).
- */
- private final CountDownLatch writeStall = new CountDownLatch(1);
- private final CountDownLatch initialWriteStall = new CountDownLatch(1);
-
- /*
- * This one works similarly by providing means to ensure a read from the
- * channel will stall at least once (no more data available on the socket).
- */
- private final CountDownLatch readStall = new CountDownLatch(1);
- private final CountDownLatch initialReadStall = new CountDownLatch(1);
-
- private final AtomicInteger writeHandles = new AtomicInteger();
- private final AtomicInteger readHandles = new AtomicInteger();
-
- private final CountDownLatch exit = new CountDownLatch(1);
-
- @Test
- public void test() throws Exception {
- try (ServerSocket server = new ServerSocket(0)) {
- int port = server.getLocalPort();
- new TestServer(server).start();
-
- final RawChannel chan = channelOf(port);
- print("RawChannel is %s", String.valueOf(chan));
- initialWriteStall.await();
-
- // It's very important not to forget the initial bytes, possibly
- // left from the HTTP thingy
- int initialBytes = chan.initialByteBuffer().remaining();
- print("RawChannel has %s initial bytes", initialBytes);
- clientRead.addAndGet(initialBytes);
-
- // tell the server we have read the initial bytes, so
- // that it makes sure there is something for us to
- // read next in case the initialBytes have already drained the
- // channel dry.
- initialReadStall.countDown();
-
- chan.registerEvent(new RawChannel.RawEvent() {
-
- private final ByteBuffer reusableBuffer = ByteBuffer.allocate(32768);
-
- @Override
- public int interestOps() {
- return SelectionKey.OP_WRITE;
- }
-
- @Override
- public void handle() {
- int i = writeHandles.incrementAndGet();
- print("OP_WRITE #%s", i);
- if (i > 3) { // Fill up the send buffer not more than 3 times
- try {
- chan.shutdownOutput();
- } catch (IOException e) {
- e.printStackTrace();
- }
- return;
- }
- long total = 0;
- try {
- long n;
- do {
- ByteBuffer[] array = {reusableBuffer.slice()};
- n = chan.write(array, 0, 1);
- total += n;
- } while (n > 0);
- print("OP_WRITE clogged SNDBUF with %s bytes", total);
- clientWritten.addAndGet(total);
- chan.registerEvent(this);
- writeStall.countDown(); // signal send buffer is full
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
- });
-
- chan.registerEvent(new RawChannel.RawEvent() {
-
- @Override
- public int interestOps() {
- return SelectionKey.OP_READ;
- }
-
- @Override
- public void handle() {
- int i = readHandles.incrementAndGet();
- print("OP_READ #%s", i);
- ByteBuffer read = null;
- long total = 0;
- while (true) {
- try {
- read = chan.read();
- } catch (IOException e) {
- e.printStackTrace();
- }
- if (read == null) {
- print("OP_READ EOF");
- break;
- } else if (!read.hasRemaining()) {
- print("OP_READ stall");
- try {
- chan.registerEvent(this);
- } catch (IOException e) {
- e.printStackTrace();
- }
- readStall.countDown();
- break;
- }
- int r = read.remaining();
- total += r;
- clientRead.addAndGet(r);
- }
- print("OP_READ read %s bytes (%s total)", total, clientRead.get());
- }
- });
- exit.await(); // All done, we need to compare results:
- assertEquals(clientRead.get(), serverWritten.get());
- assertEquals(serverRead.get(), clientWritten.get());
- }
- }
-
- private static RawChannel channelOf(int port) throws Exception {
- URI uri = URI.create("http://127.0.0.1:" + port + "/");
- print("raw channel to %s", uri.toString());
- HttpRequest req = HttpRequest.newBuilder(uri).build();
- // Switch on isWebSocket flag to prevent the connection from
- // being returned to the pool.
- ((WebSocketRequest)req).isWebSocket(true);
- HttpClient client = HttpClient.newHttpClient();
- try {
- HttpResponse<?> r = client.send(req, discard(null));
- r.body();
- return ((HttpResponseImpl) r).rawChannel();
- } finally {
- // Need to hold onto the client until the RawChannel is
- // created. This would not be needed if we had created
- // a WebSocket, but here we are fiddling directly
- // with the internals of HttpResponseImpl!
- java.lang.ref.Reference.reachabilityFence(client);
- }
- }
-
- private class TestServer extends Thread { // Powered by Slowpokes
-
- private final ServerSocket server;
-
- TestServer(ServerSocket server) throws IOException {
- this.server = server;
- }
-
- @Override
- public void run() {
- try (Socket s = server.accept()) {
- InputStream is = s.getInputStream();
- OutputStream os = s.getOutputStream();
-
- processHttp(is, os);
-
- Thread reader = new Thread(() -> {
- try {
- long n = readSlowly(is);
- print("Server read %s bytes", n);
- serverRead.addAndGet(n);
- s.shutdownInput();
- } catch (Exception e) {
- e.printStackTrace();
- }
- });
-
- Thread writer = new Thread(() -> {
- try {
- long n = writeSlowly(os);
- print("Server written %s bytes", n);
- serverWritten.addAndGet(n);
- s.shutdownOutput();
- } catch (Exception e) {
- e.printStackTrace();
- }
- });
-
- reader.start();
- writer.start();
-
- reader.join();
- writer.join();
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- exit.countDown();
- }
- }
-
- private void processHttp(InputStream is, OutputStream os)
- throws IOException
- {
- os.write("HTTP/1.1 200 OK\r\nContent-length: 0\r\n\r\n".getBytes());
-
- // write some initial bytes
- byte[] initial = byteArrayOfSize(1024);
- os.write(initial);
- os.flush();
- serverWritten.addAndGet(initial.length);
- initialWriteStall.countDown();
-
- byte[] buf = new byte[1024];
- String s = "";
- while (true) {
- int n = is.read(buf);
- if (n <= 0) {
- throw new RuntimeException("Unexpected end of request");
- }
- s = s + new String(buf, 0, n);
- if (s.contains("\r\n\r\n")) {
- break;
- }
- }
- }
-
- private long writeSlowly(OutputStream os) throws Exception {
- byte[] first = byteArrayOfSize(1024);
- long total = first.length;
- os.write(first);
- os.flush();
-
- // wait until initial bytes were read
- initialReadStall.await();
-
- // make sure there is something to read, otherwise readStall
- // will never be counted down.
- first = byteArrayOfSize(1024);
- os.write(first);
- os.flush();
- total += first.length;
-
- // Let's wait for the signal from the raw channel that its read has
- // stalled, and then continue sending a bit more stuff
- readStall.await();
- for (int i = 0; i < 32; i++) {
- byte[] b = byteArrayOfSize(1024);
- os.write(b);
- os.flush();
- total += b.length;
- TimeUnit.MILLISECONDS.sleep(1);
- }
- return total;
- }
-
- private long readSlowly(InputStream is) throws Exception {
- // Wait for the raw channel to fill up its send buffer
- writeStall.await();
- long overall = 0;
- byte[] array = new byte[1024];
- for (int n = 0; n != -1; n = is.read(array)) {
- TimeUnit.MILLISECONDS.sleep(1);
- overall += n;
- }
- return overall;
- }
- }
-
- private static void print(String format, Object... args) {
- System.out.println(Thread.currentThread() + ": " + String.format(format, args));
- }
-
- private static byte[] byteArrayOfSize(int bound) {
- return new byte[new Random().nextInt(1 + bound)];
- }
-}
--- a/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/SelectorTest.java Tue Feb 06 11:39:55 2018 +0000
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,222 +0,0 @@
-/*
- * Copyright (c) 2015, 2017, 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.
- *
- * 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.incubator.http;
-
-import java.net.*;
-import java.io.*;
-import java.nio.channels.*;
-import java.nio.ByteBuffer;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicInteger;
-import static java.lang.System.out;
-import static java.nio.charset.StandardCharsets.US_ASCII;
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
-
-import org.testng.annotations.Test;
-import jdk.incubator.http.internal.websocket.RawChannel;
-
-/**
- * Whitebox test of selector mechanics. Currently only a simple test
- * setting one read and one write event is done. It checks that the
- * write event occurs first, followed by the read event and then no
- * further events occur despite the conditions actually still existing.
- */
-@Test
-public class SelectorTest {
-
- AtomicInteger counter = new AtomicInteger();
- volatile boolean error;
- static final CountDownLatch finishingGate = new CountDownLatch(1);
- static volatile HttpClient staticDefaultClient;
-
- static HttpClient defaultClient() {
- if (staticDefaultClient == null) {
- synchronized (SelectorTest.class) {
- staticDefaultClient = HttpClient.newHttpClient();
- }
- }
- return staticDefaultClient;
- }
-
- String readSomeBytes(RawChannel chan) {
- try {
- ByteBuffer buf = chan.read();
- if (buf == null) {
- out.println("chan read returned null");
- return null;
- }
- buf.flip();
- byte[] bb = new byte[buf.remaining()];
- buf.get(bb);
- return new String(bb, US_ASCII);
- } catch (IOException ioe) {
- throw new UncheckedIOException(ioe);
- }
- }
-
- @Test
- public void test() throws Exception {
-
- try (ServerSocket server = new ServerSocket(0)) {
- int port = server.getLocalPort();
-
- out.println("Listening on port " + server.getLocalPort());
-
- TestServer t = new TestServer(server);
- t.start();
- out.println("Started server thread");
-
- try (RawChannel chan = getARawChannel(port)) {
-
- chan.registerEvent(new RawChannel.RawEvent() {
- @Override
- public int interestOps() {
- return SelectionKey.OP_READ;
- }
-
- @Override
- public void handle() {
- readSomeBytes(chan);
- out.printf("OP_READ\n");
- final int count = counter.get();
- if (count != 1) {
- out.printf("OP_READ error counter = %d\n", count);
- error = true;
- }
- }
- });
-
- chan.registerEvent(new RawChannel.RawEvent() {
- @Override
- public int interestOps() {
- return SelectionKey.OP_WRITE;
- }
-
- @Override
- public void handle() {
- out.printf("OP_WRITE\n");
- final int count = counter.get();
- if (count != 0) {
- out.printf("OP_WRITE error counter = %d\n", count);
- error = true;
- } else {
- ByteBuffer bb = ByteBuffer.wrap(TestServer.INPUT);
- counter.incrementAndGet();
- try {
- chan.write(new ByteBuffer[]{bb}, 0, 1);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
- }
-
- });
- out.println("Events registered. Waiting");
- finishingGate.await(30, SECONDS);
- if (error)
- throw new RuntimeException("Error");
- else
- out.println("No error");
- }
- }
- }
-
- static RawChannel getARawChannel(int port) throws Exception {
- URI uri = URI.create("http://127.0.0.1:" + port + "/");
- out.println("client connecting to " + uri.toString());
- HttpRequest req = HttpRequest.newBuilder(uri).build();
- // Otherwise HttpClient will think this is an ordinary connection and
- // thus all ordinary procedures apply to it, e.g. it must be put into
- // the cache
- ((HttpRequestImpl) req).isWebSocket(true);
- HttpResponse<?> r = defaultClient().send(req, discard(null));
- r.body();
- return ((HttpResponseImpl) r).rawChannel();
- }
-
- static class TestServer extends Thread {
- static final byte[] INPUT = "Hello world".getBytes(US_ASCII);
- static final byte[] OUTPUT = "Goodbye world".getBytes(US_ASCII);
- static final String FIRST_RESPONSE = "HTTP/1.1 200 OK\r\nContent-length: 0\r\n\r\n";
- final ServerSocket server;
-
- TestServer(ServerSocket server) throws IOException {
- this.server = server;
- }
-
- public void run() {
- try (Socket s = server.accept();
- InputStream is = s.getInputStream();
- OutputStream os = s.getOutputStream()) {
-
- out.println("Got connection");
- readRequest(is);
- os.write(FIRST_RESPONSE.getBytes());
- read(is);
- write(os);
- Thread.sleep(1000);
- // send some more data, and make sure WRITE op does not get called
- write(os);
- out.println("TestServer exiting");
- SelectorTest.finishingGate.countDown();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- // consumes the HTTP request
- static void readRequest(InputStream is) throws IOException {
- out.println("starting readRequest");
- byte[] buf = new byte[1024];
- String s = "";
- while (true) {
- int n = is.read(buf);
- if (n <= 0)
- throw new IOException("Error");
- s = s + new String(buf, 0, n);
- if (s.indexOf("\r\n\r\n") != -1)
- break;
- }
- out.println("returning from readRequest");
- }
-
- static void read(InputStream is) throws IOException {
- out.println("starting read");
- for (int i = 0; i < INPUT.length; i++) {
- int c = is.read();
- if (c == -1)
- throw new IOException("closed");
- if (INPUT[i] != (byte) c)
- throw new IOException("Error. Expected:" + INPUT[i] + ", got:" + c);
- }
- out.println("returning from read");
- }
-
- static void write(OutputStream os) throws IOException {
- out.println("doing write");
- os.write(OUTPUT);
- }
- }
-}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/internal/ConnectionPoolTest.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,252 @@
+/*
+ * 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.
+ *
+ * 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.incubator.http.internal;
+
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.net.Authenticator;
+import java.net.CookieHandler;
+import java.net.InetSocketAddress;
+import java.net.ProxySelector;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.List;
+import java.util.Optional;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Flow;
+import java.util.stream.IntStream;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.internal.common.FlowTube;
+
+/**
+ * @summary Verifies that the ConnectionPool correctly handle
+ * connection deadlines and purges the right connections
+ * from the cache.
+ * @bug 8187044 8187111
+ * @author danielfuchs
+ */
+public class ConnectionPoolTest {
+
+ static long getActiveCleaners() throws ClassNotFoundException {
+ // ConnectionPool.ACTIVE_CLEANER_COUNTER.get()
+ // ConnectionPoolTest.class.getModule().addReads(
+ // Class.forName("java.lang.management.ManagementFactory").getModule());
+ return java.util.stream.Stream.of(ManagementFactory.getThreadMXBean()
+ .dumpAllThreads(false, false))
+ .filter(t -> t.getThreadName().startsWith("HTTP-Cache-cleaner"))
+ .count();
+ }
+
+ public static void main(String[] args) throws Exception {
+ testCacheCleaners();
+ }
+
+ public static void testCacheCleaners() throws Exception {
+ ConnectionPool pool = new ConnectionPool(666);
+ HttpClient client = new HttpClientStub(pool);
+ InetSocketAddress proxy = InetSocketAddress.createUnresolved("bar", 80);
+ System.out.println("Adding 10 connections to pool");
+ Random random = new Random();
+
+ final int count = 20;
+ Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+ int[] keepAlives = new int[count];
+ HttpConnectionStub[] connections = new HttpConnectionStub[count];
+ long purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
+ long expected = 0;
+ if (purge != expected) {
+ throw new RuntimeException("Bad purge delay: " + purge
+ + ", expected " + expected);
+ }
+ expected = Long.MAX_VALUE;
+ for (int i=0; i<count; i++) {
+ InetSocketAddress addr = InetSocketAddress.createUnresolved("foo"+i, 80);
+ keepAlives[i] = random.nextInt(10) * 10 + 10;
+ connections[i] = new HttpConnectionStub(client, addr, proxy, true);
+ System.out.println("Adding connection: " + now
+ + " keepAlive: " + keepAlives[i]
+ + " /" + connections[i]);
+ pool.returnToPool(connections[i], now, keepAlives[i]);
+ expected = Math.min(expected, keepAlives[i] * 1000);
+ purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
+ if (purge != expected) {
+ throw new RuntimeException("Bad purge delay: " + purge
+ + ", expected " + expected);
+ }
+ }
+ int min = IntStream.of(keepAlives).min().getAsInt();
+ int max = IntStream.of(keepAlives).max().getAsInt();
+ int mean = (min + max)/2;
+ System.out.println("min=" + min + ", max=" + max + ", mean=" + mean);
+ purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
+ System.out.println("first purge would be in " + purge + " ms");
+ if (Math.abs(purge/1000 - min) > 0) {
+ throw new RuntimeException("expected " + min + " got " + purge/1000);
+ }
+ long opened = java.util.stream.Stream.of(connections)
+ .filter(HttpConnectionStub::connected).count();
+ if (opened != count) {
+ throw new RuntimeException("Opened: expected "
+ + count + " got " + opened);
+ }
+ purge = mean * 1000;
+ System.out.println("start purging at " + purge + " ms");
+ Instant next = now;
+ do {
+ System.out.println("next purge is in " + purge + " ms");
+ next = next.plus(purge, ChronoUnit.MILLIS);
+ purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(next);
+ long k = now.until(next, ChronoUnit.SECONDS);
+ System.out.println("now is " + k + "s from start");
+ for (int i=0; i<count; i++) {
+ if (connections[i].connected() != (k < keepAlives[i])) {
+ throw new RuntimeException("Bad connection state for "
+ + i
+ + "\n\t connected=" + connections[i].connected()
+ + "\n\t keepAlive=" + keepAlives[i]
+ + "\n\t elapsed=" + k);
+ }
+ }
+ } while (purge > 0);
+ opened = java.util.stream.Stream.of(connections)
+ .filter(HttpConnectionStub::connected).count();
+ if (opened != 0) {
+ throw new RuntimeException("Closed: expected "
+ + count + " got "
+ + (count-opened));
+ }
+ }
+
+ static <T> T error() {
+ throw new InternalError("Should not reach here: wrong test assumptions!");
+ }
+
+ static class FlowTubeStub implements FlowTube {
+ final HttpConnectionStub conn;
+ FlowTubeStub(HttpConnectionStub conn) { this.conn = conn; }
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) { }
+ @Override public void onError(Throwable error) { error(); }
+ @Override public void onComplete() { error(); }
+ @Override public void onNext(List<ByteBuffer> item) { error();}
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+ }
+ @Override public boolean isFinished() { return conn.closed; }
+ }
+
+ // Emulates an HttpConnection that has a strong reference to its HttpClient.
+ static class HttpConnectionStub extends HttpConnection {
+
+ public HttpConnectionStub(HttpClient client,
+ InetSocketAddress address,
+ InetSocketAddress proxy,
+ boolean secured) {
+ super(address, null);
+ this.key = ConnectionPool.cacheKey(address, proxy);
+ this.address = address;
+ this.proxy = proxy;
+ this.secured = secured;
+ this.client = client;
+ this.flow = new FlowTubeStub(this);
+ }
+
+ final InetSocketAddress proxy;
+ final InetSocketAddress address;
+ final boolean secured;
+ final ConnectionPool.CacheKey key;
+ final HttpClient client;
+ final FlowTubeStub flow;
+ volatile boolean closed;
+
+ // All these return something
+ @Override boolean connected() {return !closed;}
+ @Override boolean isSecure() {return secured;}
+ @Override boolean isProxied() {return proxy!=null;}
+ @Override ConnectionPool.CacheKey cacheKey() {return key;}
+ @Override void shutdownInput() throws IOException {}
+ @Override void shutdownOutput() throws IOException {}
+ @Override
+ public void close() {
+ closed=true;
+ System.out.println("closed: " + this);
+ }
+ @Override
+ public String toString() {
+ return "HttpConnectionStub: " + address + " proxy: " + proxy;
+ }
+
+ // All these throw errors
+ @Override public HttpPublisher publisher() {return error();}
+ @Override public CompletableFuture<Void> connectAsync() {return error();}
+ @Override SocketChannel channel() {return error();}
+ @Override
+ HttpConnection.DetachedConnectionChannel detachChannel() {
+ return error();
+ }
+ @Override
+ FlowTube getConnectionFlow() {return flow;}
+ }
+ // Emulates an HttpClient that has a strong reference to its connection pool.
+ static class HttpClientStub extends HttpClient {
+ public HttpClientStub(ConnectionPool pool) {
+ this.pool = pool;
+ }
+ final ConnectionPool pool;
+ @Override public Optional<CookieHandler> cookieHandler() {return error();}
+ @Override public HttpClient.Redirect followRedirects() {return error();}
+ @Override public Optional<ProxySelector> proxy() {return error();}
+ @Override public SSLContext sslContext() {return error();}
+ @Override public SSLParameters sslParameters() {return error();}
+ @Override public Optional<Authenticator> authenticator() {return error();}
+ @Override public HttpClient.Version version() {return HttpClient.Version.HTTP_1_1;}
+ @Override public Optional<Executor> executor() {return error();}
+ @Override
+ public <T> HttpResponse<T> send(HttpRequest req,
+ HttpResponse.BodyHandler<T> responseBodyHandler)
+ throws IOException, InterruptedException {
+ return error();
+ }
+ @Override
+ public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest req,
+ HttpResponse.BodyHandler<T> responseBodyHandler) {
+ return error();
+ }
+ @Override
+ public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest req,
+ HttpResponse.BodyHandler<T> bodyHandler,
+ HttpResponse.PushPromiseHandler<T> multiHandler) {
+ return error();
+ }
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/internal/Http1HeaderParserTest.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,379 @@
+/*
+ * 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.
+ *
+ * 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.incubator.http.internal;
+
+import java.io.ByteArrayInputStream;
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.IntStream;
+import sun.net.www.MessageHeader;
+import org.testng.annotations.Test;
+import org.testng.annotations.DataProvider;
+import static java.lang.System.out;
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.util.stream.Collectors.toList;
+import static org.testng.Assert.*;
+
+// Mostly verifies the "new" Http1HeaderParser returns the same results as the
+// tried and tested sun.net.www.MessageHeader.
+
+public class Http1HeaderParserTest {
+
+ @DataProvider(name = "responses")
+ public Object[][] responses() {
+ List<String> responses = new ArrayList<>();
+
+ String[] basic =
+ { "HTTP/1.1 200 OK\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Date: Mon, 15 Jan 2001 12:18:21 GMT\r\n" +
+ "Server: Apache/1.3.14 (Unix)\r\n" +
+ "Connection: close\r\n" +
+ "Content-Type: text/html; charset=iso-8859-1\r\n" +
+ "Content-Length: 10\r\n\r\n" +
+ "123456789",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 9\r\n" +
+ "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 9\r\n" +
+ "Content-Type: text/html; charset=UTF-8\r\n\r\n" + // more than one SP after ':'
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length:\t10\r\n" +
+ "Content-Type:\ttext/html; charset=UTF-8\r\n\r\n" + // HT separator
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length:\t\t10\r\n" +
+ "Content-Type:\t\ttext/html; charset=UTF-8\r\n\r\n" + // more than one HT after ':'
+ "XXXXX",
+
+ "HTTP/1.1 407 Proxy Authorization Required\r\n" +
+ "Proxy-Authenticate: Basic realm=\"a fake realm\"\r\n\r\n",
+
+ "HTTP/1.1 401 Unauthorized\r\n" +
+ "WWW-Authenticate: Digest realm=\"wally land\" domain=/ " +
+ "nonce=\"2B7F3A2B\" qop=\"auth\"\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n\r\n", // no value
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n\r\n" + // no value, with response body
+ "Some Response Body",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n" + // no value, followed by another header
+ "Content-Length: 10\r\n\r\n" +
+ "Some Response Body",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n" + // no value, followed by another header, with response body
+ "Content-Length: 10\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo: chegar\r\n" +
+ "X-Foo: dfuchs\r\n" + // same header appears multiple times
+ "Content-Length: 0\r\n" +
+ "X-Foo: michaelm\r\n" +
+ "X-Foo: prappo\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n" + // no value, same header appears multiple times
+ "X-Foo: dfuchs\r\n" +
+ "Content-Length: 0\r\n" +
+ "X-Foo: michaelm\r\n" +
+ "X-Foo: prappo\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Accept-Ranges: bytes\r\n" +
+ "Cache-control: max-age=0, no-cache=\"set-cookie\"\r\n" +
+ "Content-Length: 132868\r\n" +
+ "Content-Type: text/html; charset=UTF-8\r\n" +
+ "Date: Sun, 05 Nov 2017 22:24:03 GMT\r\n" +
+ "Server: Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips Communique/4.2.2\r\n" +
+ "Set-Cookie: AWSELB=AF7927F5100F4202119876ED2436B5005EE;PATH=/;MAX-AGE=900\r\n" +
+ "Vary: Host,Accept-Encoding,User-Agent\r\n" +
+ "X-Mod-Pagespeed: 1.12.34.2-0\r\n" +
+ "Connection: keep-alive\r\n\r\n"
+ };
+ Arrays.stream(basic).forEach(responses::add);
+
+ String[] foldingTemplate =
+ { "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 9\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r'
+ " charset=UTF-8\r\n" + // one preceding SP
+ "Connection: close\r\n\r\n" +
+ "XXYYZZAABBCCDDEE",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 19\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ " charset=UTF-8\r\n" + // more than one preceding SP
+ "Connection: keep-alive\r\n\r\n" +
+ "XXYYZZAABBCCDDEEFFGG",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 999\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ "\tcharset=UTF-8\r\n" + // one preceding HT
+ "Connection: close\r\n\r\n" +
+ "XXYYZZAABBCCDDEE",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 54\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ "\t\t\tcharset=UTF-8\r\n" + // more than one preceding HT
+ "Connection: keep-alive\r\n\r\n" +
+ "XXYYZZAABBCCDDEEFFGG",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: -1\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ "\t \t \tcharset=UTF-8\r\n" + // mix of preceding HT and SP
+ "Connection: keep-alive\r\n\r\n" +
+ "XXYYZZAABBCCDDEEFFGGHH",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 65\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ " \t \t charset=UTF-8\r\n" + // mix of preceding SP and HT
+ "Connection: keep-alive\r\n\r\n" +
+ "XXYYZZAABBCCDDEEFFGGHHII",
+
+ "HTTP/1.1 401 Unauthorized\r\n" +
+ "WWW-Authenticate: Digest realm=\"wally land\","
+ +"$NEWLINE domain=/,"
+ +"$NEWLINE nonce=\"2B7F3A2B\","
+ +"$NEWLINE\tqop=\"auth\"\r\n\r\n",
+
+ };
+ for (String newLineChar : new String[] { "\n", "\r", "\r\n" }) {
+ for (String template : foldingTemplate)
+ responses.add(template.replace("$NEWLINE", newLineChar));
+ }
+
+ String[] bad = // much of this is to retain parity with legacy MessageHeaders
+ { "HTTP/1.1 200 OK\r\n" +
+ "Connection:\r\n\r\n", // empty value, no body
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Connection:\r\n\r\n" + // empty value, with body
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ ": no header\r\n\r\n", // no/empty header-name, no body, no following header
+
+ "HTTP/1.1 200 OK\r\n" +
+ ": no; header\r\n" + // no/empty header-name, no body, following header
+ "Content-Length: 65\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ ": no header\r\n" + // no/empty header-name
+ "Content-Length: 65\r\n\r\n" +
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ ": no header\r\n\r\n" + // no/empty header-name, followed by header
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Conte\r" +
+ " nt-Length: 9\r\n" + // fold/bad header name ???
+ "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Conte\r" +
+ "nt-Length: 9\r\n" + // fold/bad header name ??? without preceding space
+ "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
+ "XXXXXYYZZ",
+
+ "HTTP/1.0 404 Not Found\r\n" +
+ "header-without-colon\r\n\r\n",
+
+ "HTTP/1.0 404 Not Found\r\n" +
+ "header-without-colon\r\n\r\n" +
+ "SOMEBODY",
+
+ };
+ Arrays.stream(bad).forEach(responses::add);
+
+ return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "responses")
+ public void verifyHeaders(String respString) throws Exception {
+ byte[] bytes = respString.getBytes(US_ASCII);
+ ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
+ MessageHeader m = new MessageHeader(bais);
+ Map<String,List<String>> messageHeaderMap = m.getHeaders();
+ int available = bais.available();
+
+ Http1HeaderParser decoder = new Http1HeaderParser();
+ ByteBuffer b = ByteBuffer.wrap(bytes);
+ decoder.parse(b);
+ Map<String,List<String>> decoderMap1 = decoder.headers().map();
+ assertEquals(available, b.remaining(),
+ "stream available not equal to remaining");
+
+ // assert status-line
+ String statusLine1 = messageHeaderMap.get(null).get(0);
+ String statusLine2 = decoder.statusLine();
+ if (statusLine1.startsWith("HTTP")) {// skip the case where MH's messes up the status-line
+ assertEquals(statusLine1, statusLine2, "Status-line not equal");
+ } else {
+ assertTrue(statusLine2.startsWith("HTTP/1."), "Status-line not HTTP/1.");
+ }
+
+ // remove the null'th entry with is the status-line
+ Map<String,List<String>> map = new HashMap<>();
+ for (Map.Entry<String,List<String>> e : messageHeaderMap.entrySet()) {
+ if (e.getKey() != null) {
+ map.put(e.getKey(), e.getValue());
+ }
+ }
+ messageHeaderMap = map;
+
+ assertHeadersEqual(messageHeaderMap, decoderMap1,
+ "messageHeaderMap not equal to decoderMap1");
+
+ // byte at a time
+ decoder = new Http1HeaderParser();
+ List<ByteBuffer> buffers = IntStream.range(0, bytes.length)
+ .mapToObj(i -> ByteBuffer.wrap(bytes, i, 1))
+ .collect(toList());
+ while (decoder.parse(buffers.remove(0)) != true);
+ Map<String,List<String>> decoderMap2 = decoder.headers().map();
+ assertEquals(available, buffers.size(),
+ "stream available not equals to remaining buffers");
+ assertEquals(decoderMap1, decoderMap2, "decoder maps not equal");
+ }
+
+ @DataProvider(name = "errors")
+ public Object[][] errors() {
+ List<String> responses = new ArrayList<>();
+
+ // These responses are parsed, somewhat, by MessageHeaders but give
+ // nonsensible results. They, correctly, fail with the Http1HeaderParser.
+ String[] bad =
+ {// "HTTP/1.1 402 Payment Required\r\n" +
+ // "Content-Length: 65\r\n\r", // missing trailing LF //TODO: incomplete
+
+ "HTTP/1.1 402 Payment Required\r\n" +
+ "Content-Length: 65\r\n\rT\r\n\r\nGGGGGG",
+
+ "HTTP/1.1 200OK\r\n\rT",
+
+ "HTTP/1.1 200OK\rT",
+ };
+ Arrays.stream(bad).forEach(responses::add);
+
+ return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "errors", expectedExceptions = ProtocolException.class)
+ public void errors(String respString) throws ProtocolException {
+ byte[] bytes = respString.getBytes(US_ASCII);
+ Http1HeaderParser decoder = new Http1HeaderParser();
+ ByteBuffer b = ByteBuffer.wrap(bytes);
+ decoder.parse(b);
+ }
+
+ void assertHeadersEqual(Map<String,List<String>> expected,
+ Map<String,List<String>> actual,
+ String msg) {
+
+ if (expected.equals(actual))
+ return;
+
+ assertEquals(expected.size(), actual.size(),
+ format("%s. Expected size %d, actual size %s. %nexpected= %s,%n actual=%s.",
+ msg, expected.size(), actual.size(), mapToString(expected), mapToString(actual)));
+
+ for (Map.Entry<String,List<String>> e : expected.entrySet()) {
+ String key = e.getKey();
+ List<String> values = e.getValue();
+
+ boolean found = false;
+ for (Map.Entry<String,List<String>> other: actual.entrySet()) {
+ if (key.equalsIgnoreCase(other.getKey())) {
+ found = true;
+ List<String> otherValues = other.getValue();
+ assertEquals(values.size(), otherValues.size(),
+ format("%s. Expected list size %d, actual size %s",
+ msg, values.size(), otherValues.size()));
+ if (!(values.containsAll(otherValues) && otherValues.containsAll(values)))
+ assertTrue(false, format("Lists are unequal [%s] [%s]", values, otherValues));
+ break;
+ }
+ }
+ assertTrue(found, format("header name, %s, not found in %s", key, actual));
+ }
+ }
+
+ static String mapToString(Map<String,List<String>> map) {
+ StringBuilder sb = new StringBuilder();
+ List<String> sortedKeys = new ArrayList(map.keySet());
+ Collections.sort(sortedKeys);
+ for (String key : sortedKeys) {
+ List<String> values = map.get(key);
+ sb.append("\n\t" + key + " | " + values);
+ }
+ return sb.toString();
+ }
+
+ // ---
+
+ /* Main entry point for standalone testing of the main functional test. */
+ public static void main(String... args) throws Exception {
+ Http1HeaderParserTest test = new Http1HeaderParserTest();
+ int count = 0;
+ for (Object[] objs : test.responses()) {
+ out.println("Testing " + count++ + ", " + objs[0]);
+ test.verifyHeaders((String) objs[0]);
+ }
+ for (Object[] objs : test.errors()) {
+ out.println("Testing " + count++ + ", " + objs[0]);
+ try {
+ test.errors((String) objs[0]);
+ throw new RuntimeException("Expected ProtocolException for " + objs[0]);
+ } catch (ProtocolException expected) { /* Ok */ }
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/internal/RawChannelTest.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,333 @@
+/*
+ * 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.
+ *
+ * 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.incubator.http.internal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.internal.websocket.RawChannel;
+import jdk.incubator.http.internal.websocket.WebSocketRequest;
+import org.testng.annotations.Test;
+import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
+import static org.testng.Assert.assertEquals;
+
+/*
+ * This test exercises mechanics of _independent_ reads and writes on the
+ * RawChannel. It verifies that the underlying implementation can manage more
+ * than a single type of notifications at the same time.
+ */
+public class RawChannelTest {
+
+ private final AtomicLong clientWritten = new AtomicLong();
+ private final AtomicLong serverWritten = new AtomicLong();
+ private final AtomicLong clientRead = new AtomicLong();
+ private final AtomicLong serverRead = new AtomicLong();
+
+ /*
+ * Since at this level we don't have any control over the low level socket
+ * parameters, this latch ensures a write to the channel will stall at least
+ * once (socket's send buffer filled up).
+ */
+ private final CountDownLatch writeStall = new CountDownLatch(1);
+ private final CountDownLatch initialWriteStall = new CountDownLatch(1);
+
+ /*
+ * This one works similarly by providing means to ensure a read from the
+ * channel will stall at least once (no more data available on the socket).
+ */
+ private final CountDownLatch readStall = new CountDownLatch(1);
+ private final CountDownLatch initialReadStall = new CountDownLatch(1);
+
+ private final AtomicInteger writeHandles = new AtomicInteger();
+ private final AtomicInteger readHandles = new AtomicInteger();
+
+ private final CountDownLatch exit = new CountDownLatch(1);
+
+ @Test
+ public void test() throws Exception {
+ try (ServerSocket server = new ServerSocket(0)) {
+ int port = server.getLocalPort();
+ new TestServer(server).start();
+
+ final RawChannel chan = channelOf(port);
+ print("RawChannel is %s", String.valueOf(chan));
+ initialWriteStall.await();
+
+ // It's very important not to forget the initial bytes, possibly
+ // left from the HTTP thingy
+ int initialBytes = chan.initialByteBuffer().remaining();
+ print("RawChannel has %s initial bytes", initialBytes);
+ clientRead.addAndGet(initialBytes);
+
+ // tell the server we have read the initial bytes, so
+ // that it makes sure there is something for us to
+ // read next in case the initialBytes have already drained the
+ // channel dry.
+ initialReadStall.countDown();
+
+ chan.registerEvent(new RawChannel.RawEvent() {
+
+ private final ByteBuffer reusableBuffer = ByteBuffer.allocate(32768);
+
+ @Override
+ public int interestOps() {
+ return SelectionKey.OP_WRITE;
+ }
+
+ @Override
+ public void handle() {
+ int i = writeHandles.incrementAndGet();
+ print("OP_WRITE #%s", i);
+ if (i > 3) { // Fill up the send buffer not more than 3 times
+ try {
+ chan.shutdownOutput();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return;
+ }
+ long total = 0;
+ try {
+ long n;
+ do {
+ ByteBuffer[] array = {reusableBuffer.slice()};
+ n = chan.write(array, 0, 1);
+ total += n;
+ } while (n > 0);
+ print("OP_WRITE clogged SNDBUF with %s bytes", total);
+ clientWritten.addAndGet(total);
+ chan.registerEvent(this);
+ writeStall.countDown(); // signal send buffer is full
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+ });
+
+ chan.registerEvent(new RawChannel.RawEvent() {
+
+ @Override
+ public int interestOps() {
+ return SelectionKey.OP_READ;
+ }
+
+ @Override
+ public void handle() {
+ int i = readHandles.incrementAndGet();
+ print("OP_READ #%s", i);
+ ByteBuffer read = null;
+ long total = 0;
+ while (true) {
+ try {
+ read = chan.read();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ if (read == null) {
+ print("OP_READ EOF");
+ break;
+ } else if (!read.hasRemaining()) {
+ print("OP_READ stall");
+ try {
+ chan.registerEvent(this);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ readStall.countDown();
+ break;
+ }
+ int r = read.remaining();
+ total += r;
+ clientRead.addAndGet(r);
+ }
+ print("OP_READ read %s bytes (%s total)", total, clientRead.get());
+ }
+ });
+ exit.await(); // All done, we need to compare results:
+ assertEquals(clientRead.get(), serverWritten.get());
+ assertEquals(serverRead.get(), clientWritten.get());
+ }
+ }
+
+ private static RawChannel channelOf(int port) throws Exception {
+ URI uri = URI.create("http://127.0.0.1:" + port + "/");
+ print("raw channel to %s", uri.toString());
+ HttpRequest req = HttpRequest.newBuilder(uri).build();
+ // Switch on isWebSocket flag to prevent the connection from
+ // being returned to the pool.
+ ((WebSocketRequest)req).isWebSocket(true);
+ HttpClient client = HttpClient.newHttpClient();
+ try {
+ HttpResponse<?> r = client.send(req, discard(null));
+ r.body();
+ return ((HttpResponseImpl) r).rawChannel();
+ } finally {
+ // Need to hold onto the client until the RawChannel is
+ // created. This would not be needed if we had created
+ // a WebSocket, but here we are fiddling directly
+ // with the internals of HttpResponseImpl!
+ java.lang.ref.Reference.reachabilityFence(client);
+ }
+ }
+
+ private class TestServer extends Thread { // Powered by Slowpokes
+
+ private final ServerSocket server;
+
+ TestServer(ServerSocket server) throws IOException {
+ this.server = server;
+ }
+
+ @Override
+ public void run() {
+ try (Socket s = server.accept()) {
+ InputStream is = s.getInputStream();
+ OutputStream os = s.getOutputStream();
+
+ processHttp(is, os);
+
+ Thread reader = new Thread(() -> {
+ try {
+ long n = readSlowly(is);
+ print("Server read %s bytes", n);
+ serverRead.addAndGet(n);
+ s.shutdownInput();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+
+ Thread writer = new Thread(() -> {
+ try {
+ long n = writeSlowly(os);
+ print("Server written %s bytes", n);
+ serverWritten.addAndGet(n);
+ s.shutdownOutput();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+
+ reader.start();
+ writer.start();
+
+ reader.join();
+ writer.join();
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ exit.countDown();
+ }
+ }
+
+ private void processHttp(InputStream is, OutputStream os)
+ throws IOException
+ {
+ os.write("HTTP/1.1 200 OK\r\nContent-length: 0\r\n\r\n".getBytes());
+
+ // write some initial bytes
+ byte[] initial = byteArrayOfSize(1024);
+ os.write(initial);
+ os.flush();
+ serverWritten.addAndGet(initial.length);
+ initialWriteStall.countDown();
+
+ byte[] buf = new byte[1024];
+ String s = "";
+ while (true) {
+ int n = is.read(buf);
+ if (n <= 0) {
+ throw new RuntimeException("Unexpected end of request");
+ }
+ s = s + new String(buf, 0, n);
+ if (s.contains("\r\n\r\n")) {
+ break;
+ }
+ }
+ }
+
+ private long writeSlowly(OutputStream os) throws Exception {
+ byte[] first = byteArrayOfSize(1024);
+ long total = first.length;
+ os.write(first);
+ os.flush();
+
+ // wait until initial bytes were read
+ initialReadStall.await();
+
+ // make sure there is something to read, otherwise readStall
+ // will never be counted down.
+ first = byteArrayOfSize(1024);
+ os.write(first);
+ os.flush();
+ total += first.length;
+
+ // Let's wait for the signal from the raw channel that its read has
+ // stalled, and then continue sending a bit more stuff
+ readStall.await();
+ for (int i = 0; i < 32; i++) {
+ byte[] b = byteArrayOfSize(1024);
+ os.write(b);
+ os.flush();
+ total += b.length;
+ TimeUnit.MILLISECONDS.sleep(1);
+ }
+ return total;
+ }
+
+ private long readSlowly(InputStream is) throws Exception {
+ // Wait for the raw channel to fill up its send buffer
+ writeStall.await();
+ long overall = 0;
+ byte[] array = new byte[1024];
+ for (int n = 0; n != -1; n = is.read(array)) {
+ TimeUnit.MILLISECONDS.sleep(1);
+ overall += n;
+ }
+ return overall;
+ }
+ }
+
+ private static void print(String format, Object... args) {
+ System.out.println(Thread.currentThread() + ": " + String.format(format, args));
+ }
+
+ private static byte[] byteArrayOfSize(int bound) {
+ return new byte[new Random().nextInt(1 + bound)];
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/jdk.incubator.httpclient/jdk/incubator/http/internal/SelectorTest.java Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,224 @@
+/*
+ * 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.
+ *
+ * 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.incubator.http.internal;
+
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.nio.ByteBuffer;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicInteger;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import org.testng.annotations.Test;
+import jdk.incubator.http.internal.websocket.RawChannel;
+import static java.lang.System.out;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
+
+/**
+ * Whitebox test of selector mechanics. Currently only a simple test
+ * setting one read and one write event is done. It checks that the
+ * write event occurs first, followed by the read event and then no
+ * further events occur despite the conditions actually still existing.
+ */
+@Test
+public class SelectorTest {
+
+ AtomicInteger counter = new AtomicInteger();
+ volatile boolean error;
+ static final CountDownLatch finishingGate = new CountDownLatch(1);
+ static volatile HttpClient staticDefaultClient;
+
+ static HttpClient defaultClient() {
+ if (staticDefaultClient == null) {
+ synchronized (SelectorTest.class) {
+ staticDefaultClient = HttpClient.newHttpClient();
+ }
+ }
+ return staticDefaultClient;
+ }
+
+ String readSomeBytes(RawChannel chan) {
+ try {
+ ByteBuffer buf = chan.read();
+ if (buf == null) {
+ out.println("chan read returned null");
+ return null;
+ }
+ buf.flip();
+ byte[] bb = new byte[buf.remaining()];
+ buf.get(bb);
+ return new String(bb, US_ASCII);
+ } catch (IOException ioe) {
+ throw new UncheckedIOException(ioe);
+ }
+ }
+
+ @Test
+ public void test() throws Exception {
+
+ try (ServerSocket server = new ServerSocket(0)) {
+ int port = server.getLocalPort();
+
+ out.println("Listening on port " + server.getLocalPort());
+
+ TestServer t = new TestServer(server);
+ t.start();
+ out.println("Started server thread");
+
+ try (RawChannel chan = getARawChannel(port)) {
+
+ chan.registerEvent(new RawChannel.RawEvent() {
+ @Override
+ public int interestOps() {
+ return SelectionKey.OP_READ;
+ }
+
+ @Override
+ public void handle() {
+ readSomeBytes(chan);
+ out.printf("OP_READ\n");
+ final int count = counter.get();
+ if (count != 1) {
+ out.printf("OP_READ error counter = %d\n", count);
+ error = true;
+ }
+ }
+ });
+
+ chan.registerEvent(new RawChannel.RawEvent() {
+ @Override
+ public int interestOps() {
+ return SelectionKey.OP_WRITE;
+ }
+
+ @Override
+ public void handle() {
+ out.printf("OP_WRITE\n");
+ final int count = counter.get();
+ if (count != 0) {
+ out.printf("OP_WRITE error counter = %d\n", count);
+ error = true;
+ } else {
+ ByteBuffer bb = ByteBuffer.wrap(TestServer.INPUT);
+ counter.incrementAndGet();
+ try {
+ chan.write(new ByteBuffer[]{bb}, 0, 1);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+ }
+
+ });
+ out.println("Events registered. Waiting");
+ finishingGate.await(30, SECONDS);
+ if (error)
+ throw new RuntimeException("Error");
+ else
+ out.println("No error");
+ }
+ }
+ }
+
+ static RawChannel getARawChannel(int port) throws Exception {
+ URI uri = URI.create("http://127.0.0.1:" + port + "/");
+ out.println("client connecting to " + uri.toString());
+ HttpRequest req = HttpRequest.newBuilder(uri).build();
+ // Otherwise HttpClient will think this is an ordinary connection and
+ // thus all ordinary procedures apply to it, e.g. it must be put into
+ // the cache
+ ((HttpRequestImpl) req).isWebSocket(true);
+ HttpResponse<?> r = defaultClient().send(req, discard(null));
+ r.body();
+ return ((HttpResponseImpl) r).rawChannel();
+ }
+
+ static class TestServer extends Thread {
+ static final byte[] INPUT = "Hello world".getBytes(US_ASCII);
+ static final byte[] OUTPUT = "Goodbye world".getBytes(US_ASCII);
+ static final String FIRST_RESPONSE = "HTTP/1.1 200 OK\r\nContent-length: 0\r\n\r\n";
+ final ServerSocket server;
+
+ TestServer(ServerSocket server) throws IOException {
+ this.server = server;
+ }
+
+ public void run() {
+ try (Socket s = server.accept();
+ InputStream is = s.getInputStream();
+ OutputStream os = s.getOutputStream()) {
+
+ out.println("Got connection");
+ readRequest(is);
+ os.write(FIRST_RESPONSE.getBytes());
+ read(is);
+ write(os);
+ Thread.sleep(1000);
+ // send some more data, and make sure WRITE op does not get called
+ write(os);
+ out.println("TestServer exiting");
+ SelectorTest.finishingGate.countDown();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ // consumes the HTTP request
+ static void readRequest(InputStream is) throws IOException {
+ out.println("starting readRequest");
+ byte[] buf = new byte[1024];
+ String s = "";
+ while (true) {
+ int n = is.read(buf);
+ if (n <= 0)
+ throw new IOException("Error");
+ s = s + new String(buf, 0, n);
+ if (s.indexOf("\r\n\r\n") != -1)
+ break;
+ }
+ out.println("returning from readRequest");
+ }
+
+ static void read(InputStream is) throws IOException {
+ out.println("starting read");
+ for (int i = 0; i < INPUT.length; i++) {
+ int c = is.read();
+ if (c == -1)
+ throw new IOException("closed");
+ if (INPUT[i] != (byte) c)
+ throw new IOException("Error. Expected:" + INPUT[i] + ", got:" + c);
+ }
+ out.println("returning from read");
+ }
+
+ static void write(OutputStream os) throws IOException {
+ out.println("doing write");
+ os.write(OUTPUT);
+ }
+ }
+}