# HG changeset patch # User chegar # Date 1518039937 0 # Node ID fd85b2bf2b0d1413ba8a54dcebd25ed7b4aef8a5 # Parent aedd6133e7a067879fccdee7e72d833c91ef09a1 http-client-branch: move implementation to jdk.internal.net.http diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/HttpClient.java --- a/src/java.net.http/share/classes/java/net/http/HttpClient.java Wed Feb 07 15:46:30 2018 +0000 +++ b/src/java.net.http/share/classes/java/net/http/HttpClient.java Wed Feb 07 21:45:37 2018 +0000 @@ -40,7 +40,7 @@ import javax.net.ssl.SSLParameters; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.PushPromiseHandler; -import java.net.http.internal.HttpClientBuilderImpl; +import jdk.internal.net.http.HttpClientBuilderImpl; /** * An HTTP Client. diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/HttpRequest.java --- a/src/java.net.http/share/classes/java/net/http/HttpRequest.java Wed Feb 07 15:46:30 2018 +0000 +++ b/src/java.net.http/share/classes/java/net/http/HttpRequest.java Wed Feb 07 21:45:37 2018 +0000 @@ -45,8 +45,8 @@ import java.util.concurrent.Flow; import java.util.function.Supplier; import java.net.http.HttpResponse.BodyHandler; -import java.net.http.internal.HttpRequestBuilderImpl; -import java.net.http.internal.RequestPublishers; +import jdk.internal.net.http.HttpRequestBuilderImpl; +import jdk.internal.net.http.RequestPublishers; import static java.nio.charset.StandardCharsets.UTF_8; /** diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/HttpResponse.java --- a/src/java.net.http/share/classes/java/net/http/HttpResponse.java Wed Feb 07 15:46:30 2018 +0000 +++ b/src/java.net.http/share/classes/java/net/http/HttpResponse.java Wed Feb 07 21:45:37 2018 +0000 @@ -48,13 +48,13 @@ import java.util.function.Function; import java.util.stream.Stream; import javax.net.ssl.SSLParameters; -import java.net.http.internal.BufferingSubscriber; -import java.net.http.internal.LineSubscriberAdapter; -import java.net.http.internal.ResponseBodyHandlers.FileDownloadBodyHandler; -import java.net.http.internal.ResponseBodyHandlers.PathBodyHandler; -import java.net.http.internal.ResponseBodyHandlers.PushPromisesHandlerWithMap; -import java.net.http.internal.ResponseSubscribers; -import static java.net.http.internal.common.Utils.charsetFrom; +import jdk.internal.net.http.BufferingSubscriber; +import jdk.internal.net.http.LineSubscriberAdapter; +import jdk.internal.net.http.ResponseBodyHandlers.FileDownloadBodyHandler; +import jdk.internal.net.http.ResponseBodyHandlers.PathBodyHandler; +import jdk.internal.net.http.ResponseBodyHandlers.PushPromisesHandlerWithMap; +import jdk.internal.net.http.ResponseSubscribers; +import static jdk.internal.net.http.common.Utils.charsetFrom; /** * Represents a response to a {@link HttpRequest}. diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/AbstractAsyncSSLConnection.java --- a/src/java.net.http/share/classes/java/net/http/internal/AbstractAsyncSSLConnection.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,188 +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 java.net.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 java.net.http.internal.common.SSLTube; -import java.net.http.internal.common.Log; -import java.net.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 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); - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/AbstractSubscription.java --- a/src/java.net.http/share/classes/java/net/http/internal/AbstractSubscription.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,45 +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 java.net.http.internal; - -import java.util.concurrent.Flow; -import java.net.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; } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/AsyncEvent.java --- a/src/java.net.http/share/classes/java/net/http/internal/AsyncEvent.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,70 +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 java.net.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; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/AsyncSSLConnection.java --- a/src/java.net.http/share/classes/java/net/http/internal/AsyncSSLConnection.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,115 +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 java.net.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 java.net.http.internal.common.SSLTube; -import java.net.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 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; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/AsyncSSLTunnelConnection.java --- a/src/java.net.http/share/classes/java/net/http/internal/AsyncSSLTunnelConnection.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,129 +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 java.net.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 java.net.http.HttpHeaders; -import java.net.http.internal.common.SSLTube; -import java.net.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 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; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/AsyncTriggerEvent.java --- a/src/java.net.http/share/classes/java/net/http/internal/AsyncTriggerEvent.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,59 +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 java.net.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 errorHandler; - AsyncTriggerEvent(Consumer 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; } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/AuthenticationFilter.java --- a/src/java.net.http/share/classes/java/net/http/internal/AuthenticationFilter.java Wed Feb 07 15:46:30 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 java.net.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 java.net.http.HttpHeaders; -import java.net.http.internal.common.Log; -import java.net.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 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 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 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); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/BufferingSubscriber.java --- a/src/java.net.http/share/classes/java/net/http/internal/BufferingSubscriber.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,315 +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 java.net.http.internal; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.ListIterator; -import java.util.Objects; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.Flow; -import java.util.concurrent.atomic.AtomicBoolean; -import java.net.http.HttpResponse.BodySubscriber; -import java.net.http.internal.common.Demand; -import java.net.http.internal.common.SequentialScheduler; -import java.net.http.internal.common.Utils; - -/** - * A buffering BodySubscriber. When subscribed, accumulates ( buffers ) a given - * amount ( in bytes ) of a publisher's data before pushing it to a downstream - * subscriber. - */ -public class BufferingSubscriber implements BodySubscriber -{ - /** The downstream consumer of the data. */ - private final BodySubscriber downstreamSubscriber; - /** The amount of data to be accumulate before pushing downstream. */ - private final int bufferSize; - - /** The subscription, created lazily. */ - private volatile Flow.Subscription subscription; - /** The downstream subscription, created lazily. */ - private volatile DownstreamSubscription downstreamSubscription; - - /** Must be held when accessing the internal buffers. */ - private final Object buffersLock = new Object(); - /** The internal buffers holding the buffered data. */ - private ArrayList internalBuffers; - /** The actual accumulated remaining bytes in internalBuffers. */ - private int accumulatedBytes; - - /** Holds the Throwable from upstream's onError. */ - private volatile Throwable throwable; - - /** State of the buffering subscriber: - * 1) [UNSUBSCRIBED] when initially created - * 2) [ACTIVE] when subscribed and can receive data - * 3) [ERROR | CANCELLED | COMPLETE] (terminal state) - */ - static final int UNSUBSCRIBED = 0x01; - static final int ACTIVE = 0x02; - static final int ERROR = 0x04; - static final int CANCELLED = 0x08; - static final int COMPLETE = 0x10; - - private volatile int state; - - public BufferingSubscriber(BodySubscriber downstreamSubscriber, - int bufferSize) { - this.downstreamSubscriber = Objects.requireNonNull(downstreamSubscriber); - this.bufferSize = bufferSize; - synchronized (buffersLock) { - internalBuffers = new ArrayList<>(); - } - state = UNSUBSCRIBED; - } - - /** Returns the number of bytes remaining in the given buffers. */ - private static final long remaining(List buffers) { - return buffers.stream().mapToLong(ByteBuffer::remaining).sum(); - } - - /** - * Tells whether, or not, there is at least a sufficient number of bytes - * accumulated in the internal buffers. If the subscriber is COMPLETE, and - * has some buffered data, then there is always enough ( to pass downstream ). - */ - private final boolean hasEnoughAccumulatedBytes() { - assert Thread.holdsLock(buffersLock); - return accumulatedBytes >= bufferSize - || (state == COMPLETE && accumulatedBytes > 0); - } - - /** - * Returns a new, unmodifiable, List containing exactly the - * amount of data as required before pushing downstream. The amount of data - * may be less than required ( bufferSize ), in the case where the subscriber - * is COMPLETE. - */ - private List fromInternalBuffers() { - assert Thread.holdsLock(buffersLock); - int leftToFill = bufferSize; - int state = this.state; - assert (state == ACTIVE || state == CANCELLED) - ? accumulatedBytes >= leftToFill : true; - List dsts = new ArrayList<>(); - - ListIterator itr = internalBuffers.listIterator(); - while (itr.hasNext()) { - ByteBuffer b = itr.next(); - if (b.remaining() <= leftToFill) { - itr.remove(); - if (b.position() != 0) - b = b.slice(); // ensure position = 0 when propagated - dsts.add(b); - leftToFill -= b.remaining(); - accumulatedBytes -= b.remaining(); - if (leftToFill == 0) - break; - } else { - int prevLimit = b.limit(); - b.limit(b.position() + leftToFill); - ByteBuffer slice = b.slice(); - dsts.add(slice); - b.limit(prevLimit); - b.position(b.position() + leftToFill); - accumulatedBytes -= leftToFill; - leftToFill = 0; - break; - } - } - assert (state == ACTIVE || state == CANCELLED) - ? leftToFill == 0 : state == COMPLETE; - assert (state == ACTIVE || state == CANCELLED) - ? remaining(dsts) == bufferSize : state == COMPLETE; - assert accumulatedBytes >= 0; - assert dsts.stream().noneMatch(b -> b.position() != 0); - return Collections.unmodifiableList(dsts); - } - - /** Subscription that is passed to the downstream subscriber. */ - private class DownstreamSubscription implements Flow.Subscription { - private final AtomicBoolean cancelled = new AtomicBoolean(); // false - private final Demand demand = new Demand(); - private volatile boolean illegalArg; - - @Override - public void request(long n) { - if (cancelled.get() || illegalArg) { - return; - } - if (n <= 0L) { - // pass the "bad" value upstream so the Publisher can deal with - // it appropriately, i.e. invoke onError - illegalArg = true; - subscription.request(n); - return; - } - - demand.increase(n); - - pushDemanded(); - } - - private final SequentialScheduler pushDemandedScheduler = - new SequentialScheduler(new PushDemandedTask()); - - void pushDemanded() { - if (cancelled.get()) - return; - pushDemandedScheduler.runOrSchedule(); - } - - class PushDemandedTask extends SequentialScheduler.CompleteRestartableTask { - @Override - public void run() { - try { - Throwable t = throwable; - if (t != null) { - pushDemandedScheduler.stop(); // stop the demand scheduler - downstreamSubscriber.onError(t); - return; - } - - while (true) { - List item; - synchronized (buffersLock) { - if (cancelled.get()) - return; - if (!hasEnoughAccumulatedBytes()) - break; - if (!demand.tryDecrement()) - break; - item = fromInternalBuffers(); - } - assert item != null; - - downstreamSubscriber.onNext(item); - } - if (cancelled.get()) - return; - - // complete only if all data consumed - boolean complete; - synchronized (buffersLock) { - complete = state == COMPLETE && internalBuffers.isEmpty(); - } - if (complete) { - assert internalBuffers.isEmpty(); - pushDemandedScheduler.stop(); // stop the demand scheduler - downstreamSubscriber.onComplete(); - return; - } - } catch (Throwable t) { - cancel(); // cancel if there is any error - throw t; - } - - boolean requestMore = false; - synchronized (buffersLock) { - if (!hasEnoughAccumulatedBytes() && !demand.isFulfilled()) { - // request more upstream data - requestMore = true; - } - } - if (requestMore) - subscription.request(1); - } - } - - @Override - public void cancel() { - if (cancelled.compareAndExchange(false, true)) - return; // already cancelled - - state = CANCELLED; // set CANCELLED state of upstream subscriber - subscription.cancel(); // cancel upstream subscription - pushDemandedScheduler.stop(); // stop the demand scheduler - } - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - Objects.requireNonNull(subscription); - if (this.subscription != null) { - subscription.cancel(); - return; - } - - int s = this.state; - assert s == UNSUBSCRIBED; - state = ACTIVE; - this.subscription = subscription; - downstreamSubscription = new DownstreamSubscription(); - downstreamSubscriber.onSubscribe(downstreamSubscription); - } - - @Override - public void onNext(List item) { - Objects.requireNonNull(item); - - int s = state; - if (s == CANCELLED) - return; - - if (s != ACTIVE) - throw new InternalError("onNext on inactive subscriber"); - - synchronized (buffersLock) { - internalBuffers.addAll(item); - accumulatedBytes += remaining(item); - } - - downstreamSubscription.pushDemanded(); - } - - @Override - public void onError(Throwable incomingThrowable) { - Objects.requireNonNull(incomingThrowable); - int s = state; - assert s == ACTIVE : "Expected ACTIVE, got:" + s; - state = ERROR; - Throwable t = this.throwable; - assert t == null : "Expected null, got:" + t; - this.throwable = incomingThrowable; - downstreamSubscription.pushDemanded(); - } - - @Override - public void onComplete() { - int s = state; - assert s == ACTIVE : "Expected ACTIVE, got:" + s; - state = COMPLETE; - downstreamSubscription.pushDemanded(); - } - - @Override - public CompletionStage getBody() { - return downstreamSubscriber.getBody(); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/ConnectionPool.java --- a/src/java.net.http/share/classes/java/net/http/internal/ConnectionPool.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,490 +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 java.net.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 java.net.http.internal.common.FlowTube; -import java.net.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> plainPool; - private final HashMap> 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> pool) { - LinkedList 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> pool) { - //System.out.println("cacheCleaner removing: " + c); - assert Thread.holdsLock(this); - CacheKey k = c.cacheKey(); - List l = pool.get(k); - if (l == null || l.isEmpty()) { - pool.remove(k); - return false; - } - return l.remove(c); - } - - private void - putConnection(HttpConnection c, - HashMap> pool) { - CacheKey key = c.cacheKey(); - LinkedList 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 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 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 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 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 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 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 purgeUntil(Instant now) { - if (list.isEmpty()) return Collections.emptyList(); - - List 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 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 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 item) { - triggerCleanup(new IOException("Data received while in pool")); - } - - @Override - public void subscribe(Flow.Subscriber> subscriber) { - subscriber.onSubscribe(this); - } - - @Override - public String toString() { - return "CleanupTrigger(" + connection.getConnectionFlow() + ")"; - } - - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/CookieFilter.java --- a/src/java.net.http/share/classes/java/net/http/internal/CookieFilter.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,89 +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 java.net.http.internal; - -import java.io.IOException; -import java.net.CookieHandler; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.net.http.HttpHeaders; -import java.net.http.internal.common.HttpHeadersImpl; -import java.net.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 cookieHandlerOpt = client.cookieHandler(); - if (cookieHandlerOpt.isPresent()) { - CookieHandler cookieHandler = cookieHandlerOpt.get(); - Map> userheaders = r.getUserHeaders().map(); - Map> 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 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 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; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/Exchange.java --- a/src/java.net.http/share/classes/java/net/http/internal/Exchange.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,574 +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 java.net.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 java.net.http.HttpClient; -import java.net.http.HttpHeaders; -import java.net.http.HttpResponse; -import java.net.http.HttpTimeoutException; -import java.net.http.internal.common.MinimalFuture; -import java.net.http.internal.common.Utils; -import java.net.http.internal.common.Log; - -import static java.net.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 { - - 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 exchImpl; - volatile CompletableFuture> exchangeCF; - volatile CompletableFuture bodyIgnored; - - // used to record possible cancellation raised before the exchImpl - // has been established. - private volatile IOException failed; - final AccessControlContext acc; - final MultiExchange multi; - final Executor parentExecutor; - boolean upgrading; // to HTTP/2 - final PushGroup pushGroup; - final String dbgTag; - - Exchange(HttpRequestImpl request, MultiExchange 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 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 getPushGroup() { - return pushGroup; - } - - Executor executor() { - return parentExecutor; - } - - public HttpRequestImpl request() { - return request; - } - - HttpClientImpl client() { - return client; - } - - - public CompletableFuture readBodyAsync(HttpResponse.BodyHandler 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 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> 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> - 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> 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 responseAsync() { - return responseAsyncImpl(null); - } - - CompletableFuture 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 checkFor407(ExchangeImpl ex, Throwable t, - Function,CompletableFuture> 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 expectContinue(ExchangeImpl 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 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 sendRequestBody(ExchangeImpl ex) { - assert !request.expectContinue(); - CompletableFuture cf = ex.sendBodyAsync() - .thenCompose(exIm -> exIm.getResponseAsync(parentExecutor)); - cf = wrapForUpgrade(cf); - cf = wrapForLog(cf); - return cf; - } - - CompletableFuture responseAsyncImpl0(HttpConnection connection) { - Function, CompletableFuture> 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, CompletableFuture> 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 wrapForUpgrade(CompletableFuture cf) { - if (upgrading) { - return cf.thenCompose(r -> checkForUpgradeAsync(r, exchImpl)); - } - return cf; - } - - private CompletableFuture wrapForLog(CompletableFuture cf) { - if (Log.requests()) { - return cf.thenApply(response -> { - Log.logResponse(response::toString); - return response; - }); - } - return cf; - } - - HttpResponse.BodySubscriber ignoreBody(int status, HttpHeaders hdrs) { - return HttpResponse.BodySubscriber.replace(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 - checkForUpgradeAsync(Response resp, - ExchangeImpl ex) { - - int rcode = resp.statusCode(); - if (upgrading && (rcode == 101)) { - Http1Exchange e = (Http1Exchange)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 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> 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; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/ExchangeImpl.java --- a/src/java.net.http/share/classes/java/net/http/internal/ExchangeImpl.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,211 +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 java.net.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 java.net.http.HttpResponse; -import java.net.http.internal.common.MinimalFuture; -import static java.net.http.HttpClient.Version.HTTP_1_1; -import java.net.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 { - - static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag. - private static final System.Logger DEBUG_LOGGER = - Utils.getDebugLogger("ExchangeImpl"::toString, DEBUG); - - final Exchange exchange; - - ExchangeImpl(Exchange e) { - // e == null means a http/2 pushed stream - this.exchange = e; - } - - final Exchange 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 CompletableFuture> - get(Exchange 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 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 CompletableFuture> - createExchangeImpl(Http2Connection c, - Throwable t, - Exchange 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> 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> 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 s = c.createStream(exchange); - CompletableFuture> ex = MinimalFuture.completedFuture(s); - return ex; - } - } - - private static CompletableFuture> - createHttp1Exchange(Exchange 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> sendHeadersAsync(); - - /** Sends a request body, after request headers have been sent. */ - abstract CompletableFuture> sendBodyAsync(); - - abstract CompletableFuture readBodyAsync(HttpResponse.BodyHandler handler, - boolean returnConnectionToPool, - Executor executor); - - /** - * Ignore/consume the body. - */ - abstract CompletableFuture ignoreBody(); - - /** Gets the response headers. Completes before body is read. */ - abstract CompletableFuture 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(); -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/FilterFactory.java --- a/src/java.net.http/share/classes/java/net/http/internal/FilterFactory.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,52 +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 java.net.http.internal; - -import java.util.LinkedList; -import java.util.List; - -class FilterFactory { - - final LinkedList> filterClasses = new LinkedList<>(); - - public void addFilter(Class type) { - filterClasses.add(type); - } - - List getFilterChain() { - List l = new LinkedList<>(); - for (Class 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; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/HeaderFilter.java --- a/src/java.net.http/share/classes/java/net/http/internal/HeaderFilter.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,45 +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 java.net.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; -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/HeaderParser.java --- a/src/java.net.http/share/classes/java/net/http/internal/HeaderParser.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,252 +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 java.net.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 { - int index; - boolean returnsValue; // or key - - ParserIterator (boolean returnValue) { - returnsValue = returnValue; - } - @Override - public boolean hasNext () { - return index= nkeys) { - throw new NoSuchElementException(); - } - return tab[index++][returnsValue?1:0]; - } - } - - public Iterator keys () { - return new ParserIterator (false); - } - -// public Iterator values () { -// return new ParserIterator (true); -// } - - @Override - public String toString () { - Iterator 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; -// } -// } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/Http1AsyncReceiver.java --- a/src/java.net.http/share/classes/java/net/http/internal/Http1AsyncReceiver.java Wed Feb 07 15:46:30 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 java.net.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 java.net.http.internal.common.Demand; -import java.net.http.internal.common.FlowTube.TubeSubscriber; -import java.net.http.internal.common.SequentialScheduler; -import java.net.http.internal.common.ConnectionExpiredException; -import java.net.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 queue - = new ConcurrentLinkedDeque<>(); - private final SequentialScheduler scheduler = - SequentialScheduler.synchronizedScheduler(this::flush); - private final Executor executor; - private final Http1TubeSubscriber subscriber = new Http1TubeSubscriber(); - private final AtomicReference 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 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 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= 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 lbb = Arrays.asList(qbb); - Set 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; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/Http1Exchange.java --- a/src/java.net.http/share/classes/java/net/http/internal/Http1Exchange.java Wed Feb 07 15:46:30 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 java.net.http.internal; - -import java.io.IOException; -import java.lang.System.Logger.Level; -import java.net.InetSocketAddress; -import java.net.http.HttpResponse.BodyHandler; -import java.net.http.HttpResponse.BodySubscriber; -import java.nio.ByteBuffer; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.Executor; -import java.util.concurrent.Flow; -import java.net.http.internal.common.Demand; -import java.net.http.internal.common.Log; -import java.net.http.internal.common.FlowTube; -import java.net.http.internal.common.SequentialScheduler; -import java.net.http.internal.common.MinimalFuture; -import java.net.http.internal.common.Utils; -import static java.net.http.HttpClient.Version.HTTP_1_1; - -/** - * Encapsulates one HTTP/1.1 request/response exchange. - */ -class Http1Exchange extends ExchangeImpl { - - 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 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> 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 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> headersSentCF = new MinimalFuture<>(); - /** Completed when the body has been published, or there is an error */ - private final CompletableFuture> 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 data; - DataPair(List 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 { - protected volatile Flow.Subscription subscription; - protected volatile boolean complete; - - /** Final sentinel in the stream of request body. */ - static final List 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 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> 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 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 cf = new MinimalFuture<>(); - try { - connectFlows(connection); - - debug.log(Level.DEBUG, "requestAction.headers"); - List 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> 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 getResponseAsync(Executor executor) { - CompletableFuture 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 readBodyAsync(BodyHandler handler, - boolean returnConnectionToPool, - Executor executor) - { - BodySubscriber bs = handler.apply(response.responseCode(), - response.responseHeaders()); - CompletableFuture bodyCF = response.readBody(bs, - returnConnectionToPool, - executor); - return bodyCF; - } - - @Override - CompletableFuture ignoreBody() { - return response.ignoreBody(executor); - } - - ByteBuffer drainLeftOverBytes() { - synchronized (lock) { - asyncReceiver.stop(); - return asyncReceiver.drain(Utils.EMPTY_BYTEBUFFER); - } - } - - void released() { - Http1Response resp = this.response; - if (resp != null) resp.completed(); - asyncReceiver.clear(); - } - - void completed() { - Http1Response 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> 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 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> 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> 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 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"; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/Http1HeaderParser.java --- a/src/java.net.http/share/classes/java/net/http/internal/Http1HeaderParser.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,275 +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 java.net.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 java.net.http.HttpHeaders; -import static java.lang.String.format; -import static java.util.Objects.requireNonNull; - -class Http1HeaderParser { - - private static final char CR = '\r'; - private static final char LF = '\n'; - private static final char HT = '\t'; - private static final char SP = ' '; - - private StringBuilder sb = new StringBuilder(); - private String statusLine; - private int responseCode; - private HttpHeaders headers; - private Map> 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)); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/Http1Request.java --- a/src/java.net.http/share/classes/java/net/http/internal/Http1Request.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,390 +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 java.net.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 java.net.http.HttpHeaders; -import java.net.http.HttpRequest; -import java.net.http.internal.Http1Exchange.Http1BodySubscriber; -import java.net.http.internal.common.HttpHeadersImpl; -import java.net.http.internal.common.Log; -import java.net.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> 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> filter) { - for (Map.Entry> entry : headers.map().entrySet()) { - String key = entry.getKey(); - List 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 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 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 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); - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/Http1Response.java --- a/src/java.net.http/share/classes/java/net/http/internal/Http1Response.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,525 +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 java.net.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 java.net.http.HttpHeaders; -import java.net.http.HttpResponse; -import java.net.http.internal.ResponseContent.BodyParser; -import java.net.http.internal.common.Log; -import java.net.http.internal.common.MinimalFuture; -import java.net.http.internal.common.Utils; -import static java.net.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 { - - private volatile ResponseContent content; - private final HttpRequestImpl request; - private Response response; - private final HttpConnection connection; - private HttpHeaders headers; - private int responseCode; - private final Http1Exchange 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 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 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 cf = headersReader.completion(); - assert cf != null : "parsing not started"; - - Function 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 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(), true, executor); - } - } - - public CompletableFuture readBody(HttpResponse.BodySubscriber p, - boolean return2Cache, - Executor executor) { - this.return2Cache = return2Cache; - final HttpResponse.BodySubscriber pusher = p; - - final CompletableFuture cf = new MinimalFuture<>(); - - int clen0 = (int)headers.firstValueAsLong("Content-Length").orElse(-1); - - final int clen = fixupContentLen(clen0); - - // expect-continue reads headers and body twice. - // if we reach here, we must reset the headersReader state. - asyncReceiver.unsubscribe(headersReader); - headersReader.reset(); - - 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 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); - } - } - }); - p.getBody().whenComplete((U u, Throwable t) -> { - if (t == null) - cf.complete(u); - else - cf.completeExceptionally(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 - implements Http1AsyncReceiver.Http1AsyncDelegate { - abstract void start(T parser); - abstract CompletableFuture 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 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 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 { - final Consumer onComplete; - volatile Http1HeaderParser parser; - volatile CompletableFuture cf; - volatile long count; // bytes parsed (for debug) - volatile AbstractSubscription subscription; - - HeadersReader(Consumer 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 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 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 { - final Consumer onComplete; - volatile BodyParser parser; - volatile CompletableFuture cf; - volatile AbstractSubscription subscription; - BodyReader(Consumer 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 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 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); - } - - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/Http2ClientImpl.java --- a/src/java.net.http/share/classes/java/net/http/internal/Http2ClientImpl.java Wed Feb 07 15:46:30 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 java.net.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 java.net.http.internal.common.Log; -import java.net.http.internal.common.MinimalFuture; -import java.net.http.internal.common.Utils; -import java.net.http.internal.frame.SettingsFrame; -import static java.net.http.internal.frame.SettingsFrame.INITIAL_WINDOW_SIZE; -import static java.net.http.internal.frame.SettingsFrame.ENABLE_PUSH; -import static java.net.http.internal.frame.SettingsFrame.HEADER_TABLE_SIZE; -import static java.net.http.internal.frame.SettingsFrame.MAX_CONCURRENT_STREAMS; -import static java.net.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 connections = new ConcurrentHashMap<>(); - - private final Set 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 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; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/Http2Connection.java --- a/src/java.net.http/share/classes/java/net/http/internal/Http2Connection.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1290 +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 java.net.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 java.net.http.HttpClient; -import java.net.http.HttpHeaders; -import java.net.http.internal.HttpConnection.HttpPublisher; -import java.net.http.internal.common.FlowTube; -import java.net.http.internal.common.FlowTube.TubeSubscriber; -import java.net.http.internal.common.HttpHeadersImpl; -import java.net.http.internal.common.Log; -import java.net.http.internal.common.MinimalFuture; -import java.net.http.internal.common.SequentialScheduler; -import java.net.http.internal.common.Utils; -import java.net.http.internal.frame.ContinuationFrame; -import java.net.http.internal.frame.DataFrame; -import java.net.http.internal.frame.ErrorFrame; -import java.net.http.internal.frame.FramesDecoder; -import java.net.http.internal.frame.FramesEncoder; -import java.net.http.internal.frame.GoAwayFrame; -import java.net.http.internal.frame.HeaderFrame; -import java.net.http.internal.frame.HeadersFrame; -import java.net.http.internal.frame.Http2Frame; -import java.net.http.internal.frame.MalformedFrame; -import java.net.http.internal.frame.OutgoingHeaders; -import java.net.http.internal.frame.PingFrame; -import java.net.http.internal.frame.PushPromiseFrame; -import java.net.http.internal.frame.ResetFrame; -import java.net.http.internal.frame.SettingsFrame; -import java.net.http.internal.frame.WindowUpdateFrame; -import java.net.http.internal.hpack.Encoder; -import java.net.http.internal.hpack.Decoder; -import java.net.http.internal.hpack.DecodingCallback; -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.net.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 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 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> 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 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 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 createAsync(HttpConnection connection, - Http2ClientImpl client2, - Exchange exchange, - Supplier initial) - { - return MinimalFuture.supply(() -> new Http2Connection(connection, client2, exchange, initial)); - } - - // Requires TLS handshake. So, is really async - static CompletableFuture 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 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> checkAlpnCF = (alpn) -> { - CompletableFuture 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 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 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> 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 void handlePushPromise(Stream 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 pushExch = new Exchange<>(pushReq, parent.exchange.multi); - Stream.PushedStream 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") - Stream getStream(int streamid) { - return (Stream)streams.get(streamid); - } - - /** - * Creates Stream with given id. - */ - final Stream createStream(Exchange exchange) { - Stream stream = new Stream<>(this, exchange, windowController); - return stream; - } - - Stream.PushedStream createPushStream(Stream parent, Exchange pushEx) { - PushGroup pg = parent.exchange.getPushGroup(); - return new Stream.PushedStream<>(pg, this, pushEx); - } - - void putStream(Stream 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 and then create HEADERS - * and CONTINUATION frames from the list and return the List. - */ - private List encodeHeaders(OutgoingHeaders> frame) { - List buffers = encodeHeadersImpl( - getMaxSendFrameSize(), - frame.getAttachment().getRequestPseudoHeaders(), - frame.getUserHeaders(), - frame.getSystemHeaders()); - - List frames = new ArrayList<>(buffers.size()); - Iterator 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 encodeHeadersImpl(int maxFrameSize, HttpHeaders... headers) { - ByteBuffer buffer = getHeaderBuffer(maxFrameSize); - List buffers = new ArrayList<>(); - for(HttpHeaders header : headers) { - for (Map.Entry> e : header.map().entrySet()) { - String lKey = e.getKey().toLowerCase(); - List 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 encodeHeaders(OutgoingHeaders> 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 frames = encodeHeaders(oh); - return encodeFrames(frames); - } - - private List encodeFrames(List frames) { - if (Log.frames()) { - frames.forEach(f -> Log.logFrames(f, "OUT")); - } - return framesEncoder.encodeFrames(frames); - } - - private Stream registerNewStream(OutgoingHeaders> 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> oh = (OutgoingHeaders>) 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 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 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 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; - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/HttpClientBuilderImpl.java --- a/src/java.net.http/share/classes/java/net/http/internal/HttpClientBuilderImpl.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,127 +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 java.net.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 java.net.http.HttpClient; -import java.net.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); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/HttpClientFacade.java --- a/src/java.net.http/share/classes/java/net/http/internal/HttpClientFacade.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,148 +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 java.net.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 java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandler; -import java.net.http.HttpResponse.PushPromiseHandler; -import java.net.http.WebSocket; - -/** - * 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() { - return impl.cookieHandler(); - } - - @Override - public Redirect followRedirects() { - return impl.followRedirects(); - } - - @Override - public Optional proxy() { - return impl.proxy(); - } - - @Override - public SSLContext sslContext() { - return impl.sslContext(); - } - - @Override - public SSLParameters sslParameters() { - return impl.sslParameters(); - } - - @Override - public Optional authenticator() { - return impl.authenticator(); - } - - @Override - public HttpClient.Version version() { - return impl.version(); - } - - @Override - public Optional executor() { - return impl.executor(); - } - - @Override - public HttpResponse - send(HttpRequest req, HttpResponse.BodyHandler responseBodyHandler) - throws IOException, InterruptedException - { - try { - return impl.send(req, responseBodyHandler); - } finally { - Reference.reachabilityFence(this); - } - } - - @Override - public CompletableFuture> - sendAsync(HttpRequest req, HttpResponse.BodyHandler responseBodyHandler) { - try { - return impl.sendAsync(req, responseBodyHandler); - } finally { - Reference.reachabilityFence(this); - } - } - - @Override - public CompletableFuture> - sendAsync(HttpRequest req, - BodyHandler responseBodyHandler, - PushPromiseHandler 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(); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/HttpClientImpl.java --- a/src/java.net.http/share/classes/java/net/http/internal/HttpClientImpl.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1023 +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 java.net.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 java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandler; -import java.net.http.HttpResponse.PushPromiseHandler; -import java.net.http.WebSocket; -import java.net.http.internal.common.Log; -import java.net.http.internal.common.Pair; -import java.net.http.internal.common.Utils; -import java.net.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 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 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 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 timeouts; - - /** - * This is a bit tricky: - * 1. an HttpClientFacade has a final HttpClientImpl field. - * 2. an HttpClientImpl has a final WeakReference 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 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 HttpResponse - send(HttpRequest req, BodyHandler 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 CompletableFuture> - sendAsync(HttpRequest userRequest, BodyHandler responseHandler) - { - return sendAsync(userRequest, responseHandler, null); - } - - - @Override - public CompletableFuture> - sendAsync(HttpRequest userRequest, - BodyHandler responseHandler, - PushPromiseHandler 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 mex = new MultiExchange<>(userRequest, - requestImpl, - this, - responseHandler, - pushPromiseHandler, - acc); - CompletableFuture> 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= - 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 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); - if (key != null) { - 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> errorList = new ArrayList<>(); - List 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 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 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 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. - * - *

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 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 - try { - chan.register(selector, interestOps, this); - } catch (CancelledKeyException x) { - abortPending(x); - } - } - } - - /** - * Returns a Stream containing only events that are - * registered with the given {@code interestOps}. - */ - Stream 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 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() { - return Optional.ofNullable(authenticator); - } - - /*package-private*/ final Executor theExecutor() { - return executor; - } - - @Override - public final Optional executor() { - return isDefaultExecutor ? Optional.empty() : Optional.of(executor); - } - - ConnectionPool connectionPool() { - return connections; - } - - @Override - public Redirect followRedirects() { - return followRedirects; - } - - - @Override - public Optional cookieHandler() { - return Optional.ofNullable(cookieHandler); - } - - @Override - public Optional 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 f) { - filters.addFilter(f); - } - - final List 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 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 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 - ); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/HttpConnection.java --- a/src/java.net.http/share/classes/java/net/http/internal/HttpConnection.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,493 +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 java.net.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 java.net.http.HttpClient; -import java.net.http.HttpClient.Version; -import java.net.http.HttpHeaders; -import java.net.http.internal.common.Demand; -import java.net.http.internal.common.FlowTube; -import java.net.http.internal.common.SequentialScheduler; -import java.net.http.internal.common.SequentialScheduler.DeferredCompleter; -import java.net.http.internal.common.Log; -import java.net.http.internal.common.Utils; -import static java.net.http.HttpClient.Version.HTTP_2; - -/** - * Wraps socket channel layer and takes care of SSL also. - * - * Subtypes are: - * PlainHttpConnection: regular direct TCP connection to server - * PlainProxyConnection: plain text proxy connection - * PlainTunnelingConnection: opens plain text (CONNECT) tunnel to server - * AsyncSSLConnection: TLS channel direct to server - * AsyncSSLTunnelConnection: TLS channel via (CONNECT) proxy tunnel - */ -abstract class HttpConnection implements Closeable { - - 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, 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 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 buffers) throws IOException; - void enqueueUnordered(List 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 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: - *

-     *    - 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).
-     * 
- * @param request - * @return - */ - BiPredicate> 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> 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> queue = new ConcurrentLinkedDeque<>(); - volatile Flow.Subscriber> subscriber; - volatile HttpWriteSubscription subscription; - final SequentialScheduler writeScheduler = - new SequentialScheduler(this::flushTask); - @Override - public void subscribe(Flow.Subscriber> 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 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 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 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(); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/HttpRequestBuilderImpl.java --- a/src/java.net.http/share/classes/java/net/http/internal/HttpRequestBuilderImpl.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,229 +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 java.net.http.internal; - -import java.net.URI; -import java.time.Duration; -import java.util.Optional; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpRequest.BodyPublisher; -import java.net.http.internal.common.HttpHeadersImpl; -import java.net.http.internal.common.Utils; -import static java.lang.String.format; -import static java.util.Objects.requireNonNull; -import static java.net.http.internal.common.Utils.isValidName; -import static java.net.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 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 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 "); - 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; } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/HttpRequestImpl.java --- a/src/java.net.http/share/classes/java/net/http/internal/HttpRequestImpl.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,333 +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 java.net.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 java.net.http.HttpClient; -import java.net.http.HttpHeaders; -import java.net.http.HttpRequest; -import java.net.http.internal.common.HttpHeadersImpl; -import java.net.http.internal.websocket.WebSocketRequest; - -import static java.net.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 version; - - private static String userAgent() { - PrivilegedAction 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 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() { - 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 timeout() { - return timeout == null ? Optional.empty() : Optional.of(timeout); - } - - HttpHeaders getUserHeaders() { return userHeaders; } - - HttpHeadersImpl getSystemHeaders() { return systemHeaders; } - - @Override - public Optional 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 pa = () -> new InetSocketAddress(host, port); - return AccessController.doPrivileged(pa); - } else { - return InetSocketAddress.createUnresolved(host, port); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/HttpResponseImpl.java --- a/src/java.net.http/share/classes/java/net/http/internal/HttpResponseImpl.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,177 +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 java.net.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 java.net.http.HttpClient; -import java.net.http.HttpHeaders; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.internal.websocket.RawChannel; - -/** - * The implementation class for HttpResponse - */ -class HttpResponseImpl extends HttpResponse implements RawChannel.Provider { - - final int responseCode; - final Exchange exchange; - final HttpRequest initialRequest; - final Optional> previousResponse; - final HttpHeaders headers; - final SSLParameters sslParameters; - final URI uri; - final HttpClient.Version version; - RawChannel rawchan; - final HttpConnection connection; - final Stream stream; - final T body; - - public HttpResponseImpl(HttpRequest initialRequest, - Response response, - HttpResponse previousResponse, - T body, - Exchange 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> 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 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(); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/ImmutableHeaders.java --- a/src/java.net.http/share/classes/java/net/http/internal/ImmutableHeaders.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,94 +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 java.net.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 java.net.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> map; - - public static ImmutableHeaders empty() { - return of(emptyMap()); - } - - public static ImmutableHeaders of(Map> 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> src, - Predicate keyAllowed) { - requireNonNull(src, "src"); - requireNonNull(keyAllowed, "keyAllowed"); - return new ImmutableHeaders(src, headerAllowed(keyAllowed)); - } - - public static ImmutableHeaders of(Map> src, - BiPredicate> headerAllowed) { - requireNonNull(src, "src"); - requireNonNull(headerAllowed, "headerAllowed"); - return new ImmutableHeaders(src, headerAllowed); - } - - private ImmutableHeaders(Map> src, - BiPredicate> headerAllowed) { - Map> m = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - src.entrySet().stream() - .filter(e -> headerAllowed.test(e.getKey(), e.getValue())) - .forEach(e -> - { - List values = new ArrayList<>(e.getValue()); - m.put(e.getKey(), unmodifiableList(values)); - } - ); - this.map = unmodifiableMap(m); - } - - private static BiPredicate> headerAllowed(Predicate keyAllowed) { - return (n,v) -> keyAllowed.test(n); - } - - @Override - public Map> map() { - return map; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/LineSubscriberAdapter.java --- a/src/java.net.http/share/classes/java/net/http/internal/LineSubscriberAdapter.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,467 +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 java.net.http.internal; - -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharacterCodingException; -import java.nio.charset.Charset; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CoderResult; -import java.nio.charset.CodingErrorAction; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.Flow; -import java.util.concurrent.Flow.Subscriber; -import java.util.concurrent.Flow.Subscription; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; -import java.net.http.internal.common.Demand; -import java.net.http.HttpResponse.BodySubscriber; -import java.net.http.internal.common.MinimalFuture; -import java.net.http.internal.common.SequentialScheduler; - -/** An adapter between {@code BodySubscriber} and {@code Flow.Subscriber}. */ -public final class LineSubscriberAdapter,R> - implements BodySubscriber { - private final CompletableFuture cf = new MinimalFuture<>(); - private final S subscriber; - private final Function finisher; - private final Charset charset; - private final String eol; - private volatile LineSubscription downstream; - - private LineSubscriberAdapter(S subscriber, - Function finisher, - Charset charset, - String eol) { - if (eol != null && eol.isEmpty()) - throw new IllegalArgumentException("empty line separator"); - this.subscriber = Objects.requireNonNull(subscriber); - this.finisher = Objects.requireNonNull(finisher); - this.charset = Objects.requireNonNull(charset); - this.eol = eol; - } - - @Override - public void onSubscribe(Subscription subscription) { - downstream = LineSubscription.create(subscription, - charset, - eol, - subscriber, - cf); - subscriber.onSubscribe(downstream); - } - - @Override - public void onNext(List item) { - try { - downstream.submit(item); - } catch (Throwable t) { - onError(t); - } - } - - @Override - public void onError(Throwable throwable) { - try { - downstream.signalError(throwable); - } finally { - cf.completeExceptionally(throwable); - } - } - - @Override - public void onComplete() { - try { - downstream.signalComplete(); - } finally { - cf.complete(finisher.apply(subscriber)); - } - } - - @Override - public CompletionStage getBody() { - return cf; - } - - public static , R> LineSubscriberAdapter - create(S subscriber, Function finisher, Charset charset, String eol) - { - if (eol != null && eol.isEmpty()) - throw new IllegalArgumentException("empty line separator"); - return new LineSubscriberAdapter<>(Objects.requireNonNull(subscriber), - Objects.requireNonNull(finisher), - Objects.requireNonNull(charset), - eol); - } - - static final class LineSubscription implements Flow.Subscription { - final Flow.Subscription upstreamSubscription; - final CharsetDecoder decoder; - final String newline; - final Demand downstreamDemand; - final ConcurrentLinkedDeque queue; - final SequentialScheduler scheduler; - final Flow.Subscriber upstream; - final CompletableFuture cf; - private final AtomicReference errorRef = new AtomicReference<>(); - private final AtomicLong demanded = new AtomicLong(); - private volatile boolean completed; - private volatile boolean cancelled; - - private final char[] chars = new char[1024]; - private final ByteBuffer leftover = ByteBuffer.wrap(new byte[64]); - private final CharBuffer buffer = CharBuffer.wrap(chars); - private final StringBuilder builder = new StringBuilder(); - private int lineCount; - private String nextLine; - - private LineSubscription(Flow.Subscription s, - CharsetDecoder dec, - String separator, - Flow.Subscriber subscriber, - CompletableFuture completion) { - downstreamDemand = new Demand(); - queue = new ConcurrentLinkedDeque<>(); - upstreamSubscription = Objects.requireNonNull(s); - decoder = Objects.requireNonNull(dec); - newline = separator; - upstream = Objects.requireNonNull(subscriber); - cf = Objects.requireNonNull(completion); - scheduler = SequentialScheduler.synchronizedScheduler(this::loop); - } - - @Override - public void request(long n) { - if (cancelled) return; - if (downstreamDemand.increase(n)) { - scheduler.runOrSchedule(); - } - } - - @Override - public void cancel() { - cancelled = true; - upstreamSubscription.cancel(); - } - - public void submit(List list) { - queue.addAll(list); - demanded.decrementAndGet(); - scheduler.runOrSchedule(); - } - - public void signalComplete() { - completed = true; - scheduler.runOrSchedule(); - } - - public void signalError(Throwable error) { - if (errorRef.compareAndSet(null, - Objects.requireNonNull(error))) { - scheduler.runOrSchedule(); - } - } - - // This method looks at whether some bytes where left over (in leftover) - // from decoding the previous buffer when the previous buffer was in - // underflow. If so, it takes bytes one by one from the new buffer 'in' - // and combines them with the leftover bytes until 'in' is exhausted or a - // character was produced in 'out', resolving the previous underflow. - // Returns true if the buffer is still in underflow, false otherwise. - // However, in both situation some chars might have been produced in 'out'. - private boolean isUnderFlow(ByteBuffer in, CharBuffer out, boolean endOfInput) - throws CharacterCodingException { - int limit = leftover.position(); - if (limit == 0) { - // no leftover - return false; - } else { - CoderResult res = null; - while (in.hasRemaining()) { - leftover.position(limit); - leftover.limit(++limit); - leftover.put(in.get()); - leftover.position(0); - res = decoder.decode(leftover, out, - endOfInput && !in.hasRemaining()); - int remaining = leftover.remaining(); - if (remaining > 0) { - assert leftover.position() == 0; - leftover.position(remaining); - } else { - leftover.position(0); - } - leftover.limit(leftover.capacity()); - if (res.isUnderflow() && remaining > 0 && in.hasRemaining()) { - continue; - } - if (res.isError()) { - res.throwException(); - } - assert !res.isOverflow(); - return false; - } - return !endOfInput; - } - } - - // extract characters from start to end and remove them from - // the StringBuilder - private static String take(StringBuilder b, int start, int end) { - assert start == 0; - String line; - if (end == start) return ""; - line = b.substring(start, end); - b.delete(start, end); - return line; - } - - // finds end of line, returns -1 if not found, or the position after - // the line delimiter if found, removing the delimiter in the process. - private static int endOfLine(StringBuilder b, String eol, boolean endOfInput) { - int len = b.length(); - if (eol != null) { // delimiter explicitly specified - int i = b.indexOf(eol); - if (i >= 0) { - // remove the delimiter and returns the position - // of the char after it. - b.delete(i, i + eol.length()); - return i; - } - } else { // no delimiter specified, behaves as BufferedReader::readLine - boolean crfound = false; - for (int i = 0; i < len; i++) { - char c = b.charAt(i); - if (c == '\n') { - // '\n' or '\r\n' found. - // remove the delimiter and returns the position - // of the char after it. - b.delete(crfound ? i - 1 : i, i + 1); - return crfound ? i - 1 : i; - } else if (crfound) { - // previous char was '\r', c != '\n' - assert i != 0; - // remove the delimiter and returns the position - // of the char after it. - b.delete(i - 1, i); - return i - 1; - } - crfound = c == '\r'; - } - if (crfound && endOfInput) { - // remove the delimiter and returns the position - // of the char after it. - b.delete(len - 1, len); - return len - 1; - } - } - return endOfInput && len > 0 ? len : -1; - } - - // Looks at whether the StringBuilder contains a line. - // Returns null if more character are needed. - private static String nextLine(StringBuilder b, String eol, boolean endOfInput) { - int next = endOfLine(b, eol, endOfInput); - return (next > -1) ? take(b, 0, next) : null; - } - - // Attempts to read the next line. Returns the next line if - // the delimiter was found, null otherwise. The delimiters are - // consumed. - private String nextLine() - throws CharacterCodingException { - assert nextLine == null; - LINES: - while (nextLine == null) { - boolean endOfInput = completed && queue.isEmpty(); - nextLine = nextLine(builder, newline, - endOfInput && leftover.position() == 0); - if (nextLine != null) return nextLine; - ByteBuffer b; - BUFFERS: - while ((b = queue.peek()) != null) { - if (!b.hasRemaining()) { - queue.poll(); - continue BUFFERS; - } - BYTES: - while (b.hasRemaining()) { - buffer.position(0); - buffer.limit(buffer.capacity()); - boolean endofInput = completed && queue.size() <= 1; - if (isUnderFlow(b, buffer, endofInput)) { - assert !b.hasRemaining(); - if (buffer.position() > 0) { - buffer.flip(); - builder.append(buffer); - } - continue BUFFERS; - } - CoderResult res = decoder.decode(b, buffer, endofInput); - if (res.isError()) res.throwException(); - if (buffer.position() > 0) { - buffer.flip(); - builder.append(buffer); - continue LINES; - } - if (res.isUnderflow() && b.hasRemaining()) { - //System.out.println("underflow: adding " + b.remaining() + " bytes"); - leftover.put(b); - assert !b.hasRemaining(); - continue BUFFERS; - } - } - } - - assert queue.isEmpty(); - if (endOfInput) { - // Time to cleanup: there may be some undecoded leftover bytes - // We need to flush them out. - // The decoder has been configured to replace malformed/unmappable - // chars with some replacement, in order to behave like - // InputStreamReader. - leftover.flip(); - buffer.position(0); - buffer.limit(buffer.capacity()); - - // decode() must be called just before flush, even if there - // is nothing to decode. We must do this even if leftover - // has no remaining bytes. - CoderResult res = decoder.decode(leftover, buffer, endOfInput); - if (buffer.position() > 0) { - buffer.flip(); - builder.append(buffer); - } - if (res.isError()) res.throwException(); - - // Now call decoder.flush() - buffer.position(0); - buffer.limit(buffer.capacity()); - res = decoder.flush(buffer); - if (buffer.position() > 0) { - buffer.flip(); - builder.append(buffer); - } - if (res.isError()) res.throwException(); - - // It's possible that we reach here twice - just for the - // purpose of checking that no bytes were left over, so - // we reset leftover/decoder to make the function reentrant. - leftover.position(0); - leftover.limit(leftover.capacity()); - decoder.reset(); - - // if some chars were produced then this call will - // return them. - return nextLine = nextLine(builder, newline, endOfInput); - } - return null; - } - return null; - } - - // The main sequential scheduler loop. - private void loop() { - try { - while (!cancelled) { - Throwable error = errorRef.get(); - if (error != null) { - cancelled = true; - scheduler.stop(); - upstream.onError(error); - cf.completeExceptionally(error); - return; - } - if (nextLine == null) nextLine = nextLine(); - if (nextLine == null) { - if (completed) { - scheduler.stop(); - if (leftover.position() != 0) { - // Underflow: not all bytes could be - // decoded, but no more bytes will be coming. - // This should not happen as we should already - // have got a MalformedInputException, or - // replaced the unmappable chars. - errorRef.compareAndSet(null, - new IllegalStateException( - "premature end of input (" - + leftover.position() - + " undecoded bytes)")); - continue; - } else { - upstream.onComplete(); - } - return; - } else if (demanded.get() == 0 - && !downstreamDemand.isFulfilled()) { - long incr = Math.max(1, downstreamDemand.get()); - demanded.addAndGet(incr); - upstreamSubscription.request(incr); - continue; - } else return; - } - assert nextLine != null; - assert newline != null && !nextLine.endsWith(newline) - || !nextLine.endsWith("\n") || !nextLine.endsWith("\r"); - if (downstreamDemand.tryDecrement()) { - String forward = nextLine; - nextLine = null; - upstream.onNext(forward); - } else return; // no demand: come back later - } - } catch (Throwable t) { - try { - upstreamSubscription.cancel(); - } finally { - signalError(t); - } - } - } - - static LineSubscription create(Flow.Subscription s, - Charset charset, - String lineSeparator, - Flow.Subscriber upstream, - CompletableFuture cf) { - return new LineSubscription(Objects.requireNonNull(s), - Objects.requireNonNull(charset).newDecoder() - // use the same decoder configuration than - // java.io.InputStreamReader - .onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE), - lineSeparator, - Objects.requireNonNull(upstream), - Objects.requireNonNull(cf)); - } - } -} - diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/MultiExchange.java --- a/src/java.net.http/share/classes/java/net/http/internal/MultiExchange.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,326 +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 java.net.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 java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.PushPromiseHandler; -import java.net.http.HttpTimeoutException; -import java.net.http.internal.UntrustedBodyHandler; -import java.net.http.internal.common.Log; -import java.net.http.internal.common.MinimalFuture; -import java.net.http.internal.common.ConnectionExpiredException; -import java.net.http.internal.common.Utils; -import static java.net.http.internal.common.MinimalFuture.completedFuture; -import static java.net.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 { - - 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 responseHandler; - final Executor executor; - final AtomicInteger attempts = new AtomicInteger(); - HttpRequestImpl currentreq; // used for async only - Exchange exchange; // the current exchange - Exchange previous; - volatile Throwable retryCause; - volatile boolean expiredOnce; - volatile HttpResponse 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 filters; - TimedEvent timedEvent; - volatile boolean cancelled; - final PushGroup 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 responseHandler, - PushPromiseHandler 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 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 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> responseAsync() { - CompletableFuture start = new MinimalFuture<>(); - CompletableFuture> cf = responseAsync0(start); - start.completeAsync( () -> null, executor); // trigger execution - return cf; - } - - private CompletableFuture> - responseAsync0(CompletableFuture start) { - return start.thenCompose( v -> responseAsyncImpl()) - .thenCompose((Response r) -> { - Exchange exch = getExchange(); - return exch.readBodyAsync(responseHandler) - .thenApply((T body) -> { - this.response = - new HttpResponseImpl<>(userRequest, r, this.response, body, exch); - return this.response; - }); - }); - } - - private CompletableFuture responseAsyncImpl() { - CompletableFuture 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 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 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 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 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")); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/PlainHttpConnection.java --- a/src/java.net.http/share/classes/java/net/http/internal/PlainHttpConnection.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,313 +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 java.net.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 java.net.http.internal.common.FlowTube; -import java.net.http.internal.common.Log; -import java.net.http.internal.common.MinimalFuture; -import java.net.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 cf; - - ConnectEvent(CompletableFuture 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 connectAsync() { - CompletableFuture cf = new MinimalFuture<>(); - try { - assert !connected : "Already connected"; - assert !chan.isBlocking() : "Unexpected blocking channel"; - boolean finished = false; - PrivilegedExceptionAction 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); - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/PlainProxyConnection.java --- a/src/java.net.http/share/classes/java/net/http/internal/PlainProxyConnection.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,40 +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 java.net.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); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/PlainTunnelingConnection.java --- a/src/java.net.http/share/classes/java/net/http/internal/PlainTunnelingConnection.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,166 +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 java.net.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 java.net.http.HttpHeaders; -import java.net.http.internal.common.FlowTube; -import java.net.http.internal.common.MinimalFuture; -import static java.net.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 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 mulEx = new MultiExchange<>(null, req, - client, discard(), null, null); - Exchange connectExchange = new Exchange<>(req, mulEx); - - return connectExchange - .responseAsyncImpl(delegate) - .thenCompose((Response resp) -> { - CompletableFuture 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(); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/PrivilegedExecutor.java --- a/src/java.net.http/share/classes/java/net/http/internal/PrivilegedExecutor.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,69 +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 java.net.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 pa = () -> { r.run(); return null; }; - AccessController.doPrivileged(pa, acc); - } - } - - @Override - public void execute(Runnable r) { - executor.execute(new PrivilegedRunnable(r, acc)); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/ProxyAuthenticationRequired.java --- a/src/java.net.http/share/classes/java/net/http/internal/ProxyAuthenticationRequired.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,49 +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 java.net.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; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/PullPublisher.java --- a/src/java.net.http/share/classes/java/net/http/internal/PullPublisher.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,138 +0,0 @@ -/* - * 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 java.net.http.internal; - -import java.util.Iterator; -import java.util.concurrent.Flow; -import java.net.http.internal.common.Demand; -import java.net.http.internal.common.SequentialScheduler; - -/** - * A Publisher that publishes items obtained from the given Iterable. Each new - * subscription gets a new Iterator. - */ -class PullPublisher implements Flow.Publisher { - - // 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 iterable; - private final Throwable throwable; - - PullPublisher(Iterable iterable, Throwable throwable) { - this.iterable = iterable; - this.throwable = throwable; - } - - PullPublisher(Iterable iterable) { - this(iterable, null); - } - - @Override - public void subscribe(Flow.Subscriber 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 subscriber; - private final Iterator 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 subscriber, - Iterator 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; - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/PushGroup.java --- a/src/java.net.http/share/classes/java/net/http/internal/PushGroup.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,164 +0,0 @@ -/* - * 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 java.net.http.internal; - -import java.security.AccessControlContext; -import java.security.AccessController; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandler; -import java.net.http.HttpResponse.PushPromiseHandler; -import java.net.http.internal.common.MinimalFuture; -import java.net.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 { - private final HttpRequest initiatingRequest; - - final CompletableFuture noMorePushesCF; - - volatile Throwable error; // any exception that occurred during pushes - - // user's subscriber object - final PushPromiseHandler pushPromiseHandler; - - private final AccessControlContext acc; - - int numberOfPushes; - int remainingPushes; - boolean noMorePushes = false; - - PushGroup(PushPromiseHandler pushPromiseHandler, - HttpRequestImpl initiatingRequest, - AccessControlContext acc) { - this(pushPromiseHandler, initiatingRequest, new MinimalFuture<>(), acc); - } - - // Check mainBodyHandler before calling nested constructor. - private PushGroup(HttpResponse.PushPromiseHandler pushPromiseHandler, - HttpRequestImpl initiatingRequest, - CompletableFuture> 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 { - BodyHandler bodyHandler(); - CompletableFuture> cf(); - boolean accepted(); - } - - private static class AcceptorImpl implements Acceptor { - private volatile HttpResponse.BodyHandler bodyHandler; - private volatile CompletableFuture> cf; - - CompletableFuture> accept(BodyHandler 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 bodyHandler() { return bodyHandler; } - - @Override public CompletableFuture> cf() { return cf; } - - @Override public boolean accepted() { return cf != null; } - } - - Acceptor acceptPushRequest(HttpRequest pushRequest) { - AcceptorImpl 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 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; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/RawChannelImpl.java --- a/src/java.net.http/share/classes/java/net/http/internal/RawChannelImpl.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,158 +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 java.net.http.internal; - -import java.net.http.internal.common.Utils; -import java.net.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 initial; - - RawChannelImpl(HttpClientImpl client, - HttpConnection connection, - Supplier 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() + ")"; - } - - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/RedirectFilter.java --- a/src/java.net.http/share/classes/java/net/http/internal/RedirectFilter.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,119 +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 java.net.http.internal; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpHeaders; -import java.net.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(); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/RequestPublishers.java --- a/src/java.net.http/share/classes/java/net/http/internal/RequestPublishers.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,377 +0,0 @@ -/* - * 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 java.net.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 java.net.http.HttpRequest.BodyPublisher; -import java.net.http.internal.common.Utils; - -public final class RequestPublishers { - - private RequestPublishers() { } - - public static class ByteArrayPublisher implements BodyPublisher { - private volatile Flow.Publisher 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 copy(byte[] content, int offset, int length) { - List 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 subscriber) { - List 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 delegate; - private final Iterable content; - private volatile long contentLength; - - public IterablePublisher(Iterable 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 { - final ConcurrentLinkedQueue buffers = new ConcurrentLinkedQueue<>(); - final Iterator 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 iterator() { - return new ByteBufferIterator(); - } - - @Override - public void subscribe(Flow.Subscriber subscriber) { - Iterable iterable = this::iterator; - this.delegate = new PullPublisher<>(iterable); - delegate.subscribe(subscriber); - } - - static long computeLength(Iterable 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 delegate = - new PullPublisher(Collections.emptyList(), null); - - @Override - public long contentLength() { - return 0; - } - - @Override - public void subscribe(Flow.Subscriber 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 subscriber) { - if (System.getSecurityManager() != null && acc == null) - throw new InternalError( - "Unexpected null acc when security manager has been installed"); - - InputStream is; - try { - PrivilegedExceptionAction pa = - () -> new FileInputStream(file); - is = AccessController.doPrivileged(pa, acc); - } catch (PrivilegedActionException pae) { - throw new UncheckedIOException((IOException)pae.getCause()); - } - PullPublisher publisher = - new PullPublisher<>(() -> new StreamIterator(is)); - publisher.subscribe(subscriber); - } - - @Override - public long contentLength() { - assert System.getSecurityManager() != null ? acc != null: true; - PrivilegedAction pa = () -> file.length(); - return AccessController.doPrivileged(pa, acc); - } - } - - /** - * Reads one buffer ahead all the time, blocking in hasNext() - */ - public static class StreamIterator implements Iterator { - final InputStream is; - final Supplier bufSupplier; - volatile ByteBuffer nextBuffer; - volatile boolean need2Read = true; - volatile boolean haveNext; - - StreamIterator(InputStream is) { - this(is, Utils::getBuffer); - } - - StreamIterator(InputStream is, Supplier 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 streamSupplier; - - public InputStreamPublisher(Supplier streamSupplier) { - this.streamSupplier = Objects.requireNonNull(streamSupplier); - } - - @Override - public void subscribe(Flow.Subscriber subscriber) { - PullPublisher 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 iterableOf(InputStream is) { - return () -> new StreamIterator(is); - } - - @Override - public long contentLength() { - return -1; - } - } - - public static final class PublisherAdapter implements BodyPublisher { - - private final Publisher publisher; - private final long contentLength; - - public PublisherAdapter(Publisher 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 subscriber) { - publisher.subscribe(subscriber); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/Response.java --- a/src/java.net.http/share/classes/java/net/http/internal/Response.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -/* - * 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 java.net.http.internal; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.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(); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/ResponseBodyHandlers.java --- a/src/java.net.http/share/classes/java/net/http/internal/ResponseBodyHandlers.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,170 +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 java.net.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 java.net.http.HttpHeaders; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandler; -import java.net.http.HttpResponse.BodySubscriber; -import java.net.http.internal.ResponseSubscribers.PathSubscriber; -import static java.net.http.internal.common.Utils.unchecked; - -public final class ResponseBodyHandlers { - - private 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 { - 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 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 - implements HttpResponse.PushPromiseHandler - { - private final ConcurrentMap>> pushPromisesMap; - private final Function> pushPromiseHandler; - - public PushPromisesHandlerWithMap(Function> pushPromiseHandler, - ConcurrentMap>> pushPromisesMap) { - this.pushPromiseHandler = pushPromiseHandler; - this.pushPromisesMap = pushPromisesMap; - } - - @Override - public void applyPushPromise( - HttpRequest initiatingRequest, HttpRequest pushRequest, - Function,CompletableFuture>> 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> 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 { - 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 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 asFileImpl(Path file, OpenOption... openOptions) { - return new ResponseSubscribers.PathSubscriber(file, openOptions); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/ResponseContent.java --- a/src/java.net.http/share/classes/java/net/http/internal/ResponseContent.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,467 +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 java.net.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 java.net.http.HttpHeaders; -import java.net.http.HttpResponse; -import java.net.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 { - 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 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 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 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 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 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 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 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); - } - } - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/ResponseSubscribers.java --- a/src/java.net.http/share/classes/java/net/http/internal/ResponseSubscribers.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,650 +0,0 @@ -/* - * 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 java.net.http.internal; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.lang.System.Logger.Level; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.charset.Charset; -import java.nio.file.OpenOption; -import java.nio.file.Path; -import java.security.AccessControlContext; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executor; -import java.util.concurrent.Flow; -import java.util.concurrent.Flow.Subscriber; -import java.util.concurrent.Flow.Subscription; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Stream; -import java.net.http.HttpResponse.BodySubscriber; -import java.net.http.internal.common.MinimalFuture; -import java.net.http.internal.common.Utils; -import static java.nio.charset.StandardCharsets.UTF_8; - -public class ResponseSubscribers { - - public static class ConsumerSubscriber implements BodySubscriber { - private final Consumer> consumer; - private Flow.Subscription subscription; - private final CompletableFuture result = new MinimalFuture<>(); - private final AtomicBoolean subscribed = new AtomicBoolean(); - - public ConsumerSubscriber(Consumer> consumer) { - this.consumer = Objects.requireNonNull(consumer); - } - - @Override - public CompletionStage getBody() { - return result; - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - if (!subscribed.compareAndSet(false, true)) { - subscription.cancel(); - } else { - this.subscription = subscription; - subscription.request(1); - } - } - - @Override - public void onNext(List items) { - for (ByteBuffer item : items) { - byte[] buf = new byte[item.remaining()]; - item.get(buf); - consumer.accept(Optional.of(buf)); - } - subscription.request(1); - } - - @Override - public void onError(Throwable throwable) { - result.completeExceptionally(throwable); - } - - @Override - public void onComplete() { - consumer.accept(Optional.empty()); - result.complete(null); - } - - } - - public static class PathSubscriber implements BodySubscriber { - - private final Path file; - private final CompletableFuture result = new MinimalFuture<>(); - - private volatile Flow.Subscription subscription; - private volatile FileChannel out; - private volatile AccessControlContext acc; - private final OpenOption[] options; - - public PathSubscriber(Path file, OpenOption... options) { - this.file = file; - this.options = options; - } - - public void setAccessControlContext(AccessControlContext acc) { - this.acc = acc; - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - if (System.getSecurityManager() != null && acc == null) - throw new InternalError( - "Unexpected null acc when security manager has been installed"); - - this.subscription = subscription; - try { - PrivilegedExceptionAction pa = - () -> FileChannel.open(file, options); - out = AccessController.doPrivileged(pa, acc); - } catch (PrivilegedActionException pae) { - Throwable t = pae.getCause() != null ? pae.getCause() : pae; - result.completeExceptionally(t); - subscription.cancel(); - return; - } - subscription.request(1); - } - - @Override - public void onNext(List items) { - try { - out.write(items.toArray(Utils.EMPTY_BB_ARRAY)); - } catch (IOException ex) { - Utils.close(out); - subscription.cancel(); - result.completeExceptionally(ex); - } - subscription.request(1); - } - - @Override - public void onError(Throwable e) { - result.completeExceptionally(e); - Utils.close(out); - } - - @Override - public void onComplete() { - Utils.close(out); - result.complete(file); - } - - @Override - public CompletionStage getBody() { - return result; - } - } - - public static class ByteArraySubscriber implements BodySubscriber { - private final Function finisher; - private final CompletableFuture result = new MinimalFuture<>(); - private final List received = new ArrayList<>(); - - private volatile Flow.Subscription subscription; - - public ByteArraySubscriber(Function finisher) { - this.finisher = finisher; - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - if (this.subscription != null) { - subscription.cancel(); - return; - } - this.subscription = subscription; - // We can handle whatever you've got - subscription.request(Long.MAX_VALUE); - } - - @Override - public void onNext(List items) { - // incoming buffers are allocated by http client internally, - // and won't be used anywhere except this place. - // So it's free simply to store them for further processing. - assert Utils.hasRemaining(items); - received.addAll(items); - } - - @Override - public void onError(Throwable throwable) { - received.clear(); - result.completeExceptionally(throwable); - } - - static private byte[] join(List bytes) { - int size = Utils.remaining(bytes, Integer.MAX_VALUE); - byte[] res = new byte[size]; - int from = 0; - for (ByteBuffer b : bytes) { - int l = b.remaining(); - b.get(res, from, l); - from += l; - } - return res; - } - - @Override - public void onComplete() { - try { - result.complete(finisher.apply(join(received))); - received.clear(); - } catch (IllegalArgumentException e) { - result.completeExceptionally(e); - } - } - - @Override - public CompletionStage getBody() { - return result; - } - } - - /** - * An InputStream built on top of the Flow API. - */ - public static class HttpResponseInputStream extends InputStream - implements BodySubscriber - { - final static boolean DEBUG = Utils.DEBUG; - final static int MAX_BUFFERS_IN_QUEUE = 1; // lock-step with the producer - - // An immutable ByteBuffer sentinel to mark that the last byte was received. - private static final ByteBuffer LAST_BUFFER = ByteBuffer.wrap(new byte[0]); - private static final List LAST_LIST = List.of(LAST_BUFFER); - private static final System.Logger DEBUG_LOGGER = - Utils.getDebugLogger("HttpResponseInputStream"::toString, DEBUG); - - // A queue of yet unprocessed ByteBuffers received from the flow API. - private final BlockingQueue> buffers; - private volatile Flow.Subscription subscription; - private volatile boolean closed; - private volatile Throwable failed; - private volatile Iterator currentListItr; - private volatile ByteBuffer currentBuffer; - private final AtomicBoolean subscribed = new AtomicBoolean(); - - public HttpResponseInputStream() { - this(MAX_BUFFERS_IN_QUEUE); - } - - HttpResponseInputStream(int maxBuffers) { - int capacity = (maxBuffers <= 0 ? MAX_BUFFERS_IN_QUEUE : maxBuffers); - // 1 additional slot needed for LAST_LIST added by onComplete - this.buffers = new ArrayBlockingQueue<>(capacity + 1); - } - - @Override - public CompletionStage getBody() { - // Returns the stream immediately, before the - // response body is received. - // This makes it possible for sendAsync().get().body() - // to complete before the response body is received. - return CompletableFuture.completedStage(this); - } - - // Returns the current byte buffer to read from. - // If the current buffer has no remaining data, this method will take the - // next buffer from the buffers queue, possibly blocking until - // a new buffer is made available through the Flow API, or the - // end of the flow has been reached. - private ByteBuffer current() throws IOException { - while (currentBuffer == null || !currentBuffer.hasRemaining()) { - // Check whether the stream is closed or exhausted - if (closed || failed != null) { - throw new IOException("closed", failed); - } - if (currentBuffer == LAST_BUFFER) break; - - try { - if (currentListItr == null || !currentListItr.hasNext()) { - // Take a new list of buffers from the queue, blocking - // if none is available yet... - - DEBUG_LOGGER.log(Level.DEBUG, "Taking list of Buffers"); - List lb = buffers.take(); - currentListItr = lb.iterator(); - DEBUG_LOGGER.log(Level.DEBUG, "List of Buffers Taken"); - - // Check whether an exception was encountered upstream - if (closed || failed != null) - throw new IOException("closed", failed); - - // Check whether we're done. - if (lb == LAST_LIST) { - currentListItr = null; - currentBuffer = LAST_BUFFER; - break; - } - - // Request another upstream item ( list of buffers ) - Flow.Subscription s = subscription; - if (s != null) { - DEBUG_LOGGER.log(Level.DEBUG, "Increased demand by 1"); - s.request(1); - } - assert currentListItr != null; - if (lb.isEmpty()) continue; - } - assert currentListItr != null; - assert currentListItr.hasNext(); - DEBUG_LOGGER.log(Level.DEBUG, "Next Buffer"); - currentBuffer = currentListItr.next(); - } catch (InterruptedException ex) { - // continue - } - } - assert currentBuffer == LAST_BUFFER || currentBuffer.hasRemaining(); - return currentBuffer; - } - - @Override - public int read(byte[] bytes, int off, int len) throws IOException { - // get the buffer to read from, possibly blocking if - // none is available - ByteBuffer buffer; - if ((buffer = current()) == LAST_BUFFER) return -1; - - // don't attempt to read more than what is available - // in the current buffer. - int read = Math.min(buffer.remaining(), len); - assert read > 0 && read <= buffer.remaining(); - - // buffer.get() will do the boundary check for us. - buffer.get(bytes, off, read); - return read; - } - - @Override - public int read() throws IOException { - ByteBuffer buffer; - if ((buffer = current()) == LAST_BUFFER) return -1; - return buffer.get() & 0xFF; - } - - @Override - public void onSubscribe(Flow.Subscription s) { - try { - if (!subscribed.compareAndSet(false, true)) { - s.cancel(); - } else { - // check whether the stream is already closed. - // if so, we should cancel the subscription - // immediately. - boolean closed; - synchronized (this) { - closed = this.closed; - if (!closed) { - this.subscription = s; - } - } - if (closed) { - s.cancel(); - return; - } - assert buffers.remainingCapacity() > 1; // should contain at least 2 - DEBUG_LOGGER.log(Level.DEBUG, () -> "onSubscribe: requesting " - + Math.max(1, buffers.remainingCapacity() - 1)); - s.request(Math.max(1, buffers.remainingCapacity() - 1)); - } - } catch (Throwable t) { - failed = t; - try { - close(); - } catch (IOException x) { - // OK - } finally { - onError(t); - } - } - } - - @Override - public void onNext(List t) { - Objects.requireNonNull(t); - try { - DEBUG_LOGGER.log(Level.DEBUG, "next item received"); - if (!buffers.offer(t)) { - throw new IllegalStateException("queue is full"); - } - DEBUG_LOGGER.log(Level.DEBUG, "item offered"); - } catch (Throwable ex) { - failed = ex; - try { - close(); - } catch (IOException ex1) { - // OK - } finally { - onError(ex); - } - } - } - - @Override - public void onError(Throwable thrwbl) { - subscription = null; - failed = Objects.requireNonNull(thrwbl); - // The client process that reads the input stream might - // be blocked in queue.take(). - // Tries to offer LAST_LIST to the queue. If the queue is - // full we don't care if we can't insert this buffer, as - // the client can't be blocked in queue.take() in that case. - // Adding LAST_LIST to the queue is harmless, as the client - // should find failed != null before handling LAST_LIST. - buffers.offer(LAST_LIST); - } - - @Override - public void onComplete() { - subscription = null; - onNext(LAST_LIST); - } - - @Override - public void close() throws IOException { - Flow.Subscription s; - synchronized (this) { - if (closed) return; - closed = true; - s = subscription; - subscription = null; - } - // s will be null if already completed - try { - if (s != null) { - s.cancel(); - } - } finally { - buffers.offer(LAST_LIST); - super.close(); - } - } - - } - - public static BodySubscriber> createLineStream() { - return createLineStream(UTF_8); - } - - public static BodySubscriber> createLineStream(Charset charset) { - Objects.requireNonNull(charset); - BodySubscriber s = new HttpResponseInputStream(); - return new MappedSubscriber>(s, - (InputStream stream) -> { - return new BufferedReader(new InputStreamReader(stream, charset)) - .lines().onClose(() -> Utils.close(stream)); - }); - } - - /** - * Currently this consumes all of the data and ignores it - */ - public static class NullSubscriber implements BodySubscriber { - - private final CompletableFuture cf = new MinimalFuture<>(); - private final Optional result; - private final AtomicBoolean subscribed = new AtomicBoolean(); - - public NullSubscriber(Optional result) { - this.result = result; - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - if (!subscribed.compareAndSet(false, true)) { - subscription.cancel(); - } else { - subscription.request(Long.MAX_VALUE); - } - } - - @Override - public void onNext(List items) { - Objects.requireNonNull(items); - } - - @Override - public void onError(Throwable throwable) { - cf.completeExceptionally(throwable); - } - - @Override - public void onComplete() { - if (result.isPresent()) { - cf.complete(result.get()); - } else { - cf.complete(null); - } - } - - @Override - public CompletionStage getBody() { - return cf; - } - } - - /** An adapter between {@code BodySubscriber} and {@code Flow.Subscriber}. */ - public static final class SubscriberAdapter>,R> - implements BodySubscriber - { - private final CompletableFuture cf = new MinimalFuture<>(); - private final S subscriber; - private final Function finisher; - private volatile Subscription subscription; - - public SubscriberAdapter(S subscriber, Function finisher) { - this.subscriber = Objects.requireNonNull(subscriber); - this.finisher = Objects.requireNonNull(finisher); - } - - @Override - public void onSubscribe(Subscription subscription) { - Objects.requireNonNull(subscription); - if (this.subscription != null) { - subscription.cancel(); - } else { - this.subscription = subscription; - subscriber.onSubscribe(subscription); - } - } - - @Override - public void onNext(List item) { - Objects.requireNonNull(item); - try { - subscriber.onNext(item); - } catch (Throwable throwable) { - subscription.cancel(); - onError(throwable); - } - } - - @Override - public void onError(Throwable throwable) { - Objects.requireNonNull(throwable); - try { - subscriber.onError(throwable); - } finally { - cf.completeExceptionally(throwable); - } - } - - @Override - public void onComplete() { - try { - subscriber.onComplete(); - } finally { - try { - cf.complete(finisher.apply(subscriber)); - } catch (Throwable throwable) { - cf.completeExceptionally(throwable); - } - } - } - - @Override - public CompletionStage getBody() { - return cf; - } - } - - /** - * A body subscriber which receives input from an upstream subscriber - * and maps that subscriber's body type to a new type. The upstream subscriber - * delegates all flow operations directly to this object. The - * {@link CompletionStage} returned by {@link #getBody()}} takes the output - * of the upstream {@code getBody()} and applies the mapper function to - * obtain the new {@code CompletionStage} type. - * - * Uses an Executor that must be set externally. - * - * @param the upstream body type - * @param this subscriber's body type - */ - public static class MappedSubscriber implements BodySubscriber { - final BodySubscriber upstream; - final Function mapper; - - /** - * - * @param upstream - * @param mapper - */ - public MappedSubscriber(BodySubscriber upstream, Function mapper) { - this.upstream = upstream; - this.mapper = mapper; - } - - @Override - public CompletionStage getBody() { - return upstream.getBody() - .thenApply(mapper); - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - upstream.onSubscribe(subscription); - } - - @Override - public void onNext(List item) { - upstream.onNext(item); - } - - @Override - public void onError(Throwable throwable) { - upstream.onError(throwable); - } - - @Override - public void onComplete() { - upstream.onComplete(); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/SSLDelegate.java --- a/src/java.net.http/share/classes/java/net/http/internal/SSLDelegate.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,489 +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 java.net.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 java.net.http.internal.common.Log; -import java.net.http.internal.common.Utils; -import static javax.net.ssl.SSLEngineResult.HandshakeStatus.*; - -/** - * Implements the mechanics of SSL by managing an SSLEngine object. - *

- * 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 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(); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/SocketTube.java --- a/src/java.net.http/share/classes/java/net/http/internal/SocketTube.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,956 +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 java.net.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 java.net.http.internal.common.Demand; -import java.net.http.internal.common.FlowTube; -import java.net.http.internal.common.SequentialScheduler; -import java.net.http.internal.common.SequentialScheduler.DeferredCompleter; -import java.net.http.internal.common.SequentialScheduler.RestartableTask; -import java.net.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 buffersSource; - private final Object lock = new Object(); - private final AtomicReference errorRef = new AtomicReference<>(); - private final InternalReadPublisher readPublisher; - private final InternalWriteSubscriber writeSubscriber; - private final long id = IDS.incrementAndGet(); - - public SocketTube(HttpClientImpl client, SocketChannel channel, - Supplier 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> 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 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> { - - volatile Flow.Subscription subscription; - volatile List 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 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 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 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> { - private final InternalReadSubscription subscriptionImpl - = new InternalReadSubscription(); - AtomicReference pendingSubscription = new AtomicReference<>(); - private volatile ReadSubscription subscription; - - @Override - public void subscribe(Flow.Subscriber> 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 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 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 EOF = List.of(); - - private List readAvailable() throws IOException { - ByteBuffer buf = buffersSource.get(); - assert buf.hasRemaining(); - - int read; - int pos = buf.position(); - List 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 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 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 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+")"; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/Stream.java --- a/src/java.net.http/share/classes/java/net/http/internal/Stream.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1180 +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 java.net.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 java.net.http.HttpClient; -import java.net.http.HttpHeaders; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodySubscriber; -import java.net.http.internal.common.*; -import java.net.http.internal.frame.*; -import java.net.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 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 extends ExchangeImpl { - - final static boolean DEBUG = Utils.DEBUG; // Revisit: temporary developer's flag - final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG); - - final ConcurrentLinkedQueue 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 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 requestBodyCF = new MinimalFuture<>(); - volatile CompletableFuture 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 buffers = df.getData(); - List 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 readBodyAsync(HttpResponse.BodyHandler handler, - boolean returnConnectionToPool, - Executor executor) - { - Log.logTrace("Reading body on stream {0}", streamid); - BodySubscriber bodySubscriber = handler.apply(responseCode, responseHeaders); - CompletableFuture cf = receiveData(bodySubscriber, executor); - - 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 receiveData(BodySubscriber bodySubscriber, Executor executor) { - responseBodyCF = new MinimalFuture<>(); - // We want to allow the subscriber's getBody() method to block so it - // can work with InputStreams. So, we offload execution. - executor.execute(() -> { - bodySubscriber.getBody().whenComplete((T body, Throwable t) -> { - if (t == null) - responseBodyCF.complete(body); - else - responseBodyCF.completeExceptionally(t); - }); - }); - - 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> sendBodyAsync() { - return sendBodyImpl().thenApply( v -> this); - } - - @SuppressWarnings("unchecked") - Stream(Http2Connection connection, - Exchange 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 pushStream) - throws IOException - { - if (Log.requests()) { - Log.logRequest("PUSH_PROMISE: " + pushRequest.toString()); - } - PushGroup 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 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> pushResponseCF = acceptor.cf(); - HttpResponse.BodyHandler 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> cf = pushStream.responseCF(); - cf.whenComplete((HttpResponse 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> 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> 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> 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> 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> 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> f = headerFrame(requestContentLen); - connection.sendFrame(f); - CompletableFuture> 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 { - // 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 outgoing = new ConcurrentLinkedDeque<>(); - - private final AtomicReference 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 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> response_cfs = new ArrayList<>(5); - - @Override - CompletableFuture getResponseAsync(Executor executor) { - CompletableFuture 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 cf; - int cfs_len = response_cfs.size(); - for (int i=0; i cf = response_cfs.get(i); - if (!cf.isDone()) { - cf.completeExceptionally(t); - response_cfs.remove(i); - return; - } - } - response_cfs.add(MinimalFuture.failedFuture(t)); - } - } - - CompletableFuture 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 extends Stream { - final PushGroup 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 pushCF; - CompletableFuture> responseCF; - final HttpRequestImpl pushReq; - HttpResponse.BodyHandler pushHandler; - - PushedStream(PushGroup pushGroup, - Http2Connection connection, - Exchange 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> responseCF() { - return responseCF; - } - - synchronized void setPushHandler(HttpResponse.BodyHandler pushHandler) { - this.pushHandler = pushHandler; - } - - synchronized HttpResponse.BodyHandler 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> sendBodyAsync() { - return super.sendBodyAsync() - .whenComplete((ExchangeImpl v, Throwable t) - -> pushGroup.pushError(Utils.getCompletionCause(t))); - } - - @Override - CompletableFuture> sendHeadersAsync() { - return super.sendHeadersAsync() - .whenComplete((ExchangeImpl ex, Throwable t) - -> pushGroup.pushError(Utils.getCompletionCause(t))); - } - - @Override - CompletableFuture getResponseAsync(Executor executor) { - CompletableFuture 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 readBodyAsync( - HttpResponse.BodyHandler 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 start = new MinimalFuture<>(); - start.thenCompose( v -> readBodyAsync(getPushHandler(), false, getExchange().executor())) - .whenComplete((T body, Throwable t) -> { - if (t != null) { - responseCF.completeExceptionally(t); - } else { - HttpResponseImpl 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+")"; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/TimeoutEvent.java --- a/src/java.net.http/share/classes/java/net/http/internal/TimeoutEvent.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,80 +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 java.net.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 { - - 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 + "]"; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/UntrustedBodyHandler.java --- a/src/java.net.http/share/classes/java/net/http/internal/UntrustedBodyHandler.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,34 +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 java.net.http.internal; - -import java.security.AccessControlContext; -import java.net.http.HttpResponse; - -/** A body handler that is further restricted by a given ACC. */ -public interface UntrustedBodyHandler extends HttpResponse.BodyHandler { - void setAccessControlContext(AccessControlContext acc); -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/WindowController.java --- a/src/java.net.http/share/classes/java/net/http/internal/WindowController.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,320 +0,0 @@ -/* - * 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 java.net.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 java.net.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 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>> 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> 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,Integer>>> iter = - pending.entrySet().iterator(); - - while (iter.hasNext() && size > 0) { - Map.Entry,Integer>> item = iter.next(); - Integer streamSize = streams.get(item.getKey()); - if (streamSize == null) { - iter.remove(); - } else { - Map.Entry,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,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 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(); -// } -// } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/WindowUpdateSender.java --- a/src/java.net.http/share/classes/java/net/http/internal/WindowUpdateSender.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,91 +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 java.net.http.internal; - -import java.lang.System.Logger.Level; -import java.net.http.internal.frame.SettingsFrame; -import java.net.http.internal.frame.WindowUpdateFrame; -import java.net.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() + ")"; - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/common/ByteBufferPool.java --- a/src/java.net.http/share/classes/java/net/http/internal/common/ByteBufferPool.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,60 +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 java.net.http.internal.common; - -import java.nio.ByteBuffer; -import java.util.concurrent.ConcurrentLinkedQueue; - -/** - * The class provides reuse of ByteBuffers. - * It is supposed that all requested buffers have the same size for a long period of time. - * That is why there is no any logic splitting buffers into different buckets (by size). It's unnecessary. - * - * At the same moment it is allowed to change requested buffers size (all smaller buffers will be discarded). - * It may be needed for example, if after rehandshaking netPacketBufferSize was changed. - */ -public class ByteBufferPool { - - private final java.util.Queue pool = new ConcurrentLinkedQueue<>(); - - public ByteBufferPool() { - } - - public ByteBufferReference get(int size) { - ByteBuffer buffer; - while ((buffer = pool.poll()) != null) { - if (buffer.capacity() >= size) { - return ByteBufferReference.of(buffer, this); - } - } - return ByteBufferReference.of(ByteBuffer.allocate(size), this); - } - - public void release(ByteBuffer buffer) { - buffer.clear(); - pool.offer(buffer); - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/common/ByteBufferReference.java --- a/src/java.net.http/share/classes/java/net/http/internal/common/ByteBufferReference.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,88 +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 java.net.http.internal.common; - -import java.nio.ByteBuffer; -import java.util.Objects; -import java.util.function.Supplier; - -public class ByteBufferReference implements Supplier { - - private ByteBuffer buffer; - private final ByteBufferPool pool; - - public static ByteBufferReference of(ByteBuffer buffer) { - return of(buffer, null); - } - - public static ByteBufferReference of(ByteBuffer buffer, ByteBufferPool pool) { - Objects.requireNonNull(buffer); - return new ByteBufferReference(buffer, pool); - } - - public static ByteBuffer[] toBuffers(ByteBufferReference... refs) { - ByteBuffer[] bufs = new ByteBuffer[refs.length]; - for (int i = 0; i < refs.length; i++) { - bufs[i] = refs[i].get(); - } - return bufs; - } - - public static ByteBufferReference[] toReferences(ByteBuffer... buffers) { - ByteBufferReference[] refs = new ByteBufferReference[buffers.length]; - for (int i = 0; i < buffers.length; i++) { - refs[i] = of(buffers[i]); - } - return refs; - } - - - public static void clear(ByteBufferReference[] refs) { - for(ByteBufferReference ref : refs) { - ref.clear(); - } - } - - private ByteBufferReference(ByteBuffer buffer, ByteBufferPool pool) { - this.buffer = buffer; - this.pool = pool; - } - - @Override - public ByteBuffer get() { - ByteBuffer buf = this.buffer; - assert buf!=null : "getting ByteBuffer after clearance"; - return buf; - } - - public void clear() { - ByteBuffer buf = this.buffer; - assert buf!=null : "double ByteBuffer clearance"; - this.buffer = null; - if (pool != null) { - pool.release(buf); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/common/ConnectionExpiredException.java --- a/src/java.net.http/share/classes/java/net/http/internal/common/ConnectionExpiredException.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,47 +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 java.net.http.internal.common; - -import java.io.IOException; - -/** - * Signals that an end of file or end of stream has been reached - * unexpectedly before any protocol specific data has been received. - */ -public final class ConnectionExpiredException extends IOException { - private static final long serialVersionUID = 0; - - /** - * Constructs a {@code ConnectionExpiredException} with the specified detail - * message and cause. - * - * @param s the detail message - * @param cause the throwable cause - */ - public ConnectionExpiredException(String s, Throwable cause) { - super(s, cause); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/common/DebugLogger.java --- a/src/java.net.http/share/classes/java/net/http/internal/common/DebugLogger.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,251 +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 java.net.http.internal.common; - -import java.io.PrintStream; -import java.util.Objects; -import java.util.ResourceBundle; -import java.util.function.Supplier; -import java.lang.System.Logger; - -/** - * A {@code System.Logger} that forwards all messages to an underlying - * {@code System.Logger}, after adding some decoration. - * The logger also has the ability to additionally send the logged messages - * to System.err or System.out, whether the underlying logger is activated or not. - * In addition instance of {@code DebugLogger} support both - * {@link String#format(String, Object...)} and - * {@link java.text.MessageFormat#format(String, Object...)} formatting. - * String-like formatting is enabled by the presence of "%s" or "%d" in the format - * string. MessageFormat-like formatting is enabled by the presence of "{0" or "{1". - *

- * See {@link Utils#getDebugLogger(Supplier, boolean)} and - * {@link Utils#getHpackLogger(Supplier, boolean)}. - */ -class DebugLogger implements Logger { - // deliberately not in the same subtree than standard loggers. - final static String HTTP_NAME = "jdk.internal.httpclient.debug"; - final static String HPACK_NAME = "jdk.internal.httpclient.hpack.debug"; - final static Logger HTTP = System.getLogger(HTTP_NAME); - final static Logger HPACK = System.getLogger(HPACK_NAME); - final static long START_NANOS = System.nanoTime(); - - private final Supplier dbgTag; - private final Level errLevel; - private final Level outLevel; - private final Logger logger; - private final boolean debugOn; - private final boolean traceOn; - - /** - * Create a logger for debug traces.The logger should only be used - * with levels whose severity is {@code <= DEBUG}. - * - * By default, this logger will forward all messages logged to the supplied - * {@code logger}. - * But in addition, if the message severity level is {@code >=} to - * the provided {@code errLevel} it will print the messages on System.err, - * and if the message severity level is {@code >=} to - * the provided {@code outLevel} it will also print the messages on System.out. - *

- * The logger will add some decoration to the printed message, in the form of - * {@code :[] [] : } - * - * @apiNote To obtain a logger that will always print things on stderr in - * addition to forwarding to the internal logger, use - * {@code new DebugLogger(logger, this::dbgTag, Level.OFF, Level.ALL);}. - * To obtain a logger that will only forward to the internal logger, - * use {@code new DebugLogger(logger, this::dbgTag, Level.OFF, Level.OFF);}. - * - * @param logger The internal logger to which messages will be forwarded. - * This should be either {@link #HPACK} or {@link #HTTP}; - * - * @param dbgTag A lambda that returns a string that identifies the caller - * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))") - * @param outLevel The level above which messages will be also printed on - * System.out (in addition to being forwarded to the internal logger). - * @param errLevel The level above which messages will be also printed on - * System.err (in addition to being forwarded to the internal logger). - * - * @return A logger for HTTP internal debug traces - */ - private DebugLogger(Logger logger, - Supplier dbgTag, - Level outLevel, - Level errLevel) { - this.dbgTag = dbgTag; - this.errLevel = errLevel; - this.outLevel = outLevel; - this.logger = Objects.requireNonNull(logger); - // support only static configuration. - this.debugOn = isEnabled(Level.DEBUG); - this.traceOn = isEnabled(Level.TRACE); - } - - @Override - public String getName() { - return logger.getName(); - } - - private boolean isEnabled(Level level) { - if (level == Level.OFF) return false; - int severity = level.getSeverity(); - return severity >= errLevel.getSeverity() - || severity >= outLevel.getSeverity() - || logger.isLoggable(level); - } - - @Override - public boolean isLoggable(Level level) { - // fast path, we assume these guys never change. - // support only static configuration. - if (level == Level.DEBUG) return debugOn; - if (level == Level.TRACE) return traceOn; - return isEnabled(level); - } - - @Override - public void log(Level level, ResourceBundle unused, - String format, Object... params) { - // fast path, we assume these guys never change. - // support only static configuration. - if (level == Level.DEBUG && !debugOn) return; - if (level == Level.TRACE && !traceOn) return; - - int severity = level.getSeverity(); - if (errLevel != Level.OFF - && errLevel.getSeverity() <= severity) { - print(System.err, level, format, params, null); - } - if (outLevel != Level.OFF - && outLevel.getSeverity() <= severity) { - print(System.out, level, format, params, null); - } - if (logger.isLoggable(level)) { - logger.log(level, unused, - getFormat(new StringBuilder(), format, params).toString(), - params); - } - } - - @Override - public void log(Level level, ResourceBundle unused, String msg, - Throwable thrown) { - // fast path, we assume these guys never change. - if (level == Level.DEBUG && !debugOn) return; - if (level == Level.TRACE && !traceOn) return; - - if (errLevel != Level.OFF - && errLevel.getSeverity() <= level.getSeverity()) { - print(System.err, level, msg, null, thrown); - } - if (outLevel != Level.OFF - && outLevel.getSeverity() <= level.getSeverity()) { - print(System.out, level, msg, null, thrown); - } - if (logger.isLoggable(level)) { - logger.log(level, unused, - getFormat(new StringBuilder(), msg, null).toString(), - thrown); - } - } - - private void print(PrintStream out, Level level, String msg, - Object[] params, Throwable t) { - StringBuilder sb = new StringBuilder(); - sb.append(level.name()).append(':').append(' '); - sb = format(sb, msg, params); - if (t != null) sb.append(' ').append(t.toString()); - out.println(sb.toString()); - if (t != null) { - t.printStackTrace(out); - } - } - - private StringBuilder decorate(StringBuilder sb, String msg) { - String tag = dbgTag == null ? null : dbgTag.get(); - String res = msg == null ? "" : msg; - long elapsed = System.nanoTime() - START_NANOS; - long millis = elapsed / 1000_000; - long secs = millis / 1000; - sb.append('[').append(Thread.currentThread().getName()).append(']') - .append(' ').append('['); - if (secs > 0) { - sb.append(secs).append('s'); - } - millis = millis % 1000; - if (millis > 0) { - if (secs > 0) sb.append(' '); - sb.append(millis).append("ms"); - } - sb.append(']').append(' '); - if (tag != null) { - sb.append(tag).append(' '); - } - sb.append(res); - return sb; - } - - - private StringBuilder getFormat(StringBuilder sb, String format, Object[] params) { - if (format == null || params == null || params.length == 0) { - return decorate(sb, format); - } else if (format.contains("{0}") || format.contains("{1}")) { - return decorate(sb, format); - } else if (format.contains("%s") || format.contains("%d")) { - try { - return decorate(sb, String.format(format, params)); - } catch (Throwable t) { - return decorate(sb, format); - } - } else { - return decorate(sb, format); - } - } - - private StringBuilder format(StringBuilder sb, String format, Object[] params) { - if (format == null || params == null || params.length == 0) { - return decorate(sb, format); - } else if (format.contains("{0}") || format.contains("{1}")) { - return decorate(sb, java.text.MessageFormat.format(format, params)); - } else if (format.contains("%s") || format.contains("%d")) { - try { - return decorate(sb, String.format(format, params)); - } catch (Throwable t) { - return decorate(sb, format); - } - } else { - return decorate(sb, format); - } - } - - public static DebugLogger createHttpLogger(Supplier dbgTag, Level outLevel, Level errLevel) { - return new DebugLogger(HTTP, dbgTag, outLevel, errLevel); - } - - public static DebugLogger createHpackLogger(Supplier dbgTag, Level outLevel, Level errLevel) { - return new DebugLogger(HPACK, dbgTag, outLevel, errLevel); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/common/Demand.java --- a/src/java.net.http/share/classes/java/net/http/internal/common/Demand.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,116 +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 java.net.http.internal.common; - -import java.util.concurrent.atomic.AtomicLong; - -/** - * Encapsulates operations with demand (Reactive Streams). - * - *

Demand is the aggregated number of elements requested by a Subscriber - * which is yet to be delivered (fulfilled) by the Publisher. - */ -public final class Demand { - - private final AtomicLong val = new AtomicLong(); - - /** - * Increases this demand by the specified positive value. - * - * @param n - * increment {@code > 0} - * - * @return {@code true} iff prior to this operation this demand was fulfilled - */ - public boolean increase(long n) { - if (n <= 0) { - throw new IllegalArgumentException(String.valueOf(n)); - } - long prev = val.getAndAccumulate(n, (p, i) -> p + i < 0 ? Long.MAX_VALUE : p + i); - return prev == 0; - } - - /** - * Tries to decrease this demand by the specified positive value. - * - *

The actual value this demand has been decreased by might be less than - * {@code n}, including {@code 0} (no decrease at all). - * - * @param n - * decrement {@code > 0} - * - * @return a value {@code m} ({@code 0 <= m <= n}) this demand has been - * actually decreased by - */ - public long decreaseAndGet(long n) { - if (n <= 0) { - throw new IllegalArgumentException(String.valueOf(n)); - } - long p, d; - do { - d = val.get(); - p = Math.min(d, n); - } while (!val.compareAndSet(d, d - p)); - return p; - } - - /** - * Tries to decrease this demand by {@code 1}. - * - * @return {@code true} iff this demand has been decreased by {@code 1} - */ - public boolean tryDecrement() { - return decreaseAndGet(1) == 1; - } - - /** - * @return {@code true} iff there is no unfulfilled demand - */ - public boolean isFulfilled() { - return val.get() == 0; - } - - /** - * Resets this object to its initial state. - */ - public void reset() { - val.set(0); - } - - /** - * Returns the current value of this demand. - * - * @return the current value of this demand - */ - public long get() { - return val.get(); - } - - @Override - public String toString() { - return String.valueOf(val.get()); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/common/FlowTube.java --- a/src/java.net.http/share/classes/java/net/http/internal/common/FlowTube.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,185 +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 java.net.http.internal.common; - -import java.nio.ByteBuffer; -import java.util.List; -import java.util.concurrent.Flow; - -/** - * FlowTube is an I/O abstraction that allows reading from and writing to a - * destination asynchronously. - * This is not a {@link Flow.Processor - * Flow.Processor<List<ByteBuffer>, List<ByteBuffer>>}, - * but rather models a publisher source and a subscriber sink in a bidirectional flow. - *

- * The {@code connectFlows} method should be called to connect the bidirectional - * flow. A FlowTube supports handing over the same read subscription to different - * sequential read subscribers over time. When {@code connectFlows(writePublisher, - * readSubscriber} is called, the FlowTube will call {@code dropSubscription} on - * its former readSubscriber, and {@code onSubscribe} on its new readSubscriber. - */ -public interface FlowTube extends - Flow.Publisher>, - Flow.Subscriber> { - - /** - * A subscriber for reading from the bidirectional flow. - * A TubeSubscriber is a {@code Flow.Subscriber} that can be canceled - * by calling {@code dropSubscription()}. - * Once {@code dropSubscription()} is called, the {@code TubeSubscriber} - * should stop calling any method on its subscription. - */ - static interface TubeSubscriber extends Flow.Subscriber> { - - /** - * Called when the flow is connected again, and the subscription - * is handed over to a new subscriber. - * Once {@code dropSubscription()} is called, the {@code TubeSubscriber} - * should stop calling any method on its subscription. - */ - default void dropSubscription() { } - - } - - /** - * A publisher for writing to the bidirectional flow. - */ - static interface TubePublisher extends Flow.Publisher> { - - } - - /** - * Connects the bidirectional flows to a write {@code Publisher} and a - * read {@code Subscriber}. This method can be called sequentially - * several times to switch existing publishers and subscribers to a new - * pair of write subscriber and read publisher. - * @param writePublisher A new publisher for writing to the bidirectional flow. - * @param readSubscriber A new subscriber for reading from the bidirectional - * flow. - */ - default void connectFlows(TubePublisher writePublisher, - TubeSubscriber readSubscriber) { - - this.subscribe(readSubscriber); - writePublisher.subscribe(this); - } - - /** - * Returns true if this flow was completed, either exceptionally - * or normally (EOF reached). - * @return true if the flow is finished - */ - boolean isFinished(); - - - /** - * Returns {@code s} if {@code s} is a {@code TubeSubscriber}, otherwise - * wraps it in a {@code TubeSubscriber}. - * Using the wrapper is only appropriate in the case where - * {@code dropSubscription} doesn't need to be implemented, and the - * {@code TubeSubscriber} is subscribed only once. - * - * @param s a subscriber for reading. - * @return a {@code TubeSubscriber}: either {@code s} if {@code s} is a - * {@code TubeSubscriber}, otherwise a {@code TubeSubscriber} - * wrapper that delegates to {@code s} - */ - static TubeSubscriber asTubeSubscriber(Flow.Subscriber> s) { - if (s instanceof TubeSubscriber) { - return (TubeSubscriber) s; - } - return new AbstractTubeSubscriber.TubeSubscriberWrapper(s); - } - - /** - * Returns {@code s} if {@code s} is a {@code TubePublisher}, otherwise - * wraps it in a {@code TubePublisher}. - * - * @param p a publisher for writing. - * @return a {@code TubePublisher}: either {@code s} if {@code s} is a - * {@code TubePublisher}, otherwise a {@code TubePublisher} - * wrapper that delegates to {@code s} - */ - static TubePublisher asTubePublisher(Flow.Publisher> p) { - if (p instanceof TubePublisher) { - return (TubePublisher) p; - } - return new AbstractTubePublisher.TubePublisherWrapper(p); - } - - /** - * Convenience abstract class for {@code TubePublisher} implementations. - * It is not required that a {@code TubePublisher} implementation extends - * this class. - */ - static abstract class AbstractTubePublisher implements TubePublisher { - static final class TubePublisherWrapper extends AbstractTubePublisher { - final Flow.Publisher> delegate; - public TubePublisherWrapper(Flow.Publisher> delegate) { - this.delegate = delegate; - } - @Override - public void subscribe(Flow.Subscriber> subscriber) { - delegate.subscribe(subscriber); - } - } - } - - /** - * Convenience abstract class for {@code TubeSubscriber} implementations. - * It is not required that a {@code TubeSubscriber} implementation extends - * this class. - */ - static abstract class AbstractTubeSubscriber implements TubeSubscriber { - static final class TubeSubscriberWrapper extends AbstractTubeSubscriber { - final Flow.Subscriber> delegate; - TubeSubscriberWrapper(Flow.Subscriber> delegate) { - this.delegate = delegate; - } - @Override - public void dropSubscription() {} - @Override - public void onSubscribe(Flow.Subscription subscription) { - delegate.onSubscribe(subscription); - } - @Override - public void onNext(List item) { - delegate.onNext(item); - } - @Override - public void onError(Throwable throwable) { - delegate.onError(throwable); - } - @Override - public void onComplete() { - delegate.onComplete(); - } - } - - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/common/HttpHeadersImpl.java --- a/src/java.net.http/share/classes/java/net/http/internal/common/HttpHeadersImpl.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,73 +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 java.net.http.internal.common; - -import java.net.http.HttpHeaders; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; - -/** - * Implementation of HttpHeaders. - */ -public class HttpHeadersImpl extends HttpHeaders { - - private final TreeMap> headers; - - public HttpHeadersImpl() { - headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - } - - @Override - public Map> map() { - return Collections.unmodifiableMap(headers); - } - - // package private mutators - - public HttpHeadersImpl deepCopy() { - HttpHeadersImpl h1 = new HttpHeadersImpl(); - for (Map.Entry> entry : headers.entrySet()) { - List valuesCopy = new ArrayList<>(entry.getValue()); - h1.headers.put(entry.getKey(), valuesCopy); - } - return h1; - } - - public void addHeader(String name, String value) { - headers.computeIfAbsent(name, k -> new ArrayList<>(1)) - .add(value); - } - - public void setHeader(String name, String value) { - List values = new ArrayList<>(1); // most headers has one value - values.add(value); - headers.put(name, values); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/common/Log.java --- a/src/java.net.http/share/classes/java/net/http/internal/common/Log.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,302 +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 java.net.http.internal.common; - -import java.net.http.HttpHeaders; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.function.Supplier; -import java.net.http.internal.frame.DataFrame; -import java.net.http.internal.frame.Http2Frame; -import java.net.http.internal.frame.WindowUpdateFrame; - -import javax.net.ssl.SNIServerName; -import javax.net.ssl.SSLParameters; - -/** - * -Djava.net.HttpClient.log= - * errors,requests,headers, - * frames[:control:data:window:all..],content,ssl,trace - * - * Any of errors, requests, headers or content are optional. - * - * Other handlers may be added. All logging is at level INFO - * - * Logger name is "jdk.httpclient.HttpClient" - */ -// implements System.Logger in order to be skipped when printing the caller's -// information -public abstract class Log implements System.Logger { - - static final String logProp = "jdk.httpclient.HttpClient.log"; - - public static final int OFF = 0; - public static final int ERRORS = 0x1; - public static final int REQUESTS = 0x2; - public static final int HEADERS = 0x4; - public static final int CONTENT = 0x8; - public static final int FRAMES = 0x10; - public static final int SSL = 0x20; - public static final int TRACE = 0x40; - static int logging; - - // Frame types: "control", "data", "window", "all" - public static final int CONTROL = 1; // all except DATA and WINDOW_UPDATES - public static final int DATA = 2; - public static final int WINDOW_UPDATES = 4; - public static final int ALL = CONTROL| DATA | WINDOW_UPDATES; - static int frametypes; - - static final System.Logger logger; - - static { - String s = Utils.getNetProperty(logProp); - if (s == null) { - logging = OFF; - } else { - String[] vals = s.split(","); - for (String val : vals) { - switch (val.toLowerCase(Locale.US)) { - case "errors": - logging |= ERRORS; - break; - case "requests": - logging |= REQUESTS; - break; - case "headers": - logging |= HEADERS; - break; - case "content": - logging |= CONTENT; - break; - case "ssl": - logging |= SSL; - break; - case "trace": - logging |= TRACE; - break; - case "all": - logging |= CONTENT|HEADERS|REQUESTS|FRAMES|ERRORS|TRACE|SSL; - break; - } - if (val.startsWith("frames")) { - logging |= FRAMES; - String[] types = val.split(":"); - if (types.length == 1) { - frametypes = CONTROL | DATA | WINDOW_UPDATES; - } else { - for (String type : types) { - switch (type.toLowerCase()) { - case "control": - frametypes |= CONTROL; - break; - case "data": - frametypes |= DATA; - break; - case "window": - frametypes |= WINDOW_UPDATES; - break; - case "all": - frametypes = ALL; - break; - } - } - } - } - } - } - if (logging != OFF) { - logger = System.getLogger("jdk.httpclient.HttpClient"); - } else { - logger = null; - } - } - public static boolean errors() { - return (logging & ERRORS) != 0; - } - - public static boolean requests() { - return (logging & REQUESTS) != 0; - } - - public static boolean headers() { - return (logging & HEADERS) != 0; - } - - public static boolean trace() { - return (logging & TRACE) != 0; - } - - public static boolean ssl() { - return (logging & SSL) != 0; - } - - public static boolean frames() { - return (logging & FRAMES) != 0; - } - - public static void logError(String s, Object... s1) { - if (errors()) { - logger.log(Level.INFO, "ERROR: " + s, s1); - } - } - - public static void logError(Throwable t) { - if (errors()) { - String s = Utils.stackTrace(t); - logger.log(Level.INFO, "ERROR: " + s); - } - } - - public static void logSSL(String s, Object... s1) { - if (ssl()) { - logger.log(Level.INFO, "SSL: " + s, s1); - } - } - - public static void logSSL(Supplier msgSupplier) { - if (ssl()) { - logger.log(Level.INFO, "SSL: " + msgSupplier.get()); - } - } - - public static void logTrace(String s, Object... s1) { - if (trace()) { - String format = "TRACE: " + s; - logger.log(Level.INFO, format, s1); - } - } - - public static void logRequest(String s, Object... s1) { - if (requests()) { - logger.log(Level.INFO, "REQUEST: " + s, s1); - } - } - - public static void logResponse(Supplier supplier) { - if (requests()) { - logger.log(Level.INFO, "RESPONSE: " + supplier.get()); - } - } - - public static void logHeaders(String s, Object... s1) { - if (headers()) { - logger.log(Level.INFO, "HEADERS: " + s, s1); - } - } - - public static boolean loggingFrame(Class clazz) { - if (frametypes == ALL) { - return true; - } - if (clazz == DataFrame.class) { - return (frametypes & DATA) != 0; - } else if (clazz == WindowUpdateFrame.class) { - return (frametypes & WINDOW_UPDATES) != 0; - } else { - return (frametypes & CONTROL) != 0; - } - } - - public static void logFrames(Http2Frame f, String direction) { - if (frames() && loggingFrame(f.getClass())) { - logger.log(Level.INFO, "FRAME: " + direction + ": " + f.toString()); - } - } - - public static void logParams(SSLParameters p) { - if (!Log.ssl()) { - return; - } - - if (p == null) { - Log.logSSL("SSLParameters: Null params"); - return; - } - - final StringBuilder sb = new StringBuilder("SSLParameters:"); - final List params = new ArrayList<>(); - if (p.getCipherSuites() != null) { - for (String cipher : p.getCipherSuites()) { - sb.append("\n cipher: {") - .append(params.size()).append("}"); - params.add(cipher); - } - } - - // SSLParameters.getApplicationProtocols() can't return null - // JDK 8 EXCL START - for (String approto : p.getApplicationProtocols()) { - sb.append("\n application protocol: {") - .append(params.size()).append("}"); - params.add(approto); - } - // JDK 8 EXCL END - - if (p.getProtocols() != null) { - for (String protocol : p.getProtocols()) { - sb.append("\n protocol: {") - .append(params.size()).append("}"); - params.add(protocol); - } - } - - if (p.getServerNames() != null) { - for (SNIServerName sname : p.getServerNames()) { - sb.append("\n server name: {") - .append(params.size()).append("}"); - params.add(sname.toString()); - } - } - sb.append('\n'); - - Log.logSSL(sb.toString(), params.toArray()); - } - - public static void dumpHeaders(StringBuilder sb, String prefix, HttpHeaders headers) { - if (headers != null) { - Map> h = headers.map(); - Set>> entries = h.entrySet(); - for (Map.Entry> entry : entries) { - String key = entry.getKey(); - List values = entry.getValue(); - sb.append(prefix).append(key).append(":"); - for (String value : values) { - sb.append(' ').append(value); - } - sb.append('\n'); - } - } - } - - - // not instantiable - private Log() {} -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/common/MinimalFuture.java --- a/src/java.net.http/share/classes/java/net/http/internal/common/MinimalFuture.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,110 +0,0 @@ -/* - * 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 java.net.http.internal.common; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.atomic.AtomicLong; - -import static java.util.Objects.requireNonNull; - -/* - * A CompletableFuture which does not allow any obtrusion logic. - * All methods of CompletionStage return instances of this class. - */ -public final class MinimalFuture extends CompletableFuture { - - @FunctionalInterface - public interface ExceptionalSupplier { - U get() throws Throwable; - } - - private final static AtomicLong TOKENS = new AtomicLong(); - private final long id; - - public static MinimalFuture completedFuture(U value) { - MinimalFuture f = new MinimalFuture<>(); - f.complete(value); - return f; - } - - public static CompletableFuture failedFuture(Throwable ex) { - requireNonNull(ex); - MinimalFuture f = new MinimalFuture<>(); - f.completeExceptionally(ex); - return f; - } - - public static CompletableFuture supply(ExceptionalSupplier supplier) { - CompletableFuture cf = new MinimalFuture<>(); - try { - U value = supplier.get(); - cf.complete(value); - } catch (Throwable t) { - cf.completeExceptionally(t); - } - return cf; - } - - public MinimalFuture() { - super(); - this.id = TOKENS.incrementAndGet(); - } - - @Override - public MinimalFuture newIncompleteFuture() { - return new MinimalFuture<>(); - } - - @Override - public void obtrudeValue(T value) { - throw new UnsupportedOperationException(); - } - - @Override - public void obtrudeException(Throwable ex) { - throw new UnsupportedOperationException(); - } - - @Override - public String toString() { - return super.toString() + " (id=" + id +")"; - } - - public static MinimalFuture of(CompletionStage stage) { - MinimalFuture cf = new MinimalFuture<>(); - stage.whenComplete((r,t) -> complete(cf, r, t)); - return cf; - } - - private static void complete(CompletableFuture cf, U result, Throwable t) { - if (t == null) { - cf.complete(result); - } else { - cf.completeExceptionally(t); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/common/Pair.java --- a/src/java.net.http/share/classes/java/net/http/internal/common/Pair.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,52 +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 java.net.http.internal.common; - -/** - * A simple paired value class - */ -public final class Pair { - - public Pair(T first, U second) { - this.second = second; - this.first = first; - } - - public final T first; - public final U second; - - // Because 'pair()' is shorter than 'new Pair<>()'. - // Sometimes this difference might be very significant (especially in a - // 80-ish characters boundary). Sorry diamond operator. - public static Pair pair(T first, U second) { - return new Pair<>(first, second); - } - - @Override - public String toString() { - return "(" + first + ", " + second + ")"; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/common/SSLFlowDelegate.java --- a/src/java.net.http/share/classes/java/net/http/internal/common/SSLFlowDelegate.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,906 +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 java.net.http.internal.common; - -import java.net.http.internal.common.SubscriberWrapper.SchedulingAction; - -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLEngineResult; -import javax.net.ssl.SSLEngineResult.HandshakeStatus; -import javax.net.ssl.SSLEngineResult.Status; -import javax.net.ssl.SSLException; -import java.io.IOException; -import java.lang.System.Logger.Level; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Executor; -import java.util.concurrent.Flow; -import java.util.concurrent.Flow.Subscriber; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Implements SSL using two SubscriberWrappers. - * - *

Constructor takes two Flow.Subscribers: one that receives the network - * data (after it has been encrypted by SSLFlowDelegate) data, and one that - * receives the application data (before it has been encrypted by SSLFlowDelegate). - * - *

Methods upstreamReader() and upstreamWriter() return the corresponding - * Flow.Subscribers containing Flows for the encrypted/decrypted upstream data. - * See diagram below. - * - *

How Flow.Subscribers are used in this class, and where they come from: - *

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

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

This class can be used as a synchronization aid that assists a number of - * parties in running a task in a mutually exclusive fashion. - * - *

To run the task, a party invokes {@code runOrSchedule}. To permanently - * prevent the task from subsequent runs, the party invokes {@code stop}. - * - *

The parties can, but do not have to, operate in different threads. - * - *

The task can be either synchronous ( completes when its {@code run} - * method returns ), or asynchronous ( completed when its - * {@code DeferredCompleter} is explicitly completed ). - * - *

The next run of the task will not begin until the previous run has - * finished. - * - *

The task may invoke {@code runOrSchedule} itself, which may be a normal - * situation. - */ -public final class SequentialScheduler { - - /* - Since the task is fixed and known beforehand, no blocking synchronization - (locks, queues, etc.) is required. The job can be done solely using - nonblocking primitives. - - The machinery below addresses two problems: - - 1. Running the task in a sequential order (no concurrent runs): - - begin, end, begin, end... - - 2. Avoiding indefinite recursion: - - begin - end - begin - end - ... - - Problem #1 is solved with a finite state machine with 4 states: - - BEGIN, AGAIN, END, and STOP. - - Problem #2 is solved with a "state modifier" OFFLOAD. - - Parties invoke `runOrSchedule()` to signal the task must run. A party - that has invoked `runOrSchedule()` either begins the task or exploits the - party that is either beginning the task or ending it. - - The party that is trying to end the task either ends it or begins it - again. - - To avoid indefinite recursion, before re-running the task the - TryEndDeferredCompleter sets the OFFLOAD bit, signalling to its "child" - TryEndDeferredCompleter that this ("parent") TryEndDeferredCompleter is - available and the "child" must offload the task on to the "parent". Then - a race begins. Whichever invocation of TryEndDeferredCompleter.complete - manages to unset OFFLOAD bit first does not do the work. - - There is at most 1 thread that is beginning the task and at most 2 - threads that are trying to end it: "parent" and "child". In case of a - synchronous task "parent" and "child" are the same thread. - */ - - /** - * An interface to signal the completion of a {@link RestartableTask}. - * - *

The invocation of {@code complete} completes the task. The invocation - * of {@code complete} may restart the task, if an attempt has previously - * been made to run the task while it was already running. - * - * @apiNote {@code DeferredCompleter} is useful when a task is not necessary - * complete when its {@code run} method returns, but will complete at a - * later time, and maybe in different thread. This type exists for - * readability purposes at use-sites only. - */ - public static abstract class DeferredCompleter { - - /** Extensible from this (outer) class ONLY. */ - private DeferredCompleter() { } - - /** Completes the task. Must be called once, and once only. */ - public abstract void complete(); - } - - /** - * A restartable task. - */ - @FunctionalInterface - public interface RestartableTask { - - /** - * The body of the task. - * - * @param taskCompleter - * A completer that must be invoked once, and only once, - * when this task is logically finished - */ - void run(DeferredCompleter taskCompleter); - } - - /** - * A simple and self-contained task that completes once its {@code run} - * method returns. - */ - public static abstract class CompleteRestartableTask - implements RestartableTask - { - @Override - public final void run(DeferredCompleter taskCompleter) { - try { - run(); - } finally { - taskCompleter.complete(); - } - } - - /** The body of the task. */ - protected abstract void run(); - } - - /** - * A task that runs its main loop within a synchronized block to provide - * memory visibility between runs. Since the main loop can't run concurrently, - * the lock shouldn't be contended and no deadlock should ever be possible. - */ - public static final class SynchronizedRestartableTask - extends CompleteRestartableTask { - - private final Runnable mainLoop; - private final Object lock = new Object(); - - public SynchronizedRestartableTask(Runnable mainLoop) { - this.mainLoop = mainLoop; - } - - @Override - protected void run() { - synchronized(lock) { - mainLoop.run(); - } - } - } - - private static final int OFFLOAD = 1; - private static final int AGAIN = 2; - private static final int BEGIN = 4; - private static final int STOP = 8; - private static final int END = 16; - - private final AtomicInteger state = new AtomicInteger(END); - private final RestartableTask restartableTask; - private final DeferredCompleter completer; - private final SchedulableTask schedulableTask; - - /** - * An auxiliary task that starts the restartable task: - * {@code restartableTask.run(completer)}. - */ - private final class SchedulableTask implements Runnable { - @Override - public void run() { - restartableTask.run(completer); - } - } - - public SequentialScheduler(RestartableTask restartableTask) { - this.restartableTask = requireNonNull(restartableTask); - this.completer = new TryEndDeferredCompleter(); - this.schedulableTask = new SchedulableTask(); - } - - /** - * Runs or schedules the task to be run. - * - * @implSpec The recursion which is possible here must be bounded: - * - *

{@code
-     *     this.runOrSchedule()
-     *         completer.complete()
-     *             this.runOrSchedule()
-     *                 ...
-     * }
- * - * @implNote The recursion in this implementation has the maximum - * depth of 1. - */ - public void runOrSchedule() { - runOrSchedule(schedulableTask, null); - } - - /** - * Executes or schedules the task to be executed in the provided executor. - * - *

This method can be used when potential executing from a calling - * thread is not desirable. - * - * @param executor - * An executor in which to execute the task, if the task needs - * to be executed. - * - * @apiNote The given executor can be {@code null} in which case calling - * {@code runOrSchedule(null)} is strictly equivalent to calling - * {@code runOrSchedule()}. - */ - public void runOrSchedule(Executor executor) { - runOrSchedule(schedulableTask, executor); - } - - private void runOrSchedule(SchedulableTask task, Executor executor) { - while (true) { - int s = state.get(); - if (s == END) { - if (state.compareAndSet(END, BEGIN)) { - break; - } - } else if ((s & BEGIN) != 0) { - // Tries to change the state to AGAIN, preserving OFFLOAD bit - if (state.compareAndSet(s, AGAIN | (s & OFFLOAD))) { - return; - } - } else if ((s & AGAIN) != 0 || s == STOP) { - /* In the case of AGAIN the scheduler does not provide - happens-before relationship between actions prior to - runOrSchedule() and actions that happen in task.run(). - The reason is that no volatile write is done in this case, - and the call piggybacks on the call that has actually set - AGAIN state. */ - return; - } else { - // Non-existent state, or the one that cannot be offloaded - throw new InternalError(String.valueOf(s)); - } - } - if (executor == null) { - task.run(); - } else { - executor.execute(task); - } - } - - /** The only concrete {@code DeferredCompleter} implementation. */ - private class TryEndDeferredCompleter extends DeferredCompleter { - - @Override - public void complete() { - while (true) { - int s; - while (((s = state.get()) & OFFLOAD) != 0) { - // Tries to offload ending of the task to the parent - if (state.compareAndSet(s, s & ~OFFLOAD)) { - return; - } - } - while (true) { - if ((s & OFFLOAD) != 0) { - /* OFFLOAD bit can never be observed here. Otherwise - it would mean there is another invocation of - "complete" that can run the task. */ - throw new InternalError(String.valueOf(s)); - } - if (s == BEGIN) { - if (state.compareAndSet(BEGIN, END)) { - return; - } - } else if (s == AGAIN) { - if (state.compareAndSet(AGAIN, BEGIN | OFFLOAD)) { - break; - } - } else if (s == STOP) { - return; - } else if (s == END) { - throw new IllegalStateException("Duplicate completion"); - } else { - // Non-existent state - throw new InternalError(String.valueOf(s)); - } - s = state.get(); - } - restartableTask.run(completer); - } - } - } - - /** - * Tells whether, or not, this scheduler has been permanently stopped. - * - *

Should be used from inside the task to poll the status of the - * scheduler, pretty much the same way as it is done for threads: - *

{@code
-     *     if (!Thread.currentThread().isInterrupted()) {
-     *         ...
-     *     }
-     * }
- */ - public boolean isStopped() { - return state.get() == STOP; - } - - /** - * Stops this scheduler. Subsequent invocations of {@code runOrSchedule} - * are effectively no-ops. - * - *

If the task has already begun, this invocation will not affect it, - * unless the task itself uses {@code isStopped()} method to check the state - * of the handler. - */ - public void stop() { - state.set(STOP); - } - - /** - * Returns a new {@code SequentialScheduler} that executes the provided - * {@code mainLoop} from within a {@link SynchronizedRestartableTask}. - * - * @apiNote This is equivalent to calling - * {@code new SequentialScheduler(new SynchronizedRestartableTask(mainLoop))} - * The main loop must not perform any blocking operation. - * - * @param mainLoop The main loop of the new sequential scheduler - * @return a new {@code SequentialScheduler} that executes the provided - * {@code mainLoop} from within a {@link SynchronizedRestartableTask}. - */ - public static SequentialScheduler synchronizedScheduler(Runnable mainLoop) { - return new SequentialScheduler(new SynchronizedRestartableTask(mainLoop)); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/common/SubscriberWrapper.java --- a/src/java.net.http/share/classes/java/net/http/internal/common/SubscriberWrapper.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,461 +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 java.net.http.internal.common; - -import java.io.Closeable; -import java.lang.System.Logger.Level; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Flow; -import java.util.concurrent.Flow.Subscriber; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; - -/** - * A wrapper for a Flow.Subscriber. This wrapper delivers data to the wrapped - * Subscriber which is supplied to the constructor. This class takes care of - * downstream flow control automatically and upstream flow control automatically - * by default. - *

- * Processing is done by implementing the {@link #incoming(List, boolean)} method - * which supplies buffers from upstream. This method (or any other method) - * can then call the outgoing() method to deliver processed buffers downstream. - *

- * Upstream error signals are delivered downstream directly. Cancellation from - * downstream is also propagated upstream immediately. - *

- * Each SubscriberWrapper has a {@link java.util.concurrent.CompletableFuture}{@code } - * which propagates completion/errors from downstream to upstream. Normal completion - * can only occur after onComplete() is called, but errors can be propagated upwards - * at any time. - */ -public abstract class SubscriberWrapper - implements FlowTube.TubeSubscriber, Closeable, Flow.Processor,List> - // TODO: SSLTube Subscriber will never change? Does this really need to be a TS? -{ - static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag. - final System.Logger logger = - Utils.getDebugLogger(this::dbgString, DEBUG); - - public enum SchedulingAction { CONTINUE, RETURN, RESCHEDULE } - - volatile Flow.Subscription upstreamSubscription; - final SubscriptionBase downstreamSubscription; - volatile boolean upstreamCompleted; - volatile boolean downstreamCompleted; - volatile boolean completionAcknowledged; - private volatile Subscriber> downstreamSubscriber; - // processed byte to send to the downstream subscriber. - private final ConcurrentLinkedQueue> outputQ; - private final CompletableFuture cf; - private final SequentialScheduler pushScheduler; - private final AtomicReference errorRef = new AtomicReference<>(); - final AtomicLong upstreamWindow = new AtomicLong(0); - - /** - * Wraps the given downstream subscriber. For each call to {@link - * #onNext(List) } the given filter function is invoked - * and the list (if not empty) returned is passed downstream. - * - * A {@code CompletableFuture} is supplied which can be used to signal an - * error from downstream and which terminates the wrapper or which signals - * completion of downstream activity which can be propagated upstream. Error - * completion can be signaled at any time, but normal completion must not be - * signaled before onComplete() is called. - */ - public SubscriberWrapper() - { - this.outputQ = new ConcurrentLinkedQueue<>(); - this.cf = new MinimalFuture<>(); - this.pushScheduler = - SequentialScheduler.synchronizedScheduler(new DownstreamPusher()); - this.downstreamSubscription = new SubscriptionBase(pushScheduler, - this::downstreamCompletion); - } - - @Override - public final void subscribe(Subscriber> downstreamSubscriber) { - Objects.requireNonNull(downstreamSubscriber); - this.downstreamSubscriber = downstreamSubscriber; - } - - /** - * Wraps the given downstream wrapper in this. For each call to - * {@link #onNext(List) } the incoming() method is called. - * - * The {@code downstreamCF} from the downstream wrapper is linked to this - * wrappers notifier. - * - * @param downstreamWrapper downstream destination - */ - public SubscriberWrapper(Subscriber> downstreamWrapper) - { - this(); - subscribe(downstreamWrapper); - } - - /** - * Delivers data to be processed by this wrapper. Generated data to be sent - * downstream, must be provided to the {@link #outgoing(List, boolean)}} - * method. - * - * @param buffers a List of ByteBuffers. - * @param complete if true then no more data will be added to the list - */ - protected abstract void incoming(List buffers, boolean complete); - - /** - * This method is called to determine the window size to use at any time. The - * current window is supplied together with the current downstream queue size. - * {@code 0} should be returned if no change is - * required or a positive integer which will be added to the current window. - * The default implementation maintains a downstream queue size of no greater - * than 5. The method can be overridden if required. - * - * @param currentWindow the current upstream subscription window - * @param downstreamQsize the current number of buffers waiting to be sent - * downstream - * - * @return value to add to currentWindow - */ - protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) { - if (downstreamQsize > 5) { - return 0; - } - - if (currentWindow == 0) { - return 1; - } else { - return 0; - } - } - - /** - * Override this if anything needs to be done after the upstream subscriber - * has subscribed - */ - protected void onSubscribe() { - } - - /** - * Override this if anything needs to be done before checking for error - * and processing the input queue. - * @return - */ - protected SchedulingAction enterScheduling() { - return SchedulingAction.CONTINUE; - } - - protected boolean signalScheduling() { - if (downstreamCompleted || pushScheduler.isStopped()) { - return false; - } - pushScheduler.runOrSchedule(); - return true; - } - - /** - * Delivers buffers of data downstream. After incoming() - * has been called complete == true signifying completion of the upstream - * subscription, data may continue to be delivered, up to when outgoing() is - * called complete == true, after which, the downstream subscription is - * completed. - * - * It's an error to call outgoing() with complete = true if incoming() has - * not previously been called with it. - */ - public void outgoing(ByteBuffer buffer, boolean complete) { - Objects.requireNonNull(buffer); - assert !complete || !buffer.hasRemaining(); - outgoing(List.of(buffer), complete); - } - - /** - * Sometime it might be necessary to complete the downstream subscriber - * before the upstream completes. For instance, when an SSL server - * sends a notify_close. In that case we should let the outgoing - * complete before upstream us completed. - * @return true, may be overridden by subclasses. - */ - public boolean closing() { - return false; - } - - public void outgoing(List buffers, boolean complete) { - Objects.requireNonNull(buffers); - if (complete) { - assert Utils.remaining(buffers) == 0; - boolean closing = closing(); - logger.log(Level.DEBUG, - "completionAcknowledged upstreamCompleted:%s, downstreamCompleted:%s, closing:%s", - upstreamCompleted, downstreamCompleted, closing); - if (!upstreamCompleted && !closing) - throw new IllegalStateException("upstream not completed"); - completionAcknowledged = true; - } else { - logger.log(Level.DEBUG, () -> "Adding " - + Utils.remaining(buffers) - + " to outputQ queue"); - outputQ.add(buffers); - } - logger.log(Level.DEBUG, () -> "pushScheduler " - + (pushScheduler.isStopped() ? " is stopped!" : " is alive")); - pushScheduler.runOrSchedule(); - } - - /** - * Returns a CompletableFuture which completes when this wrapper completes. - * Normal completion happens with the following steps (in order): - * 1. onComplete() is called - * 2. incoming() called with complete = true - * 3. outgoing() may continue to be called normally - * 4. outgoing called with complete = true - * 5. downstream subscriber is called onComplete() - * - * If the subscription is canceled or onComplete() is invoked the - * CompletableFuture completes exceptionally. Exceptional completion - * also occurs if downstreamCF completes exceptionally. - */ - public CompletableFuture completion() { - return cf; - } - - /** - * Invoked whenever it 'may' be possible to push buffers downstream. - */ - class DownstreamPusher implements Runnable { - @Override - public void run() { - try { - run1(); - } catch (Throwable t) { - errorCommon(t); - } - } - - private void run1() { - if (downstreamCompleted) { - logger.log(Level.DEBUG, "DownstreamPusher: downstream is already completed"); - return; - } - switch (enterScheduling()) { - case CONTINUE: break; - case RESCHEDULE: pushScheduler.runOrSchedule(); return; - case RETURN: return; - default: - errorRef.compareAndSet(null, - new InternalError("unknown scheduling command")); - break; - } - // If there was an error, send it downstream. - Throwable error = errorRef.get(); - if (error != null) { - synchronized(this) { - if (downstreamCompleted) return; - downstreamCompleted = true; - } - logger.log(Level.DEBUG, - () -> "DownstreamPusher: forwarding error downstream: " + error); - pushScheduler.stop(); - outputQ.clear(); - downstreamSubscriber.onError(error); - return; - } - - // OK - no error, let's proceed - if (!outputQ.isEmpty()) { - logger.log(Level.DEBUG, - "DownstreamPusher: queue not empty, downstreamSubscription: %s", - downstreamSubscription); - } else { - logger.log(Level.DEBUG, - "DownstreamPusher: queue empty, downstreamSubscription: %s", - downstreamSubscription); - } - - final boolean dbgOn = logger.isLoggable(Level.DEBUG); - while (!outputQ.isEmpty() && downstreamSubscription.tryDecrement()) { - List b = outputQ.poll(); - if (dbgOn) logger.log(Level.DEBUG, - "DownstreamPusher: Pushing " - + Utils.remaining(b) - + " bytes downstream"); - downstreamSubscriber.onNext(b); - } - upstreamWindowUpdate(); - checkCompletion(); - } - } - - void upstreamWindowUpdate() { - long downstreamQueueSize = outputQ.size(); - long n = upstreamWindowUpdate(upstreamWindow.get(), downstreamQueueSize); - if (n > 0) - upstreamRequest(n); - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - if (upstreamSubscription != null) { - throw new IllegalStateException("Single shot publisher"); - } - this.upstreamSubscription = subscription; - upstreamRequest(upstreamWindowUpdate(0, 0)); - logger.log(Level.DEBUG, - "calling downstreamSubscriber::onSubscribe on %s", - downstreamSubscriber); - downstreamSubscriber.onSubscribe(downstreamSubscription); - onSubscribe(); - } - - @Override - public void onNext(List item) { - logger.log(Level.DEBUG, "onNext"); - long prev = upstreamWindow.getAndDecrement(); - if (prev <= 0) - throw new IllegalStateException("invalid onNext call"); - incomingCaller(item, false); - upstreamWindowUpdate(); - } - - private void upstreamRequest(long n) { - logger.log(Level.DEBUG, "requesting %d", n); - upstreamWindow.getAndAdd(n); - upstreamSubscription.request(n); - } - - protected void requestMore() { - if (upstreamWindow.get() == 0) { - upstreamRequest(1); - } - } - - public long upstreamWindow() { - return upstreamWindow.get(); - } - - @Override - public void onError(Throwable throwable) { - logger.log(Level.DEBUG, () -> "onError: " + throwable); - errorCommon(Objects.requireNonNull(throwable)); - } - - protected boolean errorCommon(Throwable throwable) { - assert throwable != null || - (throwable = new AssertionError("null throwable")) != null; - if (errorRef.compareAndSet(null, throwable)) { - logger.log(Level.DEBUG, "error", throwable); - pushScheduler.runOrSchedule(); - upstreamCompleted = true; - cf.completeExceptionally(throwable); - return true; - } - return false; - } - - @Override - public void close() { - errorCommon(new RuntimeException("wrapper closed")); - } - - private void incomingCaller(List l, boolean complete) { - try { - incoming(l, complete); - } catch(Throwable t) { - errorCommon(t); - } - } - - @Override - public void onComplete() { - logger.log(Level.DEBUG, () -> "upstream completed: " + toString()); - upstreamCompleted = true; - incomingCaller(Utils.EMPTY_BB_LIST, true); - // pushScheduler will call checkCompletion() - pushScheduler.runOrSchedule(); - } - - /** Adds the given data to the input queue. */ - public void addData(ByteBuffer l) { - if (upstreamSubscription == null) { - throw new IllegalStateException("can't add data before upstream subscriber subscribes"); - } - incomingCaller(List.of(l), false); - } - - void checkCompletion() { - if (downstreamCompleted || !upstreamCompleted) { - return; - } - if (!outputQ.isEmpty()) { - return; - } - if (errorRef.get() != null) { - pushScheduler.runOrSchedule(); - return; - } - if (completionAcknowledged) { - logger.log(Level.DEBUG, "calling downstreamSubscriber.onComplete()"); - downstreamSubscriber.onComplete(); - // Fix me subscriber.onComplete.run(); - downstreamCompleted = true; - cf.complete(null); - } - } - - // called from the downstream Subscription.cancel() - void downstreamCompletion() { - upstreamSubscription.cancel(); - cf.complete(null); - } - - public void resetDownstreamDemand() { - downstreamSubscription.demand.reset(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("SubscriberWrapper:") - .append(" upstreamCompleted: ").append(Boolean.toString(upstreamCompleted)) - .append(" upstreamWindow: ").append(upstreamWindow.toString()) - .append(" downstreamCompleted: ").append(Boolean.toString(downstreamCompleted)) - .append(" completionAcknowledged: ").append(Boolean.toString(completionAcknowledged)) - .append(" outputQ size: ").append(Integer.toString(outputQ.size())) - //.append(" outputQ: ").append(outputQ.toString()) - .append(" cf: ").append(cf.toString()) - .append(" downstreamSubscription: ").append(downstreamSubscription.toString()); - - return sb.toString(); - } - - public String dbgString() { - return "SubscriberWrapper"; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/common/SubscriptionBase.java --- a/src/java.net.http/share/classes/java/net/http/internal/common/SubscriptionBase.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,88 +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 java.net.http.internal.common; - -import java.util.concurrent.Flow; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Maintains subscription counter and provides primitives for - * - accessing window - * - reducing window when delivering items externally - * - resume delivery when window was zero previously - * - * @author mimcmah - */ -public class SubscriptionBase implements Flow.Subscription { - - final Demand demand = new Demand(); - - final SequentialScheduler scheduler; // when window was zero and is opened, run this - final Runnable cancelAction; // when subscription cancelled, run this - final AtomicBoolean cancelled; - - public SubscriptionBase(SequentialScheduler scheduler, Runnable cancelAction) { - this.scheduler = scheduler; - this.cancelAction = cancelAction; - this.cancelled = new AtomicBoolean(false); - } - - @Override - public void request(long n) { - if (demand.increase(n)) - scheduler.runOrSchedule(); - } - - - - @Override - public synchronized String toString() { - return "SubscriptionBase: window = " + demand.get() + - " cancelled = " + cancelled.toString(); - } - - /** - * Returns true if the window was reduced by 1. In that case - * items must be supplied to subscribers and the scheduler run - * externally. If the window could not be reduced by 1, then false - * is returned and the scheduler will run later when the window is updated. - */ - public boolean tryDecrement() { - return demand.tryDecrement(); - } - - public long window() { - return demand.get(); - } - - @Override - public void cancel() { - if (cancelled.getAndSet(true)) - return; - scheduler.stop(); - cancelAction.run(); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/common/Utils.java --- a/src/java.net.http/share/classes/java/net/http/internal/common/Utils.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,755 +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 java.net.http.internal.common; - -import java.net.http.HttpHeaders; -import sun.net.NetProperties; -import sun.net.util.IPAddressUtil; -import sun.net.www.HeaderParser; - -import javax.net.ssl.SSLParameters; -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.IOException; -import java.io.PrintStream; -import java.io.UncheckedIOException; -import java.io.UnsupportedEncodingException; -import java.lang.System.Logger; -import java.lang.System.Logger.Level; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URLPermission; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutionException; -import java.util.function.BiPredicate; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static java.util.stream.Collectors.joining; - -/** - * Miscellaneous utilities - */ -public final class Utils { - - public static final boolean ASSERTIONSENABLED; - static { - boolean enabled = false; - assert enabled = true; - ASSERTIONSENABLED = enabled; - } -// public static final boolean TESTING; -// static { -// if (ASSERTIONSENABLED) { -// PrivilegedAction action = () -> System.getProperty("test.src"); -// TESTING = AccessController.doPrivileged(action) != null; -// } else TESTING = false; -// } - public static final boolean DEBUG = // Revisit: temporary dev flag. - getBooleanProperty(DebugLogger.HTTP_NAME, false); - public static final boolean DEBUG_HPACK = // Revisit: temporary dev flag. - getBooleanProperty(DebugLogger.HPACK_NAME, false); - public static final boolean TESTING = DEBUG; - - /** - * Allocated buffer size. Must never be higher than 16K. But can be lower - * if smaller allocation units preferred. HTTP/2 mandates that all - * implementations support frame payloads of at least 16K. - */ - private static final int DEFAULT_BUFSIZE = 16 * 1024; - - public static final int BUFSIZE = getIntegerNetProperty( - "jdk.httpclient.bufsize", DEFAULT_BUFSIZE - ); - - private static final Set DISALLOWED_HEADERS_SET; - static { - // A case insensitive TreeSet of strings. - TreeSet treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - treeSet.addAll(Set.of("connection", "content-length", - "date", "expect", "from", "host", "origin", - "referer", "upgrade", - "via", "warning")); - DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet); - } - - public static final Predicate - ALLOWED_HEADERS = header -> !DISALLOWED_HEADERS_SET.contains(header); - - private static final Predicate IS_PROXY_HEADER = (k) -> - k != null && k.length() > 6 && "proxy-".equalsIgnoreCase(k.substring(0,6)); - private static final Predicate NO_PROXY_HEADER = - IS_PROXY_HEADER.negate(); - private static final Predicate ALL_HEADERS = (s) -> true; - - private static final Set PROXY_AUTH_DISABLED_SCHEMES; - private static final Set PROXY_AUTH_TUNNEL_DISABLED_SCHEMES; - static { - String proxyAuthDisabled = - getNetProperty("jdk.http.auth.proxying.disabledSchemes"); - String proxyAuthTunnelDisabled = - getNetProperty("jdk.http.auth.tunneling.disabledSchemes"); - PROXY_AUTH_DISABLED_SCHEMES = - proxyAuthDisabled == null ? Set.of() : - Stream.of(proxyAuthDisabled.split(",")) - .map(String::trim) - .filter((s) -> !s.isEmpty()) - .collect(Collectors.toUnmodifiableSet()); - PROXY_AUTH_TUNNEL_DISABLED_SCHEMES = - proxyAuthTunnelDisabled == null ? Set.of() : - Stream.of(proxyAuthTunnelDisabled.split(",")) - .map(String::trim) - .filter((s) -> !s.isEmpty()) - .collect(Collectors.toUnmodifiableSet()); - } - - private static final String WSPACES = " \t\r\n"; - private static final boolean isAllowedForProxy(String name, - List value, - Set disabledSchemes, - Predicate allowedKeys) { - if (!allowedKeys.test(name)) return false; - if (disabledSchemes.isEmpty()) return true; - if (name.equalsIgnoreCase("proxy-authorization")) { - if (value.isEmpty()) return false; - for (String scheme : disabledSchemes) { - int slen = scheme.length(); - for (String v : value) { - int vlen = v.length(); - if (vlen == slen) { - if (v.equalsIgnoreCase(scheme)) { - return false; - } - } else if (vlen > slen) { - if (v.substring(0,slen).equalsIgnoreCase(scheme)) { - int c = v.codePointAt(slen); - if (WSPACES.indexOf(c) > -1 - || Character.isSpaceChar(c) - || Character.isWhitespace(c)) { - return false; - } - } - } - } - } - } - return true; - } - - public static final BiPredicate> PROXY_TUNNEL_FILTER = - (s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_TUNNEL_DISABLED_SCHEMES, - IS_PROXY_HEADER); - public static final BiPredicate> PROXY_FILTER = - (s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_DISABLED_SCHEMES, - ALL_HEADERS); - public static final BiPredicate> NO_PROXY_HEADERS_FILTER = - (n,v) -> Utils.NO_PROXY_HEADER.test(n); - - - public static boolean proxyHasDisabledSchemes(boolean tunnel) { - return tunnel ? ! PROXY_AUTH_TUNNEL_DISABLED_SCHEMES.isEmpty() - : ! PROXY_AUTH_DISABLED_SCHEMES.isEmpty(); - } - - public static ByteBuffer getBuffer() { - return ByteBuffer.allocate(BUFSIZE); - } - - public static Throwable getCompletionCause(Throwable x) { - if (!(x instanceof CompletionException) - && !(x instanceof ExecutionException)) return x; - final Throwable cause = x.getCause(); - if (cause == null) { - throw new InternalError("Unexpected null cause", x); - } - return cause; - } - - public static IOException getIOException(Throwable t) { - if (t instanceof IOException) { - return (IOException) t; - } - Throwable cause = t.getCause(); - if (cause != null) { - return getIOException(cause); - } - return new IOException(t); - } - - private Utils() { } - - /** - * Returns the security permissions required to connect to the proxy, or - * {@code null} if none is required or applicable. - */ - public static URLPermission permissionForProxy(InetSocketAddress proxyAddress) { - if (proxyAddress == null) - return null; - - StringBuilder sb = new StringBuilder(); - sb.append("socket://") - .append(proxyAddress.getHostString()).append(":") - .append(proxyAddress.getPort()); - String urlString = sb.toString(); - return new URLPermission(urlString, "CONNECT"); - } - - /** - * Returns the security permission required for the given details. - */ - public static URLPermission permissionForServer(URI uri, - String method, - Stream headers) { - String urlString = new StringBuilder() - .append(uri.getScheme()).append("://") - .append(uri.getAuthority()) - .append(uri.getPath()).toString(); - - StringBuilder actionStringBuilder = new StringBuilder(method); - String collected = headers.collect(joining(",")); - if (!collected.isEmpty()) { - actionStringBuilder.append(":").append(collected); - } - return new URLPermission(urlString, actionStringBuilder.toString()); - } - - - // ABNF primitives defined in RFC 7230 - private static final boolean[] tchar = new boolean[256]; - private static final boolean[] fieldvchar = new boolean[256]; - - static { - char[] allowedTokenChars = - ("!#$%&'*+-.^_`|~0123456789" + - "abcdefghijklmnopqrstuvwxyz" + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray(); - for (char c : allowedTokenChars) { - tchar[c] = true; - } - for (char c = 0x21; c < 0xFF; c++) { - fieldvchar[c] = true; - } - fieldvchar[0x7F] = false; // a little hole (DEL) in the range - } - - /* - * Validates a RFC 7230 field-name. - */ - public static boolean isValidName(String token) { - for (int i = 0; i < token.length(); i++) { - char c = token.charAt(i); - if (c > 255 || !tchar[c]) { - return false; - } - } - return !token.isEmpty(); - } - - /** - * If the address was created with a domain name, then return - * the domain name string. If created with a literal IP address - * then return null. We do this to avoid doing a reverse lookup - * Used to populate the TLS SNI parameter. So, SNI is only set - * when a domain name was supplied. - */ - public static String getServerName(InetSocketAddress addr) { - String host = addr.getHostString(); - if (IPAddressUtil.textToNumericFormatV4(host) != null) - return null; - if (IPAddressUtil.textToNumericFormatV6(host) != null) - return null; - return host; - } - - /* - * Validates a RFC 7230 field-value. - * - * "Obsolete line folding" rule - * - * obs-fold = CRLF 1*( SP / HTAB ) - * - * is not permitted! - */ - public static boolean isValidValue(String token) { - boolean accepted = true; - for (int i = 0; i < token.length(); i++) { - char c = token.charAt(i); - if (c > 255) { - return false; - } - if (accepted) { - if (c == ' ' || c == '\t') { - accepted = false; - } else if (!fieldvchar[c]) { - return false; // forbidden byte - } - } else { - if (c != ' ' && c != '\t') { - if (fieldvchar[c]) { - accepted = true; - } else { - return false; // forbidden byte - } - } - } - } - return accepted; - } - - public static int getIntegerNetProperty(String name, int defaultValue) { - return AccessController.doPrivileged((PrivilegedAction) () -> - NetProperties.getInteger(name, defaultValue)); - } - - static String getNetProperty(String name) { - return AccessController.doPrivileged((PrivilegedAction) () -> - NetProperties.get(name)); - } - - static boolean getBooleanProperty(String name, boolean def) { - return AccessController.doPrivileged((PrivilegedAction) () -> - Boolean.parseBoolean(System.getProperty(name, String.valueOf(def)))); - } - - public static SSLParameters copySSLParameters(SSLParameters p) { - SSLParameters p1 = new SSLParameters(); - p1.setAlgorithmConstraints(p.getAlgorithmConstraints()); - p1.setCipherSuites(p.getCipherSuites()); - // JDK 8 EXCL START - p1.setEnableRetransmissions(p.getEnableRetransmissions()); - p1.setMaximumPacketSize(p.getMaximumPacketSize()); - // JDK 8 EXCL END - p1.setEndpointIdentificationAlgorithm(p.getEndpointIdentificationAlgorithm()); - p1.setNeedClientAuth(p.getNeedClientAuth()); - String[] protocols = p.getProtocols(); - if (protocols != null) { - p1.setProtocols(protocols.clone()); - } - p1.setSNIMatchers(p.getSNIMatchers()); - p1.setServerNames(p.getServerNames()); - p1.setUseCipherSuitesOrder(p.getUseCipherSuitesOrder()); - p1.setWantClientAuth(p.getWantClientAuth()); - return p1; - } - - /** - * Set limit to position, and position to mark. - */ - public static void flipToMark(ByteBuffer buffer, int mark) { - buffer.limit(buffer.position()); - buffer.position(mark); - } - - public static String stackTrace(Throwable t) { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - String s = null; - try { - PrintStream p = new PrintStream(bos, true, "US-ASCII"); - t.printStackTrace(p); - s = bos.toString("US-ASCII"); - } catch (UnsupportedEncodingException ex) { - throw new InternalError(ex); // Can't happen - } - return s; - } - - /** - * Copies as much of src to dst as possible. - * Return number of bytes copied - */ - public static int copy(ByteBuffer src, ByteBuffer dst) { - int srcLen = src.remaining(); - int dstLen = dst.remaining(); - if (srcLen > dstLen) { - int diff = srcLen - dstLen; - int limit = src.limit(); - src.limit(limit - diff); - dst.put(src); - src.limit(limit); - } else { - dst.put(src); - } - return srcLen - src.remaining(); - } - - /** Threshold beyond which data is no longer copied into the current - * buffer, if that buffer has enough unused space. */ - private static final int COPY_THRESHOLD = 8192; - - /** - * Adds the data from buffersToAdd to currentList. Either 1) appends the - * data from a particular buffer to the last buffer in the list ( if - * there is enough unused space ), or 2) adds it to the list. - * - * @return the number of bytes added - */ - public static long accumulateBuffers(List currentList, - List buffersToAdd) { - long accumulatedBytes = 0; - for (ByteBuffer bufferToAdd : buffersToAdd) { - int remaining = bufferToAdd.remaining(); - if (remaining <= 0) - continue; - int listSize = currentList.size(); - if (listSize == 0) { - currentList.add(bufferToAdd); - accumulatedBytes = remaining; - continue; - } - - ByteBuffer lastBuffer = currentList.get(listSize - 1); - int freeSpace = lastBuffer.capacity() - lastBuffer.limit(); - if (remaining <= COPY_THRESHOLD && freeSpace >= remaining) { - // append the new data to the unused space in the last buffer - int position = lastBuffer.position(); - int limit = lastBuffer.limit(); - lastBuffer.position(limit); - lastBuffer.limit(limit + remaining); - lastBuffer.put(bufferToAdd); - lastBuffer.position(position); - } else { - currentList.add(bufferToAdd); - } - accumulatedBytes += remaining; - } - return accumulatedBytes; - } - - public static ByteBuffer copy(ByteBuffer src) { - ByteBuffer dst = ByteBuffer.allocate(src.remaining()); - dst.put(src); - dst.flip(); - return dst; - } - - public static String dump(Object... objects) { - return Arrays.toString(objects); - } - - public static String stringOf(Collection source) { - // We don't know anything about toString implementation of this - // collection, so let's create an array - return Arrays.toString(source.toArray()); - } - - public static long remaining(ByteBuffer[] bufs) { - long remain = 0; - for (ByteBuffer buf : bufs) { - remain += buf.remaining(); - } - return remain; - } - - public static boolean hasRemaining(List bufs) { - synchronized (bufs) { - for (ByteBuffer buf : bufs) { - if (buf.hasRemaining()) - return true; - } - } - return false; - } - - public static long remaining(List bufs) { - long remain = 0; - synchronized (bufs) { - for (ByteBuffer buf : bufs) { - remain += buf.remaining(); - } - } - return remain; - } - - public static int remaining(List bufs, int max) { - long remain = 0; - synchronized (bufs) { - for (ByteBuffer buf : bufs) { - remain += buf.remaining(); - if (remain > max) { - throw new IllegalArgumentException("too many bytes"); - } - } - } - return (int) remain; - } - - public static long remaining(ByteBufferReference[] refs) { - long remain = 0; - for (ByteBufferReference ref : refs) { - remain += ref.get().remaining(); - } - return remain; - } - - public static int remaining(ByteBufferReference[] refs, int max) { - long remain = 0; - for (ByteBufferReference ref : refs) { - remain += ref.get().remaining(); - if (remain > max) { - throw new IllegalArgumentException("too many bytes"); - } - } - return (int) remain; - } - - public static int remaining(ByteBuffer[] refs, int max) { - long remain = 0; - for (ByteBuffer b : refs) { - remain += b.remaining(); - if (remain > max) { - throw new IllegalArgumentException("too many bytes"); - } - } - return (int) remain; - } - - public static void close(Closeable... closeables) { - for (Closeable c : closeables) { - try { - c.close(); - } catch (IOException ignored) { } - } - } - - // Put all these static 'empty' singletons here - public static final ByteBuffer EMPTY_BYTEBUFFER = ByteBuffer.allocate(0); - public static final ByteBuffer[] EMPTY_BB_ARRAY = new ByteBuffer[0]; - public static final List EMPTY_BB_LIST = List.of(); - public static final ByteBufferReference[] EMPTY_BBR_ARRAY = new ByteBufferReference[0]; - - /** - * Returns a slice of size {@code amount} from the given buffer. If the - * buffer contains more data than {@code amount}, then the slice's capacity - * ( and, but not just, its limit ) is set to {@code amount}. If the buffer - * does not contain more data than {@code amount}, then the slice's capacity - * will be the same as the given buffer's capacity. - */ - public static ByteBuffer sliceWithLimitedCapacity(ByteBuffer buffer, int amount) { - final int index = buffer.position() + amount; - final int limit = buffer.limit(); - if (index != limit) { - // additional data in the buffer - buffer.limit(index); // ensures that the slice does not go beyond - } else { - // no additional data in the buffer - buffer.limit(buffer.capacity()); // allows the slice full capacity - } - - ByteBuffer newb = buffer.slice(); - buffer.position(index); - buffer.limit(limit); // restore the original buffer's limit - newb.limit(amount); // slices limit to amount (capacity may be greater) - return newb; - } - - /** - * Get the Charset from the Content-encoding header. Defaults to - * UTF_8 - */ - public static Charset charsetFrom(HttpHeaders headers) { - String type = headers.firstValue("Content-type") - .orElse("text/html; charset=utf-8"); - int i = type.indexOf(";"); - if (i >= 0) type = type.substring(i+1); - try { - HeaderParser parser = new HeaderParser(type); - String value = parser.findValue("charset"); - if (value == null) return StandardCharsets.UTF_8; - return Charset.forName(value); - } catch (Throwable x) { - Log.logTrace("Can't find charset in \"{0}\" ({1})", type, x); - return StandardCharsets.UTF_8; - } - } - - public static UncheckedIOException unchecked(IOException e) { - return new UncheckedIOException(e); - } - - /** - * Get a logger for debug HTTP traces. - * - * The logger should only be used with levels whose severity is - * {@code <= DEBUG}. By default, this logger will forward all messages - * logged to an internal logger named "jdk.internal.httpclient.debug". - * In addition, if the property -Djdk.internal.httpclient.debug=true is set, - * it will print the messages on stderr. - * The logger will add some decoration to the printed message, in the form of - * {@code :[] [] : } - * - * @param dbgTag A lambda that returns a string that identifies the caller - * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))") - * - * @return A logger for HTTP internal debug traces - */ - public static Logger getDebugLogger(Supplier dbgTag) { - return getDebugLogger(dbgTag, DEBUG); - } - - /** - * Get a logger for debug HTTP traces.The logger should only be used - * with levels whose severity is {@code <= DEBUG}. - * - * By default, this logger will forward all messages logged to an internal - * logger named "jdk.internal.httpclient.debug". - * In addition, if the message severity level is >= to - * the provided {@code errLevel} it will print the messages on stderr. - * The logger will add some decoration to the printed message, in the form of - * {@code :[] [] : } - * - * @apiNote To obtain a logger that will always print things on stderr in - * addition to forwarding to the internal logger, use - * {@code getDebugLogger(this::dbgTag, Level.ALL);}. - * This is also equivalent to calling - * {@code getDebugLogger(this::dbgTag, true);}. - * To obtain a logger that will only forward to the internal logger, - * use {@code getDebugLogger(this::dbgTag, Level.OFF);}. - * This is also equivalent to calling - * {@code getDebugLogger(this::dbgTag, false);}. - * - * @param dbgTag A lambda that returns a string that identifies the caller - * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))") - * @param errLevel The level above which messages will be also printed on - * stderr (in addition to be forwarded to the internal logger). - * - * @return A logger for HTTP internal debug traces - */ - static Logger getDebugLogger(Supplier dbgTag, Level errLevel) { - return DebugLogger.createHttpLogger(dbgTag, Level.OFF, errLevel); - } - - /** - * Get a logger for debug HTTP traces.The logger should only be used - * with levels whose severity is {@code <= DEBUG}. - * - * By default, this logger will forward all messages logged to an internal - * logger named "jdk.internal.httpclient.debug". - * In addition, the provided boolean {@code on==true}, it will print the - * messages on stderr. - * The logger will add some decoration to the printed message, in the form of - * {@code :[] [] : } - * - * @apiNote To obtain a logger that will always print things on stderr in - * addition to forwarding to the internal logger, use - * {@code getDebugLogger(this::dbgTag, true);}. - * This is also equivalent to calling - * {@code getDebugLogger(this::dbgTag, Level.ALL);}. - * To obtain a logger that will only forward to the internal logger, - * use {@code getDebugLogger(this::dbgTag, false);}. - * This is also equivalent to calling - * {@code getDebugLogger(this::dbgTag, Level.OFF);}. - * - * @param dbgTag A lambda that returns a string that identifies the caller - * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))") - * @param on Whether messages should also be printed on - * stderr (in addition to be forwarded to the internal logger). - * - * @return A logger for HTTP internal debug traces - */ - public static Logger getDebugLogger(Supplier dbgTag, boolean on) { - Level errLevel = on ? Level.ALL : Level.OFF; - return getDebugLogger(dbgTag, errLevel); - } - - /** - * Get a logger for debug HPACK traces.The logger should only be used - * with levels whose severity is {@code <= DEBUG}. - * - * By default, this logger will forward all messages logged to an internal - * logger named "jdk.internal.httpclient.hpack.debug". - * In addition, if the message severity level is >= to - * the provided {@code outLevel} it will print the messages on stdout. - * The logger will add some decoration to the printed message, in the form of - * {@code :[] [] : } - * - * @apiNote To obtain a logger that will always print things on stdout in - * addition to forwarding to the internal logger, use - * {@code getHpackLogger(this::dbgTag, Level.ALL);}. - * This is also equivalent to calling - * {@code getHpackLogger(this::dbgTag, true);}. - * To obtain a logger that will only forward to the internal logger, - * use {@code getHpackLogger(this::dbgTag, Level.OFF);}. - * This is also equivalent to calling - * {@code getHpackLogger(this::dbgTag, false);}. - * - * @param dbgTag A lambda that returns a string that identifies the caller - * (e.g: "Http2Connection(SocketTube(3))/hpack.Decoder(3)") - * @param outLevel The level above which messages will be also printed on - * stdout (in addition to be forwarded to the internal logger). - * - * @return A logger for HPACK internal debug traces - */ - public static Logger getHpackLogger(Supplier dbgTag, Level outLevel) { - Level errLevel = Level.OFF; - return DebugLogger.createHpackLogger(dbgTag, outLevel, errLevel); - } - - /** - * Get a logger for debug HPACK traces.The logger should only be used - * with levels whose severity is {@code <= DEBUG}. - * - * By default, this logger will forward all messages logged to an internal - * logger named "jdk.internal.httpclient.hpack.debug". - * In addition, the provided boolean {@code on==true}, it will print the - * messages on stdout. - * The logger will add some decoration to the printed message, in the form of - * {@code :[] [] : } - * - * @apiNote To obtain a logger that will always print things on stdout in - * addition to forwarding to the internal logger, use - * {@code getHpackLogger(this::dbgTag, true);}. - * This is also equivalent to calling - * {@code getHpackLogger(this::dbgTag, Level.ALL);}. - * To obtain a logger that will only forward to the internal logger, - * use {@code getHpackLogger(this::dbgTag, false);}. - * This is also equivalent to calling - * {@code getHpackLogger(this::dbgTag, Level.OFF);}. - * - * @param dbgTag A lambda that returns a string that identifies the caller - * (e.g: "Http2Connection(SocketTube(3))/hpack.Decoder(3)") - * @param on Whether messages should also be printed on - * stdout (in addition to be forwarded to the internal logger). - * - * @return A logger for HPACK internal debug traces - */ - public static Logger getHpackLogger(Supplier dbgTag, boolean on) { - Level outLevel = on ? Level.ALL : Level.OFF; - return getHpackLogger(dbgTag, outLevel); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/ContinuationFrame.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/ContinuationFrame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +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 java.net.http.internal.frame; - -import java.nio.ByteBuffer; -import java.util.List; - -public class ContinuationFrame extends HeaderFrame { - - public static final int TYPE = 0x9; - - public ContinuationFrame(int streamid, int flags, List headerBlocks) { - super(streamid, flags, headerBlocks); - } - - public ContinuationFrame(int streamid, ByteBuffer headersBlock) { - this(streamid, 0, List.of(headersBlock)); - } - - @Override - public int type() { - return TYPE; - } - - @Override - int length() { - return headerLength; - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/DataFrame.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/DataFrame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,112 +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 java.net.http.internal.frame; - -import java.net.http.internal.common.Utils; - -import java.nio.ByteBuffer; -import java.util.List; - -public class DataFrame extends Http2Frame { - - public static final int TYPE = 0x0; - - // Flags - public static final int END_STREAM = 0x1; - public static final int PADDED = 0x8; - - private int padLength; - private final List data; - private final int dataLength; - - public DataFrame(int streamid, int flags, ByteBuffer data) { - this(streamid, flags, List.of(data)); - } - - public DataFrame(int streamid, int flags, List data) { - super(streamid, flags); - this.data = data; - this.dataLength = Utils.remaining(data, Integer.MAX_VALUE); - } - - public DataFrame(int streamid, int flags, List data, int padLength) { - this(streamid, flags, data); - if (padLength > 0) { - setPadLength(padLength); - } - } - - @Override - public int type() { - return TYPE; - } - - @Override - int length() { - return dataLength + (((flags & PADDED) != 0) ? (padLength + 1) : 0); - } - - @Override - public String flagAsString(int flag) { - switch (flag) { - case END_STREAM: - return "END_STREAM"; - case PADDED: - return "PADDED"; - } - return super.flagAsString(flag); - } - - public List getData() { - return data; - } - - public int getDataLength() { - return dataLength; - } - - int getPadLength() { - return padLength; - } - - public void setPadLength(int padLength) { - this.padLength = padLength; - flags |= PADDED; - } - - public int payloadLength() { - // RFC 7540 6.1: - // The entire DATA frame payload is included in flow control, - // including the Pad Length and Padding fields if present - if ((flags & PADDED) != 0) { - return dataLength + (1 + padLength); - } else { - return dataLength; - } - } - - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/ErrorFrame.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/ErrorFrame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,91 +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 java.net.http.internal.frame; - -public abstract class ErrorFrame extends Http2Frame { - - // error codes - public static final int NO_ERROR = 0x0; - public static final int PROTOCOL_ERROR = 0x1; - public static final int INTERNAL_ERROR = 0x2; - public static final int FLOW_CONTROL_ERROR = 0x3; - public static final int SETTINGS_TIMEOUT = 0x4; - public static final int STREAM_CLOSED = 0x5; - public static final int FRAME_SIZE_ERROR = 0x6; - public static final int REFUSED_STREAM = 0x7; - public static final int CANCEL = 0x8; - public static final int COMPRESSION_ERROR = 0x9; - public static final int CONNECT_ERROR = 0xa; - public static final int ENHANCE_YOUR_CALM = 0xb; - public static final int INADEQUATE_SECURITY = 0xc; - public static final int HTTP_1_1_REQUIRED = 0xd; - static final int LAST_ERROR = 0xd; - - static final String[] errorStrings = { - "Not an error", - "Protocol error", - "Internal error", - "Flow control error", - "Settings timeout", - "Stream is closed", - "Frame size error", - "Stream not processed", - "Stream cancelled", - "Compression state not updated", - "TCP Connection error on CONNECT", - "Processing capacity exceeded", - "Negotiated TLS parameters not acceptable", - "Use HTTP/1.1 for request" - }; - - public static String stringForCode(int code) { - if (code < 0) { - throw new IllegalArgumentException(); - } - - if (code > LAST_ERROR) { - return "Error: " + Integer.toString(code); - } else { - return errorStrings[code]; - } - } - - int errorCode; - - public ErrorFrame(int streamid, int flags, int errorCode) { - super(streamid, flags); - this.errorCode = errorCode; - } - - @Override - public String toString() { - return super.toString() + " Error: " + stringForCode(errorCode); - } - - public int getErrorCode() { - return this.errorCode; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/FramesDecoder.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/FramesDecoder.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,545 +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 java.net.http.internal.frame; - -import java.io.IOException; -import java.lang.System.Logger.Level; -import java.nio.ByteBuffer; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.List; -import java.net.http.internal.common.Log; -import java.net.http.internal.common.Utils; -import static java.nio.charset.StandardCharsets.UTF_8; - -/** - * Frames Decoder - *

- * collect buffers until frame decoding is possible, - * all decoded frames are passed to the FrameProcessor callback in order of decoding. - * - * It's a stateful class due to the fact that FramesDecoder stores buffers inside. - * Should be allocated only the single instance per connection. - */ -public class FramesDecoder { - - static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag. - static final System.Logger DEBUG_LOGGER = - Utils.getDebugLogger("FramesDecoder"::toString, DEBUG); - - @FunctionalInterface - public interface FrameProcessor { - void processFrame(Http2Frame frame) throws IOException; - } - - private final FrameProcessor frameProcessor; - private final int maxFrameSize; - - private ByteBuffer currentBuffer; // current buffer either null or hasRemaining - - private final ArrayDeque tailBuffers = new ArrayDeque<>(); - private int tailSize = 0; - - private boolean slicedToDataFrame = false; - - private final List prepareToRelease = new ArrayList<>(); - - // if true - Frame Header was parsed (9 bytes consumed) and subsequent fields have meaning - // otherwise - stopped at frames boundary - private boolean frameHeaderParsed = false; - private int frameLength; - private int frameType; - private int frameFlags; - private int frameStreamid; - private boolean closed; - - /** - * Creates Frame Decoder - * - * @param frameProcessor - callback for decoded frames - */ - public FramesDecoder(FrameProcessor frameProcessor) { - this(frameProcessor, 16 * 1024); - } - - /** - * Creates Frame Decoder - * @param frameProcessor - callback for decoded frames - * @param maxFrameSize - maxFrameSize accepted by this decoder - */ - public FramesDecoder(FrameProcessor frameProcessor, int maxFrameSize) { - this.frameProcessor = frameProcessor; - this.maxFrameSize = Math.min(Math.max(16 * 1024, maxFrameSize), 16 * 1024 * 1024 - 1); - } - - /** Threshold beyond which data is no longer copied into the current buffer, - * if that buffer has enough unused space. */ - private static final int COPY_THRESHOLD = 8192; - - /** - * Adds the data from the given buffer, and performs frame decoding if - * possible. Either 1) appends the data from the given buffer to the - * current buffer ( if there is enough unused space ), or 2) adds it to the - * next buffer in the queue. - * - * If there is enough data to perform frame decoding then, all buffers are - * decoded and the FrameProcessor is invoked. - */ - public void decode(ByteBuffer inBoundBuffer) throws IOException { - if (closed) { - DEBUG_LOGGER.log(Level.DEBUG, "closed: ignoring buffer (%s bytes)", - inBoundBuffer.remaining()); - inBoundBuffer.position(inBoundBuffer.limit()); - return; - } - int remaining = inBoundBuffer.remaining(); - DEBUG_LOGGER.log(Level.DEBUG, "decodes: %d", remaining); - if (remaining > 0) { - if (currentBuffer == null) { - currentBuffer = inBoundBuffer; - } else { - ByteBuffer b = currentBuffer; - if (!tailBuffers.isEmpty()) { - b = tailBuffers.getLast(); - } - - int limit = b.limit(); - int freeSpace = b.capacity() - limit; - if (remaining <= COPY_THRESHOLD && freeSpace >= remaining) { - // append the new data to the unused space in the current buffer - int position = b.position(); - b.position(limit); - b.limit(limit + inBoundBuffer.remaining()); - b.put(inBoundBuffer); - b.position(position); - if (b != currentBuffer) - tailSize += remaining; - DEBUG_LOGGER.log(Level.DEBUG, "copied: %d", remaining); - } else { - DEBUG_LOGGER.log(Level.DEBUG, "added: %d", remaining); - tailBuffers.add(inBoundBuffer); - tailSize += remaining; - } - } - } - DEBUG_LOGGER.log(Level.DEBUG, "Tail size is now: %d, current=", - tailSize, - (currentBuffer == null ? 0 : - currentBuffer.remaining())); - Http2Frame frame; - while ((frame = nextFrame()) != null) { - DEBUG_LOGGER.log(Level.DEBUG, "Got frame: %s", frame); - frameProcessor.processFrame(frame); - frameProcessed(); - } - } - - private Http2Frame nextFrame() throws IOException { - while (true) { - if (currentBuffer == null) { - return null; // no data at all - } - long available = currentBuffer.remaining() + tailSize; - if (!frameHeaderParsed) { - if (available >= Http2Frame.FRAME_HEADER_SIZE) { - parseFrameHeader(); - if (frameLength > maxFrameSize) { - // connection error - return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, - "Frame type("+frameType+") " - +"length("+frameLength - +") exceeds MAX_FRAME_SIZE(" - + maxFrameSize+")"); - } - frameHeaderParsed = true; - } else { - DEBUG_LOGGER.log(Level.DEBUG, - "Not enough data to parse header, needs: %d, has: %d", - Http2Frame.FRAME_HEADER_SIZE, available); - return null; - } - } - available = currentBuffer == null ? 0 : currentBuffer.remaining() + tailSize; - if ((frameLength == 0) || - (currentBuffer != null && available >= frameLength)) { - Http2Frame frame = parseFrameBody(); - frameHeaderParsed = false; - // frame == null means we have to skip this frame and try parse next - if (frame != null) { - return frame; - } - } else { - DEBUG_LOGGER.log(Level.DEBUG, - "Not enough data to parse frame body, needs: %d, has: %d", - frameLength, available); - return null; // no data for the whole frame header - } - } - } - - private void frameProcessed() { - prepareToRelease.clear(); - } - - private void parseFrameHeader() throws IOException { - int x = getInt(); - this.frameLength = (x >>> 8) & 0x00ffffff; - this.frameType = x & 0xff; - this.frameFlags = getByte(); - this.frameStreamid = getInt() & 0x7fffffff; - // R: A reserved 1-bit field. The semantics of this bit are undefined, - // MUST be ignored when receiving. - } - - // move next buffer from tailBuffers to currentBuffer if required - private void nextBuffer() { - if (!currentBuffer.hasRemaining()) { - if (!slicedToDataFrame) { - prepareToRelease.add(currentBuffer); - } - slicedToDataFrame = false; - currentBuffer = tailBuffers.poll(); - if (currentBuffer != null) { - tailSize -= currentBuffer.remaining(); - } - } - } - - public int getByte() { - int res = currentBuffer.get() & 0xff; - nextBuffer(); - return res; - } - - public int getShort() { - if (currentBuffer.remaining() >= 2) { - int res = currentBuffer.getShort() & 0xffff; - nextBuffer(); - return res; - } - int val = getByte(); - val = (val << 8) + getByte(); - return val; - } - - public int getInt() { - if (currentBuffer.remaining() >= 4) { - int res = currentBuffer.getInt(); - nextBuffer(); - return res; - } - int val = getByte(); - val = (val << 8) + getByte(); - val = (val << 8) + getByte(); - val = (val << 8) + getByte(); - return val; - - } - - public byte[] getBytes(int n) { - byte[] bytes = new byte[n]; - int offset = 0; - while (n > 0) { - int length = Math.min(n, currentBuffer.remaining()); - currentBuffer.get(bytes, offset, length); - offset += length; - n -= length; - nextBuffer(); - } - return bytes; - - } - - private List getBuffers(boolean isDataFrame, int bytecount) { - List res = new ArrayList<>(); - while (bytecount > 0) { - int remaining = currentBuffer.remaining(); - int extract = Math.min(remaining, bytecount); - ByteBuffer extractedBuf; - if (isDataFrame) { - extractedBuf = Utils.sliceWithLimitedCapacity(currentBuffer, extract) - .asReadOnlyBuffer(); - slicedToDataFrame = true; - } else { - // Header frames here - // HPACK decoding should performed under lock and immediately after frame decoding. - // in that case it is safe to release original buffer, - // because of sliced buffer has a very short life - extractedBuf = Utils.sliceWithLimitedCapacity(currentBuffer, extract); - } - res.add(extractedBuf); - bytecount -= extract; - nextBuffer(); - } - return res; - } - - public void close(String msg) { - closed = true; - tailBuffers.clear(); - int bytes = tailSize; - ByteBuffer b = currentBuffer; - if (b != null) { - bytes += b.remaining(); - b.position(b.limit()); - } - tailSize = 0; - currentBuffer = null; - DEBUG_LOGGER.log(Level.DEBUG, "closed %s, ignoring %d bytes", msg, bytes); - } - - public void skipBytes(int bytecount) { - while (bytecount > 0) { - int remaining = currentBuffer.remaining(); - int extract = Math.min(remaining, bytecount); - currentBuffer.position(currentBuffer.position() + extract); - bytecount -= remaining; - nextBuffer(); - } - } - - private Http2Frame parseFrameBody() throws IOException { - assert frameHeaderParsed; - switch (frameType) { - case DataFrame.TYPE: - return parseDataFrame(frameLength, frameStreamid, frameFlags); - case HeadersFrame.TYPE: - return parseHeadersFrame(frameLength, frameStreamid, frameFlags); - case PriorityFrame.TYPE: - return parsePriorityFrame(frameLength, frameStreamid, frameFlags); - case ResetFrame.TYPE: - return parseResetFrame(frameLength, frameStreamid, frameFlags); - case SettingsFrame.TYPE: - return parseSettingsFrame(frameLength, frameStreamid, frameFlags); - case PushPromiseFrame.TYPE: - return parsePushPromiseFrame(frameLength, frameStreamid, frameFlags); - case PingFrame.TYPE: - return parsePingFrame(frameLength, frameStreamid, frameFlags); - case GoAwayFrame.TYPE: - return parseGoAwayFrame(frameLength, frameStreamid, frameFlags); - case WindowUpdateFrame.TYPE: - return parseWindowUpdateFrame(frameLength, frameStreamid, frameFlags); - case ContinuationFrame.TYPE: - return parseContinuationFrame(frameLength, frameStreamid, frameFlags); - default: - // RFC 7540 4.1 - // Implementations MUST ignore and discard any frame that has a type that is unknown. - Log.logTrace("Unknown incoming frame type: {0}", frameType); - skipBytes(frameLength); - return null; - } - } - - private Http2Frame parseDataFrame(int frameLength, int streamid, int flags) { - // non-zero stream - if (streamid == 0) { - return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, - "zero streamId for DataFrame"); - } - int padLength = 0; - if ((flags & DataFrame.PADDED) != 0) { - padLength = getByte(); - if (padLength >= frameLength) { - return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, - "the length of the padding is the length of the frame payload or greater"); - } - frameLength--; - } - DataFrame df = new DataFrame(streamid, flags, - getBuffers(true, frameLength - padLength), padLength); - skipBytes(padLength); - return df; - - } - - private Http2Frame parseHeadersFrame(int frameLength, int streamid, int flags) { - // non-zero stream - if (streamid == 0) { - return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, - "zero streamId for HeadersFrame"); - } - int padLength = 0; - if ((flags & HeadersFrame.PADDED) != 0) { - padLength = getByte(); - frameLength--; - } - boolean hasPriority = (flags & HeadersFrame.PRIORITY) != 0; - boolean exclusive = false; - int streamDependency = 0; - int weight = 0; - if (hasPriority) { - int x = getInt(); - exclusive = (x & 0x80000000) != 0; - streamDependency = x & 0x7fffffff; - weight = getByte(); - frameLength -= 5; - } - if(frameLength < padLength) { - return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, - "Padding exceeds the size remaining for the header block"); - } - HeadersFrame hf = new HeadersFrame(streamid, flags, - getBuffers(false, frameLength - padLength), padLength); - skipBytes(padLength); - if (hasPriority) { - hf.setPriority(streamDependency, exclusive, weight); - } - return hf; - } - - private Http2Frame parsePriorityFrame(int frameLength, int streamid, int flags) { - // non-zero stream; no flags - if (streamid == 0) { - return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, - "zero streamId for PriorityFrame"); - } - if(frameLength != 5) { - skipBytes(frameLength); - return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, streamid, - "PriorityFrame length is "+ frameLength+", expected 5"); - } - int x = getInt(); - int weight = getByte(); - return new PriorityFrame(streamid, x & 0x7fffffff, (x & 0x80000000) != 0, weight); - } - - private Http2Frame parseResetFrame(int frameLength, int streamid, int flags) { - // non-zero stream; no flags - if (streamid == 0) { - return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, - "zero streamId for ResetFrame"); - } - if(frameLength != 4) { - return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, - "ResetFrame length is "+ frameLength+", expected 4"); - } - return new ResetFrame(streamid, getInt()); - } - - private Http2Frame parseSettingsFrame(int frameLength, int streamid, int flags) { - // only zero stream - if (streamid != 0) { - return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, - "non-zero streamId for SettingsFrame"); - } - if ((SettingsFrame.ACK & flags) != 0 && frameLength > 0) { - // RFC 7540 6.5 - // Receipt of a SETTINGS frame with the ACK flag set and a length - // field value other than 0 MUST be treated as a connection error - return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, - "ACK SettingsFrame is not empty"); - } - if (frameLength % 6 != 0) { - return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, - "invalid SettingsFrame size: "+frameLength); - } - SettingsFrame sf = new SettingsFrame(flags); - int n = frameLength / 6; - for (int i=0; i 0 && id <= SettingsFrame.MAX_PARAM) { - // a known parameter. Ignore otherwise - sf.setParameter(id, val); // TODO parameters validation - } - } - return sf; - } - - private Http2Frame parsePushPromiseFrame(int frameLength, int streamid, int flags) { - // non-zero stream - if (streamid == 0) { - return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, - "zero streamId for PushPromiseFrame"); - } - int padLength = 0; - if ((flags & PushPromiseFrame.PADDED) != 0) { - padLength = getByte(); - frameLength--; - } - int promisedStream = getInt() & 0x7fffffff; - frameLength -= 4; - if(frameLength < padLength) { - return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, - "Padding exceeds the size remaining for the PushPromiseFrame"); - } - PushPromiseFrame ppf = new PushPromiseFrame(streamid, flags, promisedStream, - getBuffers(false, frameLength - padLength), padLength); - skipBytes(padLength); - return ppf; - } - - private Http2Frame parsePingFrame(int frameLength, int streamid, int flags) { - // only zero stream - if (streamid != 0) { - return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, - "non-zero streamId for PingFrame"); - } - if(frameLength != 8) { - return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, - "PingFrame length is "+ frameLength+", expected 8"); - } - return new PingFrame(flags, getBytes(8)); - } - - private Http2Frame parseGoAwayFrame(int frameLength, int streamid, int flags) { - // only zero stream; no flags - if (streamid != 0) { - return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, - "non-zero streamId for GoAwayFrame"); - } - if (frameLength < 8) { - return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, - "Invalid GoAway frame size"); - } - int lastStream = getInt() & 0x7fffffff; - int errorCode = getInt(); - byte[] debugData = getBytes(frameLength - 8); - if (debugData.length > 0) { - Log.logError("GoAway debugData " + new String(debugData, UTF_8)); - } - return new GoAwayFrame(lastStream, errorCode, debugData); - } - - private Http2Frame parseWindowUpdateFrame(int frameLength, int streamid, int flags) { - // any stream; no flags - if(frameLength != 4) { - return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, - "WindowUpdateFrame length is "+ frameLength+", expected 4"); - } - return new WindowUpdateFrame(streamid, getInt() & 0x7fffffff); - } - - private Http2Frame parseContinuationFrame(int frameLength, int streamid, int flags) { - // non-zero stream; - if (streamid == 0) { - return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, - "zero streamId for ContinuationFrame"); - } - return new ContinuationFrame(streamid, flags, getBuffers(false, frameLength)); - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/FramesEncoder.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/FramesEncoder.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,293 +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 java.net.http.internal.frame; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -/** - * Frames Encoder - * - * Encode framed into ByteBuffers. - * The class is stateless. - */ -public class FramesEncoder { - - - public FramesEncoder() { - } - - public List encodeFrames(List frames) { - List bufs = new ArrayList<>(frames.size() * 2); - for (HeaderFrame f : frames) { - bufs.addAll(encodeFrame(f)); - } - return bufs; - } - - public ByteBuffer encodeConnectionPreface(byte[] preface, SettingsFrame frame) { - final int length = frame.length(); - ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length + preface.length); - buf.put(preface); - putSettingsFrame(buf, frame, length); - buf.flip(); - return buf; - } - - public List encodeFrame(Http2Frame frame) { - switch (frame.type()) { - case DataFrame.TYPE: - return encodeDataFrame((DataFrame) frame); - case HeadersFrame.TYPE: - return encodeHeadersFrame((HeadersFrame) frame); - case PriorityFrame.TYPE: - return encodePriorityFrame((PriorityFrame) frame); - case ResetFrame.TYPE: - return encodeResetFrame((ResetFrame) frame); - case SettingsFrame.TYPE: - return encodeSettingsFrame((SettingsFrame) frame); - case PushPromiseFrame.TYPE: - return encodePushPromiseFrame((PushPromiseFrame) frame); - case PingFrame.TYPE: - return encodePingFrame((PingFrame) frame); - case GoAwayFrame.TYPE: - return encodeGoAwayFrame((GoAwayFrame) frame); - case WindowUpdateFrame.TYPE: - return encodeWindowUpdateFrame((WindowUpdateFrame) frame); - case ContinuationFrame.TYPE: - return encodeContinuationFrame((ContinuationFrame) frame); - default: - throw new UnsupportedOperationException("Not supported frame "+frame.type()+" ("+frame.getClass().getName()+")"); - } - } - - private static final int NO_FLAGS = 0; - private static final int ZERO_STREAM = 0; - - private List encodeDataFrame(DataFrame frame) { - // non-zero stream - assert frame.streamid() != 0; - ByteBuffer buf = encodeDataFrameStart(frame); - if (frame.getFlag(DataFrame.PADDED)) { - return joinWithPadding(buf, frame.getData(), frame.getPadLength()); - } else { - return join(buf, frame.getData()); - } - } - - private ByteBuffer encodeDataFrameStart(DataFrame frame) { - boolean isPadded = frame.getFlag(DataFrame.PADDED); - final int length = frame.getDataLength() + (isPadded ? (frame.getPadLength() + 1) : 0); - ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 1 : 0)); - putHeader(buf, length, DataFrame.TYPE, frame.getFlags(), frame.streamid()); - if (isPadded) { - buf.put((byte) frame.getPadLength()); - } - buf.flip(); - return buf; - } - - private List encodeHeadersFrame(HeadersFrame frame) { - // non-zero stream - assert frame.streamid() != 0; - ByteBuffer buf = encodeHeadersFrameStart(frame); - if (frame.getFlag(HeadersFrame.PADDED)) { - return joinWithPadding(buf, frame.getHeaderBlock(), frame.getPadLength()); - } else { - return join(buf, frame.getHeaderBlock()); - } - } - - private ByteBuffer encodeHeadersFrameStart(HeadersFrame frame) { - boolean isPadded = frame.getFlag(HeadersFrame.PADDED); - boolean hasPriority = frame.getFlag(HeadersFrame.PRIORITY); - final int length = frame.getHeaderLength() + (isPadded ? (frame.getPadLength() + 1) : 0) + (hasPriority ? 5 : 0); - ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 1 : 0) + (hasPriority ? 5 : 0)); - putHeader(buf, length, HeadersFrame.TYPE, frame.getFlags(), frame.streamid()); - if (isPadded) { - buf.put((byte) frame.getPadLength()); - } - if (hasPriority) { - putPriority(buf, frame.getExclusive(), frame.getStreamDependency(), frame.getWeight()); - } - buf.flip(); - return buf; - } - - private List encodePriorityFrame(PriorityFrame frame) { - // non-zero stream; no flags - assert frame.streamid() != 0; - final int length = 5; - ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); - putHeader(buf, length, PriorityFrame.TYPE, NO_FLAGS, frame.streamid()); - putPriority(buf, frame.exclusive(), frame.streamDependency(), frame.weight()); - buf.flip(); - return List.of(buf); - } - - private List encodeResetFrame(ResetFrame frame) { - // non-zero stream; no flags - assert frame.streamid() != 0; - final int length = 4; - ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); - putHeader(buf, length, ResetFrame.TYPE, NO_FLAGS, frame.streamid()); - buf.putInt(frame.getErrorCode()); - buf.flip(); - return List.of(buf); - } - - private List encodeSettingsFrame(SettingsFrame frame) { - // only zero stream - assert frame.streamid() == 0; - final int length = frame.length(); - ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); - putSettingsFrame(buf, frame, length); - buf.flip(); - return List.of(buf); - } - - private List encodePushPromiseFrame(PushPromiseFrame frame) { - // non-zero stream - assert frame.streamid() != 0; - boolean isPadded = frame.getFlag(PushPromiseFrame.PADDED); - final int length = frame.getHeaderLength() + (isPadded ? 5 : 4); - ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 5 : 4)); - putHeader(buf, length, PushPromiseFrame.TYPE, frame.getFlags(), frame.streamid()); - if (isPadded) { - buf.put((byte) frame.getPadLength()); - } - buf.putInt(frame.getPromisedStream()); - buf.flip(); - - if (frame.getFlag(PushPromiseFrame.PADDED)) { - return joinWithPadding(buf, frame.getHeaderBlock(), frame.getPadLength()); - } else { - return join(buf, frame.getHeaderBlock()); - } - } - - private List encodePingFrame(PingFrame frame) { - // only zero stream - assert frame.streamid() == 0; - final int length = 8; - ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); - putHeader(buf, length, PingFrame.TYPE, frame.getFlags(), ZERO_STREAM); - buf.put(frame.getData()); - buf.flip(); - return List.of(buf); - } - - private List encodeGoAwayFrame(GoAwayFrame frame) { - // only zero stream; no flags - assert frame.streamid() == 0; - byte[] debugData = frame.getDebugData(); - final int length = 8 + debugData.length; - ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); - putHeader(buf, length, GoAwayFrame.TYPE, NO_FLAGS, ZERO_STREAM); - buf.putInt(frame.getLastStream()); - buf.putInt(frame.getErrorCode()); - if (debugData.length > 0) { - buf.put(debugData); - } - buf.flip(); - return List.of(buf); - } - - private List encodeWindowUpdateFrame(WindowUpdateFrame frame) { - // any stream; no flags - final int length = 4; - ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); - putHeader(buf, length, WindowUpdateFrame.TYPE, NO_FLAGS, frame.streamid); - buf.putInt(frame.getUpdate()); - buf.flip(); - return List.of(buf); - } - - private List encodeContinuationFrame(ContinuationFrame frame) { - // non-zero stream; - assert frame.streamid() != 0; - final int length = frame.getHeaderLength(); - ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE); - putHeader(buf, length, ContinuationFrame.TYPE, frame.getFlags(), frame.streamid()); - buf.flip(); - return join(buf, frame.getHeaderBlock()); - } - - private List joinWithPadding(ByteBuffer buf, List data, int padLength) { - int len = data.size(); - if (len == 0) return List.of(buf, getPadding(padLength)); - else if (len == 1) return List.of(buf, data.get(0), getPadding(padLength)); - else if (len == 2) return List.of(buf, data.get(0), data.get(1), getPadding(padLength)); - List res = new ArrayList<>(len+2); - res.add(buf); - res.addAll(data); - res.add(getPadding(padLength)); - return res; - } - - private List join(ByteBuffer buf, List data) { - int len = data.size(); - if (len == 0) return List.of(buf); - else if (len == 1) return List.of(buf, data.get(0)); - else if (len == 2) return List.of(buf, data.get(0), data.get(1)); - List joined = new ArrayList<>(len + 1); - joined.add(buf); - joined.addAll(data); - return joined; - } - - private void putSettingsFrame(ByteBuffer buf, SettingsFrame frame, int length) { - // only zero stream; - assert frame.streamid() == 0; - putHeader(buf, length, SettingsFrame.TYPE, frame.getFlags(), ZERO_STREAM); - frame.toByteBuffer(buf); - } - - private void putHeader(ByteBuffer buf, int length, int type, int flags, int streamId) { - int x = (length << 8) + type; - buf.putInt(x); - buf.put((byte) flags); - buf.putInt(streamId); - } - - private void putPriority(ByteBuffer buf, boolean exclusive, int streamDependency, int weight) { - buf.putInt(exclusive ? (1 << 31) + streamDependency : streamDependency); - buf.put((byte) weight); - } - - private ByteBuffer getBuffer(int capacity) { - return ByteBuffer.allocate(capacity); - } - - public ByteBuffer getPadding(int length) { - if (length > 255) { - throw new IllegalArgumentException("Padding too big"); - } - return ByteBuffer.allocate(length); // zeroed! - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/GoAwayFrame.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/GoAwayFrame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,71 +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 java.net.http.internal.frame; - -import static java.nio.charset.StandardCharsets.UTF_8; - -public class GoAwayFrame extends ErrorFrame { - - private final int lastStream; - private final byte[] debugData; - - public static final int TYPE = 0x7; - - - public GoAwayFrame(int lastStream, int errorCode) { - this(lastStream, errorCode, new byte[0]); - } - - public GoAwayFrame(int lastStream, int errorCode, byte[] debugData) { - super(0, 0, errorCode); - this.lastStream = lastStream; - this.debugData = debugData.clone(); - } - - @Override - public int type() { - return TYPE; - } - - @Override - int length() { - return 8 + debugData.length; - } - - @Override - public String toString() { - return super.toString() + " Debugdata: " + new String(debugData, UTF_8); - } - - public int getLastStream() { - return this.lastStream; - } - - public byte[] getDebugData() { - return debugData.clone(); - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/HeaderFrame.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/HeaderFrame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,76 +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 java.net.http.internal.frame; - -import java.net.http.internal.common.Utils; - -import java.nio.ByteBuffer; -import java.util.List; - -/** - * Either a HeadersFrame or a ContinuationFrame - */ -public abstract class HeaderFrame extends Http2Frame { - - final int headerLength; - final List headerBlocks; - - public static final int END_STREAM = 0x1; - public static final int END_HEADERS = 0x4; - - public HeaderFrame(int streamid, int flags, List headerBlocks) { - super(streamid, flags); - this.headerBlocks = headerBlocks; - this.headerLength = Utils.remaining(headerBlocks, Integer.MAX_VALUE); - } - - @Override - public String flagAsString(int flag) { - switch (flag) { - case END_HEADERS: - return "END_HEADERS"; - case END_STREAM: - return "END_STREAM"; - } - return super.flagAsString(flag); - } - - - public List getHeaderBlock() { - return headerBlocks; - } - - int getHeaderLength() { - return headerLength; - } - - /** - * Returns true if this block is the final block of headers. - */ - public boolean endHeaders() { - return getFlag(END_HEADERS); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/HeadersFrame.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/HeadersFrame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,114 +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 java.net.http.internal.frame; - -import java.nio.ByteBuffer; -import java.util.List; - -public class HeadersFrame extends HeaderFrame { - - public static final int TYPE = 0x1; - - // Flags - public static final int END_STREAM = 0x1; - public static final int PADDED = 0x8; - public static final int PRIORITY = 0x20; - - - private int padLength; - private int streamDependency; - private int weight; - private boolean exclusive; - - public HeadersFrame(int streamid, int flags, List headerBlocks, int padLength) { - super(streamid, flags, headerBlocks); - if (padLength > 0) { - setPadLength(padLength); - } - } - - public HeadersFrame(int streamid, int flags, List headerBlocks) { - super(streamid, flags, headerBlocks); - } - - public HeadersFrame(int streamid, int flags, ByteBuffer headerBlock) { - this(streamid, flags, List.of(headerBlock)); - } - - @Override - public int type() { - return TYPE; - } - - @Override - int length() { - return headerLength - + ((flags & PADDED) != 0 ? (1 + padLength) : 0) - + ((flags & PRIORITY) != 0 ? 5 : 0); - } - - @Override - public String flagAsString(int flag) { - switch (flag) { - case END_STREAM: - return "END_STREAM"; - case PADDED: - return "PADDED"; - case PRIORITY: - return "PRIORITY"; - } - return super.flagAsString(flag); - } - - public void setPadLength(int padLength) { - this.padLength = padLength; - flags |= PADDED; - } - - int getPadLength() { - return padLength; - } - - public void setPriority(int streamDependency, boolean exclusive, int weight) { - this.streamDependency = streamDependency; - this.exclusive = exclusive; - this.weight = weight; - this.flags |= PRIORITY; - } - - public int getStreamDependency() { - return streamDependency; - } - - public int getWeight() { - return weight; - } - - public boolean getExclusive() { - return exclusive; - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/Http2Frame.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/Http2Frame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,141 +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 java.net.http.internal.frame; - -/** - * When sending a frame, the length field must be set in sub-class - * by calling computeLength() - */ -public abstract class Http2Frame { - - public static final int FRAME_HEADER_SIZE = 9; - - protected int streamid; - protected int flags; - - public Http2Frame(int streamid, int flags) { - this.streamid = streamid; - this.flags = flags; - } - - public int streamid() { - return streamid; - } - - public void setFlag(int flag) { - flags |= flag; - } - - public int getFlags() { - return flags; - } - - public boolean getFlag(int flag) { - return (flags & flag) != 0; - } - -// public void clearFlag(int flag) { -// flags &= 0xffffffff ^ flag; -// } - - public void streamid(int streamid) { - this.streamid = streamid; - } - - - private String typeAsString() { - return asString(type()); - } - - public int type() { - return -1; // Unknown type - } - - int length() { - return -1; // Unknown length - } - - - public static String asString(int type) { - switch (type) { - case DataFrame.TYPE: - return "DATA"; - case HeadersFrame.TYPE: - return "HEADERS"; - case ContinuationFrame.TYPE: - return "CONTINUATION"; - case ResetFrame.TYPE: - return "RESET"; - case PriorityFrame.TYPE: - return "PRIORITY"; - case SettingsFrame.TYPE: - return "SETTINGS"; - case GoAwayFrame.TYPE: - return "GOAWAY"; - case PingFrame.TYPE: - return "PING"; - case PushPromiseFrame.TYPE: - return "PUSH_PROMISE"; - case WindowUpdateFrame.TYPE: - return "WINDOW_UPDATE"; - default: - return "UNKNOWN"; - } - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(typeAsString()) - .append(": length=") - .append(Integer.toString(length())) - .append(", streamid=") - .append(streamid) - .append(", flags="); - - int f = flags; - int i = 0; - if (f == 0) { - sb.append("0 "); - } else { - while (f != 0) { - if ((f & 1) == 1) { - sb.append(flagAsString(1 << i)) - .append(' '); - } - f = f >> 1; - i++; - } - } - return sb.toString(); - } - - // Override - public String flagAsString(int f) { - return "unknown"; - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/MalformedFrame.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/MalformedFrame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,71 +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 java.net.http.internal.frame; - -public class MalformedFrame extends Http2Frame { - - private int errorCode; - // if errorStream == 0 means Connection Error; RFC 7540 5.4.1 - // if errorStream != 0 means Stream Error; RFC 7540 5.4.2 - private int errorStream; - private String msg; - - /** - * Creates Connection Error malformed frame - * @param errorCode - error code, as specified by RFC 7540 - * @param msg - internal debug message - */ - public MalformedFrame(int errorCode, String msg) { - this(errorCode, 0 , msg); - } - - /** - * Creates Stream Error malformed frame - * @param errorCode - error code, as specified by RFC 7540 - * @param errorStream - id of error stream (RST_FRAME will be send for this stream) - * @param msg - internal debug message - */ - public MalformedFrame(int errorCode, int errorStream, String msg) { - super(0, 0); - this.errorCode = errorCode; - this.errorStream = errorStream; - this.msg = msg; - } - - @Override - public String toString() { - return super.toString() + " MalformedFrame, Error: " + ErrorFrame.stringForCode(errorCode) - + " streamid: " + streamid + " reason: " + msg; - } - - public int getErrorCode() { - return errorCode; - } - - public String getMessage() { - return msg; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/OutgoingHeaders.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/OutgoingHeaders.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +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 java.net.http.internal.frame; - -import java.net.http.HttpHeaders; - -/** - * Contains all parameters for outgoing headers. Is converted to - * HeadersFrame and ContinuationFrames by Http2Connection. - */ -public class OutgoingHeaders extends Http2Frame { - - int streamDependency; - int weight; - boolean exclusive; - T attachment; - - public static final int PRIORITY = 0x20; - - HttpHeaders user, system; - - public OutgoingHeaders(HttpHeaders hdrs1, HttpHeaders hdrs2, T attachment) { - super(0, 0); - this.user = hdrs2; - this.system = hdrs1; - this.attachment = attachment; - } - - public void setPriority(int streamDependency, boolean exclusive, int weight) { - this.streamDependency = streamDependency; - this.exclusive = exclusive; - this.weight = weight; - this.flags |= PRIORITY; - } - - public int getStreamDependency() { - return streamDependency; - } - - public int getWeight() { - return weight; - } - - public boolean getExclusive() { - return exclusive; - } - - public T getAttachment() { - return attachment; - } - - public HttpHeaders getUserHeaders() { - return user; - } - - public HttpHeaders getSystemHeaders() { - return system; - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/PingFrame.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/PingFrame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,67 +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 java.net.http.internal.frame; - -public class PingFrame extends Http2Frame { - - - private final byte[] data; - - public static final int TYPE = 0x6; - - // Flags - public static final int ACK = 0x1; - - public PingFrame(int flags, byte[] data) { - super(0, flags); - assert data.length == 8; - this.data = data.clone(); - } - - @Override - public int type() { - return TYPE; - } - - @Override - int length() { - return 8; - } - - @Override - public String flagAsString(int flag) { - switch (flag) { - case ACK: - return "ACK"; - } - return super.flagAsString(flag); - } - - public byte[] getData() { - return data.clone(); - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/PriorityFrame.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/PriorityFrame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,65 +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 java.net.http.internal.frame; - -public class PriorityFrame extends Http2Frame { - - private final int streamDependency; - private final int weight; - private final boolean exclusive; - - public static final int TYPE = 0x2; - - public PriorityFrame(int streamId, int streamDependency, boolean exclusive, int weight) { - super(streamId, 0); - this.streamDependency = streamDependency; - this.exclusive = exclusive; - this.weight = weight; - } - - @Override - public int type() { - return TYPE; - } - - @Override - int length() { - return 5; - } - - public int streamDependency() { - return streamDependency; - } - - public int weight() { - return weight; - } - - public boolean exclusive() { - return exclusive; - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/PushPromiseFrame.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/PushPromiseFrame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,95 +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 java.net.http.internal.frame; - -import java.nio.ByteBuffer; -import java.util.List; - -public class PushPromiseFrame extends HeaderFrame { - - private int padLength; - private final int promisedStream; - - public static final int TYPE = 0x5; - - // Flags - public static final int END_HEADERS = 0x4; - public static final int PADDED = 0x8; - - public PushPromiseFrame(int streamid, int flags, int promisedStream, List buffers, int padLength) { - super(streamid, flags, buffers); - this.promisedStream = promisedStream; - if(padLength > 0 ) { - setPadLength(padLength); - } - } - - @Override - public int type() { - return TYPE; - } - - @Override - int length() { - return headerLength + ((flags & PADDED) != 0 ? 5 : 4); - } - - @Override - public String toString() { - return super.toString() + " promisedStreamid: " + promisedStream - + " headerLength: " + headerLength; - } - - @Override - public String flagAsString(int flag) { - switch (flag) { - case PADDED: - return "PADDED"; - case END_HEADERS: - return "END_HEADERS"; - } - return super.flagAsString(flag); - } - - public void setPadLength(int padLength) { - this.padLength = padLength; - flags |= PADDED; - } - - public int getPadLength() { - return padLength; - } - - public int getPromisedStream() { - return promisedStream; - } - - @Override - public boolean endHeaders() { - return getFlag(END_HEADERS); - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/ResetFrame.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/ResetFrame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,48 +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 java.net.http.internal.frame; - -public class ResetFrame extends ErrorFrame { - - public static final int TYPE = 0x3; - - // See ErrorFrame for error values - - public ResetFrame(int streamid, int errorCode) { - super(streamid, 0, errorCode); - } - - @Override - public int type() { - return TYPE; - } - - @Override - int length() { - return 4; - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/SettingsFrame.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/SettingsFrame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,166 +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 java.net.http.internal.frame; - -import java.nio.ByteBuffer; -import java.util.Arrays; - -public class SettingsFrame extends Http2Frame { - - private final int[] parameters; - - public static final int TYPE = 0x4; - - // Flags - public static final int ACK = 0x1; - - @Override - public String flagAsString(int flag) { - switch (flag) { - case ACK: - return "ACK"; - } - return super.flagAsString(flag); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(super.toString()) - .append(" Settings: "); - - for (int i = 0; i < MAX_PARAM; i++) { - if (parameters[i] != -1) { - sb.append(name(i)) - .append("=") - .append(Integer.toString(parameters[i])) - .append(' '); - } - } - return sb.toString(); - } - - // Parameters - public static final int HEADER_TABLE_SIZE = 0x1; - public static final int ENABLE_PUSH = 0x2; - public static final int MAX_CONCURRENT_STREAMS = 0x3; - public static final int INITIAL_WINDOW_SIZE = 0x4; - public static final int MAX_FRAME_SIZE = 0x5; - public static final int MAX_HEADER_LIST_SIZE = 0x6; - - private String name(int i) { - switch (i+1) { - case HEADER_TABLE_SIZE: - return "HEADER_TABLE_SIZE"; - case ENABLE_PUSH: - return "ENABLE_PUSH"; - case MAX_CONCURRENT_STREAMS: - return "MAX_CONCURRENT_STREAMS"; - case INITIAL_WINDOW_SIZE: - return "INITIAL_WINDOW_SIZE"; - case MAX_FRAME_SIZE: - return "MAX_FRAME_SIZE"; - case MAX_HEADER_LIST_SIZE: - return "MAX_HEADER_LIST_SIZE"; - } - return "unknown parameter"; - } - public static final int MAX_PARAM = 0x6; - - public SettingsFrame(int flags) { - super(0, flags); - parameters = new int [MAX_PARAM]; - Arrays.fill(parameters, -1); - } - - public SettingsFrame() { - this(0); - } - - public SettingsFrame(SettingsFrame other) { - super(0, other.flags); - parameters = Arrays.copyOf(other.parameters, MAX_PARAM); - } - - @Override - public int type() { - return TYPE; - } - - public int getParameter(int paramID) { - if (paramID > MAX_PARAM) { - throw new IllegalArgumentException("illegal parameter"); - } - return parameters[paramID-1]; - } - - public SettingsFrame setParameter(int paramID, int value) { - if (paramID > MAX_PARAM) { - throw new IllegalArgumentException("illegal parameter"); - } - parameters[paramID-1] = value; - return this; - } - - int length() { - int len = 0; - for (int i : parameters) { - if (i != -1) { - len += 6; - } - } - return len; - } - - void toByteBuffer(ByteBuffer buf) { - for (int i = 0; i < MAX_PARAM; i++) { - if (parameters[i] != -1) { - buf.putShort((short) (i + 1)); - buf.putInt(parameters[i]); - } - } - } - - public byte[] toByteArray() { - byte[] bytes = new byte[length()]; - ByteBuffer buf = ByteBuffer.wrap(bytes); - toByteBuffer(buf); - return bytes; - } - - private static final int K = 1024; - - public static SettingsFrame getDefaultSettings() { - SettingsFrame f = new SettingsFrame(); - // TODO: check these values - f.setParameter(ENABLE_PUSH, 1); - f.setParameter(HEADER_TABLE_SIZE, 4 * K); - f.setParameter(MAX_CONCURRENT_STREAMS, 35); - f.setParameter(INITIAL_WINDOW_SIZE, 64 * K - 1); - f.setParameter(MAX_FRAME_SIZE, 16 * K); - return f; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/frame/WindowUpdateFrame.java --- a/src/java.net.http/share/classes/java/net/http/internal/frame/WindowUpdateFrame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,62 +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 java.net.http.internal.frame; - -public class WindowUpdateFrame extends Http2Frame { - - private final int windowUpdate; - - public static final int TYPE = 0x8; - - public WindowUpdateFrame(int streamid, int windowUpdate) { - super(streamid, 0); - this.windowUpdate = windowUpdate; - } - - @Override - public int type() { - return TYPE; - } - - @Override - int length() { - return 4; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(super.toString()) - .append(" WindowUpdate: ") - .append(windowUpdate); - return sb.toString(); - } - - public int getUpdate() { - return this.windowUpdate; - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/BinaryRepresentationWriter.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/BinaryRepresentationWriter.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,34 +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 java.net.http.internal.hpack; - -import java.nio.ByteBuffer; - -interface BinaryRepresentationWriter { - - boolean write(HeaderTable table, ByteBuffer destination); - - BinaryRepresentationWriter reset(); -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/BulkSizeUpdateWriter.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/BulkSizeUpdateWriter.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,78 +0,0 @@ -/* - * 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 java.net.http.internal.hpack; - -import java.nio.ByteBuffer; -import java.util.Iterator; - -import static java.util.Objects.requireNonNull; - -final class BulkSizeUpdateWriter implements BinaryRepresentationWriter { - - private final SizeUpdateWriter writer = new SizeUpdateWriter(); - private Iterator maxSizes; - private boolean writing; - private boolean configured; - - BulkSizeUpdateWriter maxHeaderTableSizes(Iterable sizes) { - if (configured) { - throw new IllegalStateException("Already configured"); - } - requireNonNull(sizes, "sizes"); - maxSizes = sizes.iterator(); - configured = true; - return this; - } - - @Override - public boolean write(HeaderTable table, ByteBuffer destination) { - if (!configured) { - throw new IllegalStateException("Configure first"); - } - while (true) { - if (writing) { - if (!writer.write(table, destination)) { - return false; - } - writing = false; - } else if (maxSizes.hasNext()) { - writing = true; - writer.reset(); - writer.maxHeaderTableSize(maxSizes.next()); - } else { - configured = false; - return true; - } - } - } - - @Override - public BulkSizeUpdateWriter reset() { - maxSizes = null; - writing = false; - configured = false; - return this; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/Decoder.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/Decoder.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,594 +0,0 @@ -/* - * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package java.net.http.internal.hpack; - -import java.net.http.internal.hpack.HPACK.Logger; -import jdk.internal.vm.annotation.Stable; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.concurrent.atomic.AtomicLong; - -import static java.net.http.internal.hpack.HPACK.Logger.Level.EXTRA; -import static java.net.http.internal.hpack.HPACK.Logger.Level.NORMAL; -import static java.lang.String.format; -import static java.util.Objects.requireNonNull; - -/** - * Decodes headers from their binary representation. - * - *

Typical lifecycle looks like this: - * - *

{@link #Decoder(int) new Decoder} - * ({@link #setMaxCapacity(int) setMaxCapacity}? - * {@link #decode(ByteBuffer, boolean, DecodingCallback) decode})* - * - * @apiNote - * - *

The design intentions behind Decoder were to facilitate flexible and - * incremental style of processing. - * - *

{@code Decoder} does not require a complete header block in a single - * {@code ByteBuffer}. The header block can be spread across many buffers of any - * size and decoded one-by-one the way it makes most sense for the user. This - * way also allows not to limit the size of the header block. - * - *

Headers are delivered to the {@linkplain DecodingCallback callback} as - * soon as they become decoded. Using the callback also gives the user a freedom - * to decide how headers are processed. The callback does not limit the number - * of headers decoded during single decoding operation. - * - * @since 9 - */ -public final class Decoder { - - private final Logger logger; - private static final AtomicLong DECODERS_IDS = new AtomicLong(); - - @Stable - private static final State[] states = new State[256]; - - static { - // To be able to do a quick lookup, each of 256 possibilities are mapped - // to corresponding states. - // - // We can safely do this since patterns 1, 01, 001, 0001, 0000 are - // Huffman prefixes and therefore are inherently not ambiguous. - // - // I do it mainly for better debugging (to not go each time step by step - // through if...else tree). As for performance win for the decoding, I - // believe is negligible. - for (int i = 0; i < states.length; i++) { - if ((i & 0b1000_0000) == 0b1000_0000) { - states[i] = State.INDEXED; - } else if ((i & 0b1100_0000) == 0b0100_0000) { - states[i] = State.LITERAL_WITH_INDEXING; - } else if ((i & 0b1110_0000) == 0b0010_0000) { - states[i] = State.SIZE_UPDATE; - } else if ((i & 0b1111_0000) == 0b0001_0000) { - states[i] = State.LITERAL_NEVER_INDEXED; - } else if ((i & 0b1111_0000) == 0b0000_0000) { - states[i] = State.LITERAL; - } else { - throw new InternalError(String.valueOf(i)); - } - } - } - - private final long id; - private final HeaderTable table; - - private State state = State.READY; - private final IntegerReader integerReader; - private final StringReader stringReader; - private final StringBuilder name; - private final StringBuilder value; - private int intValue; - private boolean firstValueRead; - private boolean firstValueIndex; - private boolean nameHuffmanEncoded; - private boolean valueHuffmanEncoded; - private int capacity; - - /** - * Constructs a {@code Decoder} with the specified initial capacity of the - * header table. - * - *

The value has to be agreed between decoder and encoder out-of-band, - * e.g. by a protocol that uses HPACK - * (see 4.2. Maximum Table Size). - * - * @param capacity - * a non-negative integer - * - * @throws IllegalArgumentException - * if capacity is negative - */ - public Decoder(int capacity) { - id = DECODERS_IDS.incrementAndGet(); - logger = HPACK.getLogger().subLogger("Decoder#" + id); - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("new decoder with maximum table size %s", - capacity)); - } - if (logger.isLoggable(NORMAL)) { - /* To correlate with logging outside HPACK, knowing - hashCode/toString is important */ - logger.log(NORMAL, () -> { - String hashCode = Integer.toHexString( - System.identityHashCode(this)); - return format("toString='%s', identityHashCode=%s", - toString(), hashCode); - }); - } - setMaxCapacity0(capacity); - table = new HeaderTable(capacity, logger.subLogger("HeaderTable")); - integerReader = new IntegerReader(); - stringReader = new StringReader(); - name = new StringBuilder(512); - value = new StringBuilder(1024); - } - - /** - * Sets a maximum capacity of the header table. - * - *

The value has to be agreed between decoder and encoder out-of-band, - * e.g. by a protocol that uses HPACK - * (see 4.2. Maximum Table Size). - * - * @param capacity - * a non-negative integer - * - * @throws IllegalArgumentException - * if capacity is negative - */ - public void setMaxCapacity(int capacity) { - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("setting maximum table size to %s", - capacity)); - } - setMaxCapacity0(capacity); - } - - private void setMaxCapacity0(int capacity) { - if (capacity < 0) { - throw new IllegalArgumentException("capacity >= 0: " + capacity); - } - // FIXME: await capacity update if less than what was prior to it - this.capacity = capacity; - } - - /** - * Decodes a header block from the given buffer to the given callback. - * - *

Suppose a header block is represented by a sequence of - * {@code ByteBuffer}s in the form of {@code Iterator}. And the - * consumer of decoded headers is represented by the callback. Then to - * decode the header block, the following approach might be used: - * - *

{@code
-     * while (buffers.hasNext()) {
-     *     ByteBuffer input = buffers.next();
-     *     decoder.decode(input, callback, !buffers.hasNext());
-     * }
-     * }
- * - *

The decoder reads as much as possible of the header block from the - * given buffer, starting at the buffer's position, and increments its - * position to reflect the bytes read. The buffer's mark and limit will not - * be modified. - * - *

Once the method is invoked with {@code endOfHeaderBlock == true}, the - * current header block is deemed ended, and inconsistencies, if any, are - * reported immediately by throwing an {@code IOException}. - * - *

Each callback method is called only after the implementation has - * processed the corresponding bytes. If the bytes revealed a decoding - * error, the callback method is not called. - * - *

In addition to exceptions thrown directly by the method, any - * exceptions thrown from the {@code callback} will bubble up. - * - * @apiNote The method asks for {@code endOfHeaderBlock} flag instead of - * returning it for two reasons. The first one is that the user of the - * decoder always knows which chunk is the last. The second one is to throw - * the most detailed exception possible, which might be useful for - * diagnosing issues. - * - * @implNote This implementation is not atomic in respect to decoding - * errors. In other words, if the decoding operation has thrown a decoding - * error, the decoder is no longer usable. - * - * @param headerBlock - * the chunk of the header block, may be empty - * @param endOfHeaderBlock - * true if the chunk is the final (or the only one) in the sequence - * - * @param consumer - * the callback - * @throws IOException - * in case of a decoding error - * @throws NullPointerException - * if either headerBlock or consumer are null - */ - public void decode(ByteBuffer headerBlock, - boolean endOfHeaderBlock, - DecodingCallback consumer) throws IOException { - requireNonNull(headerBlock, "headerBlock"); - requireNonNull(consumer, "consumer"); - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("reading %s, end of header block? %s", - headerBlock, endOfHeaderBlock)); - } - while (headerBlock.hasRemaining()) { - proceed(headerBlock, consumer); - } - if (endOfHeaderBlock && state != State.READY) { - logger.log(NORMAL, () -> format("unexpected end of %s representation", - state)); - throw new IOException("Unexpected end of header block"); - } - } - - private void proceed(ByteBuffer input, DecodingCallback action) - throws IOException { - switch (state) { - case READY: - resumeReady(input); - break; - case INDEXED: - resumeIndexed(input, action); - break; - case LITERAL: - resumeLiteral(input, action); - break; - case LITERAL_WITH_INDEXING: - resumeLiteralWithIndexing(input, action); - break; - case LITERAL_NEVER_INDEXED: - resumeLiteralNeverIndexed(input, action); - break; - case SIZE_UPDATE: - resumeSizeUpdate(input, action); - break; - default: - throw new InternalError("Unexpected decoder state: " + state); - } - } - - private void resumeReady(ByteBuffer input) { - int b = input.get(input.position()) & 0xff; // absolute read - State s = states[b]; - if (logger.isLoggable(EXTRA)) { - logger.log(EXTRA, () -> format("next binary representation %s (first byte 0x%02x)", - s, b)); - } - switch (s) { - case INDEXED: - integerReader.configure(7); - state = State.INDEXED; - firstValueIndex = true; - break; - case LITERAL: - state = State.LITERAL; - firstValueIndex = (b & 0b0000_1111) != 0; - if (firstValueIndex) { - integerReader.configure(4); - } - break; - case LITERAL_WITH_INDEXING: - state = State.LITERAL_WITH_INDEXING; - firstValueIndex = (b & 0b0011_1111) != 0; - if (firstValueIndex) { - integerReader.configure(6); - } - break; - case LITERAL_NEVER_INDEXED: - state = State.LITERAL_NEVER_INDEXED; - firstValueIndex = (b & 0b0000_1111) != 0; - if (firstValueIndex) { - integerReader.configure(4); - } - break; - case SIZE_UPDATE: - integerReader.configure(5); - state = State.SIZE_UPDATE; - firstValueIndex = true; - break; - default: - throw new InternalError(String.valueOf(s)); - } - if (!firstValueIndex) { - input.get(); // advance, next stop: "String Literal" - } - } - - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | 1 | Index (7+) | - // +---+---------------------------+ - // - private void resumeIndexed(ByteBuffer input, DecodingCallback action) - throws IOException { - if (!integerReader.read(input)) { - return; - } - intValue = integerReader.get(); - integerReader.reset(); - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("indexed %s", intValue)); - } - try { - HeaderTable.HeaderField f = getHeaderFieldAt(intValue); - action.onIndexed(intValue, f.name, f.value); - } finally { - state = State.READY; - } - } - - private HeaderTable.HeaderField getHeaderFieldAt(int index) - throws IOException - { - HeaderTable.HeaderField f; - try { - f = table.get(index); - } catch (IndexOutOfBoundsException e) { - throw new IOException("header fields table index", e); - } - return f; - } - - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | 0 | 0 | 0 | 0 | Index (4+) | - // +---+---+-----------------------+ - // | H | Value Length (7+) | - // +---+---------------------------+ - // | Value String (Length octets) | - // +-------------------------------+ - // - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | 0 | 0 | 0 | 0 | 0 | - // +---+---+-----------------------+ - // | H | Name Length (7+) | - // +---+---------------------------+ - // | Name String (Length octets) | - // +---+---------------------------+ - // | H | Value Length (7+) | - // +---+---------------------------+ - // | Value String (Length octets) | - // +-------------------------------+ - // - private void resumeLiteral(ByteBuffer input, DecodingCallback action) - throws IOException { - if (!completeReading(input)) { - return; - } - try { - if (firstValueIndex) { - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')", - intValue, value)); - } - HeaderTable.HeaderField f = getHeaderFieldAt(intValue); - action.onLiteral(intValue, f.name, value, valueHuffmanEncoded); - } else { - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')", - name, value)); - } - action.onLiteral(name, nameHuffmanEncoded, value, valueHuffmanEncoded); - } - } finally { - cleanUpAfterReading(); - } - } - - // - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | 0 | 1 | Index (6+) | - // +---+---+-----------------------+ - // | H | Value Length (7+) | - // +---+---------------------------+ - // | Value String (Length octets) | - // +-------------------------------+ - // - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | 0 | 1 | 0 | - // +---+---+-----------------------+ - // | H | Name Length (7+) | - // +---+---------------------------+ - // | Name String (Length octets) | - // +---+---------------------------+ - // | H | Value Length (7+) | - // +---+---------------------------+ - // | Value String (Length octets) | - // +-------------------------------+ - // - private void resumeLiteralWithIndexing(ByteBuffer input, - DecodingCallback action) - throws IOException { - if (!completeReading(input)) { - return; - } - try { - // - // 1. (name, value) will be stored in the table as strings - // 2. Most likely the callback will also create strings from them - // ------------------------------------------------------------------------ - // Let's create those string beforehand (and only once!) to benefit everyone - // - String n; - String v = value.toString(); - if (firstValueIndex) { - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')", - intValue, value)); - } - HeaderTable.HeaderField f = getHeaderFieldAt(intValue); - n = f.name; - action.onLiteralWithIndexing(intValue, n, v, valueHuffmanEncoded); - } else { - n = name.toString(); - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')", - n, value)); - } - action.onLiteralWithIndexing(n, nameHuffmanEncoded, v, valueHuffmanEncoded); - } - table.put(n, v); - } finally { - cleanUpAfterReading(); - } - } - - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | 0 | 0 | 0 | 1 | Index (4+) | - // +---+---+-----------------------+ - // | H | Value Length (7+) | - // +---+---------------------------+ - // | Value String (Length octets) | - // +-------------------------------+ - // - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | 0 | 0 | 0 | 1 | 0 | - // +---+---+-----------------------+ - // | H | Name Length (7+) | - // +---+---------------------------+ - // | Name String (Length octets) | - // +---+---------------------------+ - // | H | Value Length (7+) | - // +---+---------------------------+ - // | Value String (Length octets) | - // +-------------------------------+ - // - private void resumeLiteralNeverIndexed(ByteBuffer input, - DecodingCallback action) - throws IOException { - if (!completeReading(input)) { - return; - } - try { - if (firstValueIndex) { - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')", - intValue, value)); - } - HeaderTable.HeaderField f = getHeaderFieldAt(intValue); - action.onLiteralNeverIndexed(intValue, f.name, value, valueHuffmanEncoded); - } else { - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')", - name, value)); - } - action.onLiteralNeverIndexed(name, nameHuffmanEncoded, value, valueHuffmanEncoded); - } - } finally { - cleanUpAfterReading(); - } - } - - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | 0 | 0 | 1 | Max size (5+) | - // +---+---------------------------+ - // - private void resumeSizeUpdate(ByteBuffer input, - DecodingCallback action) throws IOException { - if (!integerReader.read(input)) { - return; - } - intValue = integerReader.get(); - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("dynamic table size update %s", - intValue)); - } - assert intValue >= 0; - if (intValue > capacity) { - throw new IOException( - format("Received capacity exceeds expected: capacity=%s, expected=%s", - intValue, capacity)); - } - integerReader.reset(); - try { - action.onSizeUpdate(intValue); - table.setMaxSize(intValue); - } finally { - state = State.READY; - } - } - - private boolean completeReading(ByteBuffer input) throws IOException { - if (!firstValueRead) { - if (firstValueIndex) { - if (!integerReader.read(input)) { - return false; - } - intValue = integerReader.get(); - integerReader.reset(); - } else { - if (!stringReader.read(input, name)) { - return false; - } - nameHuffmanEncoded = stringReader.isHuffmanEncoded(); - stringReader.reset(); - } - firstValueRead = true; - return false; - } else { - if (!stringReader.read(input, value)) { - return false; - } - } - valueHuffmanEncoded = stringReader.isHuffmanEncoded(); - stringReader.reset(); - return true; - } - - private void cleanUpAfterReading() { - name.setLength(0); - value.setLength(0); - firstValueRead = false; - state = State.READY; - } - - private enum State { - READY, - INDEXED, - LITERAL_NEVER_INDEXED, - LITERAL, - LITERAL_WITH_INDEXING, - SIZE_UPDATE - } - - HeaderTable getTable() { - return table; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/DecodingCallback.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/DecodingCallback.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,295 +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 java.net.http.internal.hpack; - -import java.nio.ByteBuffer; - -/** - * Delivers results of the {@link Decoder#decode(ByteBuffer, boolean, - * DecodingCallback) decoding operation}. - * - *

Methods of the callback are never called by a decoder with any of the - * arguments being {@code null}. - * - * @apiNote - * - *

The callback provides methods for all possible - * binary representations. - * This could be useful for implementing an intermediary, logging, debugging, - * etc. - * - *

The callback is an interface in order to interoperate with lambdas (in - * the most common use case): - *

{@code
- *     DecodingCallback callback = (name, value) -> System.out.println(name + ", " + value);
- * }
- * - *

Names and values are {@link CharSequence}s rather than {@link String}s in - * order to allow users to decide whether or not they need to create objects. A - * {@code CharSequence} might be used in-place, for example, to be appended to - * an {@link Appendable} (e.g. {@link StringBuilder}) and then discarded. - * - *

That said, if a passed {@code CharSequence} needs to outlast the method - * call, it needs to be copied. - * - * @since 9 - */ -@FunctionalInterface -public interface DecodingCallback { - - /** - * A method the more specific methods of the callback forward their calls - * to. - * - * @param name - * header name - * @param value - * header value - */ - void onDecoded(CharSequence name, CharSequence value); - - /** - * A more finer-grained version of {@link #onDecoded(CharSequence, - * CharSequence)} that also reports on value sensitivity. - * - *

Value sensitivity must be considered, for example, when implementing - * an intermediary. A {@code value} is sensitive if it was represented as Literal Header - * Field Never Indexed. - * - *

It is required that intermediaries MUST use the {@linkplain - * Encoder#header(CharSequence, CharSequence, boolean) same representation} - * for encoding this header field in order to protect its value which is not - * to be put at risk by compressing it. - * - * @implSpec - * - *

The default implementation invokes {@code onDecoded(name, value)}. - * - * @param name - * header name - * @param value - * header value - * @param sensitive - * whether or not the value is sensitive - * - * @see #onLiteralNeverIndexed(int, CharSequence, CharSequence, boolean) - * @see #onLiteralNeverIndexed(CharSequence, boolean, CharSequence, boolean) - */ - default void onDecoded(CharSequence name, - CharSequence value, - boolean sensitive) { - onDecoded(name, value); - } - - /** - * An Indexed - * Header Field decoded. - * - * @implSpec - * - *

The default implementation invokes - * {@code onDecoded(name, value, false)}. - * - * @param index - * index of an entry in the table - * @param name - * header name - * @param value - * header value - */ - default void onIndexed(int index, CharSequence name, CharSequence value) { - onDecoded(name, value, false); - } - - /** - * A Literal - * Header Field without Indexing decoded, where a {@code name} was - * referred by an {@code index}. - * - * @implSpec - * - *

The default implementation invokes - * {@code onDecoded(name, value, false)}. - * - * @param index - * index of an entry in the table - * @param name - * header name - * @param value - * header value - * @param valueHuffman - * if the {@code value} was Huffman encoded - */ - default void onLiteral(int index, - CharSequence name, - CharSequence value, - boolean valueHuffman) { - onDecoded(name, value, false); - } - - /** - * A Literal - * Header Field without Indexing decoded, where both a {@code name} and - * a {@code value} were literal. - * - * @implSpec - * - *

The default implementation invokes - * {@code onDecoded(name, value, false)}. - * - * @param name - * header name - * @param nameHuffman - * if the {@code name} was Huffman encoded - * @param value - * header value - * @param valueHuffman - * if the {@code value} was Huffman encoded - */ - default void onLiteral(CharSequence name, - boolean nameHuffman, - CharSequence value, - boolean valueHuffman) { - onDecoded(name, value, false); - } - - /** - * A Literal - * Header Field Never Indexed decoded, where a {@code name} - * was referred by an {@code index}. - * - * @implSpec - * - *

The default implementation invokes - * {@code onDecoded(name, value, true)}. - * - * @param index - * index of an entry in the table - * @param name - * header name - * @param value - * header value - * @param valueHuffman - * if the {@code value} was Huffman encoded - */ - default void onLiteralNeverIndexed(int index, - CharSequence name, - CharSequence value, - boolean valueHuffman) { - onDecoded(name, value, true); - } - - /** - * A Literal - * Header Field Never Indexed decoded, where both a {@code - * name} and a {@code value} were literal. - * - * @implSpec - * - *

The default implementation invokes - * {@code onDecoded(name, value, true)}. - * - * @param name - * header name - * @param nameHuffman - * if the {@code name} was Huffman encoded - * @param value - * header value - * @param valueHuffman - * if the {@code value} was Huffman encoded - */ - default void onLiteralNeverIndexed(CharSequence name, - boolean nameHuffman, - CharSequence value, - boolean valueHuffman) { - onDecoded(name, value, true); - } - - /** - * A Literal - * Header Field with Incremental Indexing decoded, where a {@code name} - * was referred by an {@code index}. - * - * @implSpec - * - *

The default implementation invokes - * {@code onDecoded(name, value, false)}. - * - * @param index - * index of an entry in the table - * @param name - * header name - * @param value - * header value - * @param valueHuffman - * if the {@code value} was Huffman encoded - */ - default void onLiteralWithIndexing(int index, - CharSequence name, - CharSequence value, - boolean valueHuffman) { - onDecoded(name, value, false); - } - - /** - * A Literal - * Header Field with Incremental Indexing decoded, where both a {@code - * name} and a {@code value} were literal. - * - * @implSpec - * - *

The default implementation invokes - * {@code onDecoded(name, value, false)}. - * - * @param name - * header name - * @param nameHuffman - * if the {@code name} was Huffman encoded - * @param value - * header value - * @param valueHuffman - * if the {@code value} was Huffman encoded - */ - default void onLiteralWithIndexing(CharSequence name, - boolean nameHuffman, - CharSequence value, - boolean valueHuffman) { - onDecoded(name, value, false); - } - - /** - * A Dynamic Table - * Size Update decoded. - * - * @implSpec - * - *

The default implementation does nothing. - * - * @param capacity - * new capacity of the header table - */ - default void onSizeUpdate(int capacity) { } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/Encoder.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/Encoder.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,525 +0,0 @@ -/* - * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package java.net.http.internal.hpack; - -import java.net.http.internal.hpack.HPACK.Logger; - -import java.nio.ByteBuffer; -import java.nio.ReadOnlyBufferException; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; - -import static java.lang.String.format; -import static java.util.Objects.requireNonNull; -import static java.net.http.internal.hpack.HPACK.Logger.Level.EXTRA; -import static java.net.http.internal.hpack.HPACK.Logger.Level.NORMAL; - -/** - * Encodes headers to their binary representation. - * - *

Typical lifecycle looks like this: - * - *

{@link #Encoder(int) new Encoder} - * ({@link #setMaxCapacity(int) setMaxCapacity}? - * {@link #encode(ByteBuffer) encode})* - * - *

Suppose headers are represented by {@code Map>}. - * A supplier and a consumer of {@link ByteBuffer}s in forms of - * {@code Supplier} and {@code Consumer} respectively. - * Then to encode headers, the following approach might be used: - * - *

{@code
- *     for (Map.Entry> h : headers.entrySet()) {
- *         String name = h.getKey();
- *         for (String value : h.getValue()) {
- *             encoder.header(name, value);        // Set up header
- *             boolean encoded;
- *             do {
- *                 ByteBuffer b = buffersSupplier.get();
- *                 encoded = encoder.encode(b);    // Encode the header
- *                 buffersConsumer.accept(b);
- *             } while (!encoded);
- *         }
- *     }
- * }
- * - *

Though the specification does not define - * how an encoder is to be implemented, a default implementation is provided by - * the method {@link #header(CharSequence, CharSequence, boolean)}. - * - *

To provide a custom encoding implementation, {@code Encoder} has to be - * extended. A subclass then can access methods for encoding using specific - * representations (e.g. {@link #literal(int, CharSequence, boolean) literal}, - * {@link #indexed(int) indexed}, etc.) - * - * @apiNote - * - *

An Encoder provides an incremental way of encoding headers. - * {@link #encode(ByteBuffer)} takes a buffer a returns a boolean indicating - * whether, or not, the buffer was sufficiently sized to hold the - * remaining of the encoded representation. - * - *

This way, there's no need to provide a buffer of a specific size, or to - * resize (and copy) the buffer on demand, when the remaining encoded - * representation will not fit in the buffer's remaining space. Instead, an - * array of existing buffers can be used, prepended with a frame that encloses - * the resulting header block afterwards. - * - *

Splitting the encoding operation into header set up and header encoding, - * separates long lived arguments ({@code name}, {@code value}, - * {@code sensitivity}, etc.) from the short lived ones (e.g. {@code buffer}), - * simplifying each operation itself. - * - * @implNote - * - *

The default implementation does not use dynamic table. It reports to a - * coupled Decoder a size update with the value of {@code 0}, and never changes - * it afterwards. - * - * @since 9 - */ -public class Encoder { - - private static final AtomicLong ENCODERS_IDS = new AtomicLong(); - - // TODO: enum: no huffman/smart huffman/always huffman - private static final boolean DEFAULT_HUFFMAN = true; - - private final Logger logger; - private final long id; - private final IndexedWriter indexedWriter = new IndexedWriter(); - private final LiteralWriter literalWriter = new LiteralWriter(); - private final LiteralNeverIndexedWriter literalNeverIndexedWriter - = new LiteralNeverIndexedWriter(); - private final LiteralWithIndexingWriter literalWithIndexingWriter - = new LiteralWithIndexingWriter(); - private final SizeUpdateWriter sizeUpdateWriter = new SizeUpdateWriter(); - private final BulkSizeUpdateWriter bulkSizeUpdateWriter - = new BulkSizeUpdateWriter(); - - private BinaryRepresentationWriter writer; - private final HeaderTable headerTable; - - private boolean encoding; - - private int maxCapacity; - private int currCapacity; - private int lastCapacity; - private long minCapacity; - private boolean capacityUpdate; - private boolean configuredCapacityUpdate; - - /** - * Constructs an {@code Encoder} with the specified maximum capacity of the - * header table. - * - *

The value has to be agreed between decoder and encoder out-of-band, - * e.g. by a protocol that uses HPACK - * (see 4.2. Maximum Table Size). - * - * @param maxCapacity - * a non-negative integer - * - * @throws IllegalArgumentException - * if maxCapacity is negative - */ - public Encoder(int maxCapacity) { - id = ENCODERS_IDS.incrementAndGet(); - this.logger = HPACK.getLogger().subLogger("Encoder#" + id); - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("new encoder with maximum table size %s", - maxCapacity)); - } - if (logger.isLoggable(EXTRA)) { - /* To correlate with logging outside HPACK, knowing - hashCode/toString is important */ - logger.log(EXTRA, () -> { - String hashCode = Integer.toHexString( - System.identityHashCode(this)); - /* Since Encoder can be subclassed hashCode AND identity - hashCode might be different. So let's print both. */ - return format("toString='%s', hashCode=%s, identityHashCode=%s", - toString(), hashCode(), hashCode); - }); - } - if (maxCapacity < 0) { - throw new IllegalArgumentException( - "maxCapacity >= 0: " + maxCapacity); - } - // Initial maximum capacity update mechanics - minCapacity = Long.MAX_VALUE; - currCapacity = -1; - setMaxCapacity0(maxCapacity); - headerTable = new HeaderTable(lastCapacity, logger.subLogger("HeaderTable")); - } - - /** - * Sets up the given header {@code (name, value)}. - * - *

Fixates {@code name} and {@code value} for the duration of encoding. - * - * @param name - * the name - * @param value - * the value - * - * @throws NullPointerException - * if any of the arguments are {@code null} - * @throws IllegalStateException - * if the encoder hasn't fully encoded the previous header, or - * hasn't yet started to encode it - * @see #header(CharSequence, CharSequence, boolean) - */ - public void header(CharSequence name, CharSequence value) - throws IllegalStateException { - header(name, value, false); - } - - /** - * Sets up the given header {@code (name, value)} with possibly sensitive - * value. - * - *

If the {@code value} is sensitive (think security, secrecy, etc.) - * this encoder will compress it using a special representation - * (see 6.2.3. Literal Header Field Never Indexed). - * - *

Fixates {@code name} and {@code value} for the duration of encoding. - * - * @param name - * the name - * @param value - * the value - * @param sensitive - * whether or not the value is sensitive - * - * @throws NullPointerException - * if any of the arguments are {@code null} - * @throws IllegalStateException - * if the encoder hasn't fully encoded the previous header, or - * hasn't yet started to encode it - * @see #header(CharSequence, CharSequence) - * @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean) - */ - public void header(CharSequence name, - CharSequence value, - boolean sensitive) throws IllegalStateException { - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("encoding ('%s', '%s'), sensitive: %s", - name, value, sensitive)); - } - // Arguably a good balance between complexity of implementation and - // efficiency of encoding - requireNonNull(name, "name"); - requireNonNull(value, "value"); - HeaderTable t = getHeaderTable(); - int index = t.indexOf(name, value); - if (index > 0) { - indexed(index); - } else if (index < 0) { - if (sensitive) { - literalNeverIndexed(-index, value, DEFAULT_HUFFMAN); - } else { - literal(-index, value, DEFAULT_HUFFMAN); - } - } else { - if (sensitive) { - literalNeverIndexed(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN); - } else { - literal(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN); - } - } - } - - /** - * Sets a maximum capacity of the header table. - * - *

The value has to be agreed between decoder and encoder out-of-band, - * e.g. by a protocol that uses HPACK - * (see 4.2. Maximum Table Size). - * - *

May be called any number of times after or before a complete header - * has been encoded. - * - *

If the encoder decides to change the actual capacity, an update will - * be encoded before a new encoding operation starts. - * - * @param capacity - * a non-negative integer - * - * @throws IllegalArgumentException - * if capacity is negative - * @throws IllegalStateException - * if the encoder hasn't fully encoded the previous header, or - * hasn't yet started to encode it - */ - public void setMaxCapacity(int capacity) { - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("setting maximum table size to %s", - capacity)); - } - setMaxCapacity0(capacity); - } - - private void setMaxCapacity0(int capacity) { - checkEncoding(); - if (capacity < 0) { - throw new IllegalArgumentException("capacity >= 0: " + capacity); - } - int calculated = calculateCapacity(capacity); - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("actual maximum table size will be %s", - calculated)); - } - if (calculated < 0 || calculated > capacity) { - throw new IllegalArgumentException( - format("0 <= calculated <= capacity: calculated=%s, capacity=%s", - calculated, capacity)); - } - capacityUpdate = true; - // maxCapacity needs to be updated unconditionally, so the encoder - // always has the newest one (in case it decides to update it later - // unsolicitedly) - // Suppose maxCapacity = 4096, and the encoder has decided to use only - // 2048. It later can choose anything else from the region [0, 4096]. - maxCapacity = capacity; - lastCapacity = calculated; - minCapacity = Math.min(minCapacity, lastCapacity); - } - - /** - * Calculates actual capacity to be used by this encoder in response to - * a request to update maximum table size. - * - *

Default implementation does not add anything to the headers table, - * hence this method returns {@code 0}. - * - *

It is an error to return a value {@code c}, where {@code c < 0} or - * {@code c > maxCapacity}. - * - * @param maxCapacity - * upper bound - * - * @return actual capacity - */ - protected int calculateCapacity(int maxCapacity) { - return 0; - } - - /** - * Encodes the {@linkplain #header(CharSequence, CharSequence) set up} - * header into the given buffer. - * - *

The encoder writes as much as possible of the header's binary - * representation into the given buffer, starting at the buffer's position, - * and increments its position to reflect the bytes written. The buffer's - * mark and limit will not be modified. - * - *

Once the method has returned {@code true}, the current header is - * deemed encoded. A new header may be set up. - * - * @param headerBlock - * the buffer to encode the header into, may be empty - * - * @return {@code true} if the current header has been fully encoded, - * {@code false} otherwise - * - * @throws NullPointerException - * if the buffer is {@code null} - * @throws ReadOnlyBufferException - * if this buffer is read-only - * @throws IllegalStateException - * if there is no set up header - */ - public final boolean encode(ByteBuffer headerBlock) { - if (!encoding) { - throw new IllegalStateException("A header hasn't been set up"); - } - if (logger.isLoggable(EXTRA)) { - logger.log(EXTRA, () -> format("writing to %s", headerBlock)); - } - if (!prependWithCapacityUpdate(headerBlock)) { // TODO: log - return false; - } - boolean done = writer.write(headerTable, headerBlock); - if (done) { - writer.reset(); // FIXME: WHY? - encoding = false; - } - return done; - } - - private boolean prependWithCapacityUpdate(ByteBuffer headerBlock) { - if (capacityUpdate) { - if (!configuredCapacityUpdate) { - List sizes = new LinkedList<>(); - if (minCapacity < currCapacity) { - sizes.add((int) minCapacity); - if (minCapacity != lastCapacity) { - sizes.add(lastCapacity); - } - } else if (lastCapacity != currCapacity) { - sizes.add(lastCapacity); - } - bulkSizeUpdateWriter.maxHeaderTableSizes(sizes); - configuredCapacityUpdate = true; - } - boolean done = bulkSizeUpdateWriter.write(headerTable, headerBlock); - if (done) { - minCapacity = lastCapacity; - currCapacity = lastCapacity; - bulkSizeUpdateWriter.reset(); - capacityUpdate = false; - configuredCapacityUpdate = false; - } - return done; - } - return true; - } - - protected final void indexed(int index) throws IndexOutOfBoundsException { - checkEncoding(); - if (logger.isLoggable(EXTRA)) { - logger.log(EXTRA, () -> format("indexed %s", index)); - } - encoding = true; - writer = indexedWriter.index(index); - } - - protected final void literal(int index, - CharSequence value, - boolean useHuffman) - throws IndexOutOfBoundsException { - if (logger.isLoggable(EXTRA)) { - logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')", - index, value)); - } - checkEncoding(); - encoding = true; - writer = literalWriter - .index(index).value(value, useHuffman); - } - - protected final void literal(CharSequence name, - boolean nameHuffman, - CharSequence value, - boolean valueHuffman) { - if (logger.isLoggable(EXTRA)) { - logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')", - name, value)); - } - checkEncoding(); - encoding = true; - writer = literalWriter - .name(name, nameHuffman).value(value, valueHuffman); - } - - protected final void literalNeverIndexed(int index, - CharSequence value, - boolean valueHuffman) - throws IndexOutOfBoundsException { - if (logger.isLoggable(EXTRA)) { - logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')", - index, value)); - } - checkEncoding(); - encoding = true; - writer = literalNeverIndexedWriter - .index(index).value(value, valueHuffman); - } - - protected final void literalNeverIndexed(CharSequence name, - boolean nameHuffman, - CharSequence value, - boolean valueHuffman) { - if (logger.isLoggable(EXTRA)) { - logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')", - name, value)); - } - checkEncoding(); - encoding = true; - writer = literalNeverIndexedWriter - .name(name, nameHuffman).value(value, valueHuffman); - } - - protected final void literalWithIndexing(int index, - CharSequence value, - boolean valueHuffman) - throws IndexOutOfBoundsException { - if (logger.isLoggable(EXTRA)) { - logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')", - index, value)); - } - checkEncoding(); - encoding = true; - writer = literalWithIndexingWriter - .index(index).value(value, valueHuffman); - } - - protected final void literalWithIndexing(CharSequence name, - boolean nameHuffman, - CharSequence value, - boolean valueHuffman) { - if (logger.isLoggable(EXTRA)) { // TODO: include huffman info? - logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')", - name, value)); - } - checkEncoding(); - encoding = true; - writer = literalWithIndexingWriter - .name(name, nameHuffman).value(value, valueHuffman); - } - - protected final void sizeUpdate(int capacity) - throws IllegalArgumentException { - if (logger.isLoggable(EXTRA)) { - logger.log(EXTRA, () -> format("dynamic table size update %s", - capacity)); - } - checkEncoding(); - // Ensure subclass follows the contract - if (capacity > this.maxCapacity) { - throw new IllegalArgumentException( - format("capacity <= maxCapacity: capacity=%s, maxCapacity=%s", - capacity, maxCapacity)); - } - writer = sizeUpdateWriter.maxHeaderTableSize(capacity); - } - - protected final int getMaxCapacity() { - return maxCapacity; - } - - protected final HeaderTable getHeaderTable() { - return headerTable; - } - - protected final void checkEncoding() { // TODO: better name e.g. checkIfEncodingInProgress() - if (encoding) { - throw new IllegalStateException( - "Previous encoding operation hasn't finished yet"); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/HPACK.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/HPACK.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,174 +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 java.net.http.internal.hpack; - -import java.net.http.internal.common.Utils; -import java.net.http.internal.hpack.HPACK.Logger.Level; - -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.Map; -import java.util.ResourceBundle; -import java.util.function.Supplier; - -import static java.lang.String.format; -import static java.util.stream.Collectors.joining; -import static java.net.http.internal.hpack.HPACK.Logger.Level.EXTRA; -import static java.net.http.internal.hpack.HPACK.Logger.Level.NONE; -import static java.net.http.internal.hpack.HPACK.Logger.Level.NORMAL; - -/** - * Internal utilities and stuff. - */ -public final class HPACK { - - private static final RootLogger LOGGER; - private static final Map logLevels = - Map.of("NORMAL", NORMAL, "EXTRA", EXTRA); - - static { - String PROPERTY = "jdk.internal.httpclient.hpack.log.level"; - - String value = AccessController.doPrivileged( - (PrivilegedAction) () -> System.getProperty(PROPERTY)); - - if (value == null) { - LOGGER = new RootLogger(NONE); - } else { - String upperCasedValue = value.toUpperCase(); - Level l = logLevels.get(upperCasedValue); - if (l == null) { - LOGGER = new RootLogger(NONE); - LOGGER.log(System.Logger.Level.INFO, - () -> format("%s value '%s' not recognized (use %s); logging disabled", - PROPERTY, value, logLevels.keySet().stream().collect(joining(", ")))); - } else { - LOGGER = new RootLogger(l); - LOGGER.log(System.Logger.Level.DEBUG, - () -> format("logging level %s", l)); - } - } - } - - public static Logger getLogger() { - return LOGGER; - } - - private HPACK() { } - - /** - * The purpose of this logger is to provide means of diagnosing issues _in - * the HPACK implementation_. It's not a general purpose logger. - */ - // implements System.Logger to make it possible to skip this class - // when looking for the Caller. - public static class Logger implements System.Logger { - - /** - * Log detail level. - */ - public enum Level { - - NONE(0, System.Logger.Level.OFF), - NORMAL(1, System.Logger.Level.DEBUG), - EXTRA(2, System.Logger.Level.TRACE); - - private final int level; - final System.Logger.Level systemLevel; - - Level(int i, System.Logger.Level system) { - level = i; - systemLevel = system; - } - - public final boolean implies(Level other) { - return this.level >= other.level; - } - } - - private final String name; - private final Level level; - private final String path; - private final System.Logger logger; - - private Logger(String path, String name, Level level) { - this(path, name, level, null); - } - - private Logger(String p, String name, Level level, System.Logger logger) { - this.path = p; - this.name = name; - this.level = level; - this.logger = Utils.getHpackLogger(path::toString, level.systemLevel); - } - - public final String getName() { - return name; - } - - @Override - public boolean isLoggable(System.Logger.Level level) { - return logger.isLoggable(level); - } - - @Override - public void log(System.Logger.Level level, ResourceBundle bundle, String msg, Throwable thrown) { - logger.log(level, bundle, msg,thrown); - } - - @Override - public void log(System.Logger.Level level, ResourceBundle bundle, String format, Object... params) { - logger.log(level, bundle, format, params); - } - - /* - * Usual performance trick for logging, reducing performance overhead in - * the case where logging with the specified level is a NOP. - */ - - public boolean isLoggable(Level level) { - return this.level.implies(level); - } - - public void log(Level level, Supplier s) { - if (this.level.implies(level)) { - logger.log(level.systemLevel, s); - } - } - - public Logger subLogger(String name) { - return new Logger(path + "/" + name, name, level); - } - - } - - private static final class RootLogger extends Logger { - - protected RootLogger(Level level) { - super("hpack", "hpack", level); - } - - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/HeaderTable.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/HeaderTable.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,546 +0,0 @@ -/* - * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package java.net.http.internal.hpack; - -import java.net.http.internal.hpack.HPACK.Logger; -import jdk.internal.vm.annotation.Stable; - -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.NoSuchElementException; - -import static java.lang.String.format; -import static java.net.http.internal.hpack.HPACK.Logger.Level.EXTRA; -import static java.net.http.internal.hpack.HPACK.Logger.Level.NORMAL; - -// -// Header Table combined from two tables: static and dynamic. -// -// There is a single address space for index values. Index-aware methods -// correspond to the table as a whole. Size-aware methods only to the dynamic -// part of it. -// -final class HeaderTable { - - @Stable - private static final HeaderField[] staticTable = { - null, // To make index 1-based, instead of 0-based - new HeaderField(":authority"), - new HeaderField(":method", "GET"), - new HeaderField(":method", "POST"), - new HeaderField(":path", "/"), - new HeaderField(":path", "/index.html"), - new HeaderField(":scheme", "http"), - new HeaderField(":scheme", "https"), - new HeaderField(":status", "200"), - new HeaderField(":status", "204"), - new HeaderField(":status", "206"), - new HeaderField(":status", "304"), - new HeaderField(":status", "400"), - new HeaderField(":status", "404"), - new HeaderField(":status", "500"), - new HeaderField("accept-charset"), - new HeaderField("accept-encoding", "gzip, deflate"), - new HeaderField("accept-language"), - new HeaderField("accept-ranges"), - new HeaderField("accept"), - new HeaderField("access-control-allow-origin"), - new HeaderField("age"), - new HeaderField("allow"), - new HeaderField("authorization"), - new HeaderField("cache-control"), - new HeaderField("content-disposition"), - new HeaderField("content-encoding"), - new HeaderField("content-language"), - new HeaderField("content-length"), - new HeaderField("content-location"), - new HeaderField("content-range"), - new HeaderField("content-type"), - new HeaderField("cookie"), - new HeaderField("date"), - new HeaderField("etag"), - new HeaderField("expect"), - new HeaderField("expires"), - new HeaderField("from"), - new HeaderField("host"), - new HeaderField("if-match"), - new HeaderField("if-modified-since"), - new HeaderField("if-none-match"), - new HeaderField("if-range"), - new HeaderField("if-unmodified-since"), - new HeaderField("last-modified"), - new HeaderField("link"), - new HeaderField("location"), - new HeaderField("max-forwards"), - new HeaderField("proxy-authenticate"), - new HeaderField("proxy-authorization"), - new HeaderField("range"), - new HeaderField("referer"), - new HeaderField("refresh"), - new HeaderField("retry-after"), - new HeaderField("server"), - new HeaderField("set-cookie"), - new HeaderField("strict-transport-security"), - new HeaderField("transfer-encoding"), - new HeaderField("user-agent"), - new HeaderField("vary"), - new HeaderField("via"), - new HeaderField("www-authenticate") - }; - - private static final int STATIC_TABLE_LENGTH = staticTable.length - 1; - private static final int ENTRY_SIZE = 32; - private static final Map> staticIndexes; - - static { - staticIndexes = new HashMap<>(STATIC_TABLE_LENGTH); // TODO: Map.of - for (int i = 1; i <= STATIC_TABLE_LENGTH; i++) { - HeaderField f = staticTable[i]; - Map values = staticIndexes - .computeIfAbsent(f.name, k -> new LinkedHashMap<>()); - values.put(f.value, i); - } - } - - private final Logger logger; - private final Table dynamicTable = new Table(0); - private int maxSize; - private int size; - - public HeaderTable(int maxSize, Logger logger) { - this.logger = logger; - setMaxSize(maxSize); - } - - // - // The method returns: - // - // * a positive integer i where i (i = [1..Integer.MAX_VALUE]) is an - // index of an entry with a header (n, v), where n.equals(name) && - // v.equals(value) - // - // * a negative integer j where j (j = [-Integer.MAX_VALUE..-1]) is an - // index of an entry with a header (n, v), where n.equals(name) - // - // * 0 if there's no entry e such that e.getName().equals(name) - // - // The rationale behind this design is to allow to pack more useful data - // into a single invocation, facilitating a single pass where possible - // (the idea is the same as in java.util.Arrays.binarySearch(int[], int)). - // - public int indexOf(CharSequence name, CharSequence value) { - // Invoking toString() will possibly allocate Strings for the sake of - // the search, which doesn't feel right. - String n = name.toString(); - String v = value.toString(); - - // 1. Try exact match in the static region - Map values = staticIndexes.get(n); - if (values != null) { - Integer idx = values.get(v); - if (idx != null) { - return idx; - } - } - // 2. Try exact match in the dynamic region - int didx = dynamicTable.indexOf(n, v); - if (didx > 0) { - return STATIC_TABLE_LENGTH + didx; - } else if (didx < 0) { - if (values != null) { - // 3. Return name match from the static region - return -values.values().iterator().next(); // Iterator allocation - } else { - // 4. Return name match from the dynamic region - return -STATIC_TABLE_LENGTH + didx; - } - } else { - if (values != null) { - // 3. Return name match from the static region - return -values.values().iterator().next(); // Iterator allocation - } else { - return 0; - } - } - } - - public int size() { - return size; - } - - public int maxSize() { - return maxSize; - } - - public int length() { - return STATIC_TABLE_LENGTH + dynamicTable.size(); - } - - HeaderField get(int index) { - checkIndex(index); - if (index <= STATIC_TABLE_LENGTH) { - return staticTable[index]; - } else { - return dynamicTable.get(index - STATIC_TABLE_LENGTH); - } - } - - void put(CharSequence name, CharSequence value) { - // Invoking toString() will possibly allocate Strings. But that's - // unavoidable at this stage. If a CharSequence is going to be stored in - // the table, it must not be mutable (e.g. for the sake of hashing). - put(new HeaderField(name.toString(), value.toString())); - } - - private void put(HeaderField h) { - if (logger.isLoggable(NORMAL)) { - logger.log(NORMAL, () -> format("adding ('%s', '%s')", - h.name, h.value)); - } - int entrySize = sizeOf(h); - if (logger.isLoggable(EXTRA)) { - logger.log(EXTRA, () -> format("size of ('%s', '%s') is %s", - h.name, h.value, entrySize)); - } - while (entrySize > maxSize - size && size != 0) { - if (logger.isLoggable(EXTRA)) { - logger.log(EXTRA, () -> format("insufficient space %s, must evict entry", - (maxSize - size))); - } - evictEntry(); - } - if (entrySize > maxSize - size) { - if (logger.isLoggable(EXTRA)) { - logger.log(EXTRA, () -> format("not adding ('%s, '%s'), too big", - h.name, h.value)); - } - return; - } - size += entrySize; - dynamicTable.add(h); - if (logger.isLoggable(EXTRA)) { - logger.log(EXTRA, () -> format("('%s, '%s') added", h.name, h.value)); - logger.log(EXTRA, this::toString); - } - } - - void setMaxSize(int maxSize) { - if (maxSize < 0) { - throw new IllegalArgumentException( - "maxSize >= 0: maxSize=" + maxSize); - } - while (maxSize < size && size != 0) { - evictEntry(); - } - this.maxSize = maxSize; - int upperBound = (maxSize / ENTRY_SIZE) + 1; - this.dynamicTable.setCapacity(upperBound); - } - - HeaderField evictEntry() { - HeaderField f = dynamicTable.remove(); - int s = sizeOf(f); - this.size -= s; - if (logger.isLoggable(EXTRA)) { - logger.log(EXTRA, () -> format("evicted entry ('%s', '%s') of size %s", - f.name, f.value, s)); - logger.log(EXTRA, this::toString); - } - return f; - } - - @Override - public String toString() { - double used = maxSize == 0 ? 0 : 100 * (((double) size) / maxSize); - return format("dynamic length: %d, full length: %s, used space: %s/%s (%.1f%%)", - dynamicTable.size(), length(), size, maxSize, used); - } - - private int checkIndex(int index) { - int len = length(); - if (index < 1 || index > len) { - throw new IndexOutOfBoundsException( - format("1 <= index <= length(): index=%s, length()=%s", - index, len)); - } - return index; - } - - int sizeOf(HeaderField f) { - return f.name.length() + f.value.length() + ENTRY_SIZE; - } - - // - // Diagnostic information in the form used in the RFC 7541 - // - String getStateString() { - if (size == 0) { - return "empty."; - } - - StringBuilder b = new StringBuilder(); - for (int i = 1, size = dynamicTable.size(); i <= size; i++) { - HeaderField e = dynamicTable.get(i); - b.append(format("[%3d] (s = %3d) %s: %s\n", i, - sizeOf(e), e.name, e.value)); - } - b.append(format(" Table size:%4s", this.size)); - return b.toString(); - } - - // Convert to a Value Object (JDK-8046159)? - static final class HeaderField { - - final String name; - final String value; - - public HeaderField(String name) { - this(name, ""); - } - - public HeaderField(String name, String value) { - this.name = name; - this.value = value; - } - - @Override - public String toString() { - return value.isEmpty() ? name : name + ": " + value; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - HeaderField that = (HeaderField) o; - return name.equals(that.name) && value.equals(that.value); - } - - @Override - public int hashCode() { - return 31 * name.hashCode() + value.hashCode(); - } - } - - // - // To quickly find an index of an entry in the dynamic table with the given - // contents an effective inverse mapping is needed. Here's a simple idea - // behind such a mapping. - // - // # The problem: - // - // We have a queue with an O(1) lookup by index: - // - // get: index -> x - // - // What we want is an O(1) reverse lookup: - // - // indexOf: x -> index - // - // # Solution: - // - // Let's store an inverse mapping in a Map. This have a problem - // that when a new element is added to the queue, all indexes in the map - // become invalid. Namely, the new element is assigned with an index of 1, - // and each index i, i > 1 becomes shifted by 1 to the left: - // - // 1, 1, 2, 3, ... , n-1, n - // - // Re-establishing the invariant would seem to require a pass through the - // map incrementing all indexes (map values) by 1, which is O(n). - // - // The good news is we can do much better then this! - // - // Let's create a single field of type long, called 'counter'. Then each - // time a new element 'x' is added to the queue, a value of this field gets - // incremented. Then the resulting value of the 'counter_x' is then put as a - // value under key 'x' to the map: - // - // map.put(x, counter_x) - // - // It gives us a map that maps an element to a value the counter had at the - // time the element had been added. - // - // In order to retrieve an index of any element 'x' in the queue (at any - // given time) we simply need to subtract the value (the snapshot of the - // counter at the time when the 'x' was added) from the current value of the - // counter. This operation basically answers the question: - // - // How many elements ago 'x' was the tail of the queue? - // - // Which is the same as its index in the queue now. Given, of course, it's - // still in the queue. - // - // I'm pretty sure in a real life long overflow will never happen, so it's - // not too practical to add recalibrating code, but a pedantic person might - // want to do so: - // - // if (counter == Long.MAX_VALUE) { - // recalibrate(); - // } - // - // Where 'recalibrate()' goes through the table doing this: - // - // value -= counter - // - // That's given, of course, the size of the table itself is less than - // Long.MAX_VALUE :-) - // - private static final class Table { - - private final Map> map; - private final CircularBuffer buffer; - private long counter = 1; - - Table(int capacity) { - buffer = new CircularBuffer<>(capacity); - map = new HashMap<>(capacity); - } - - void add(HeaderField f) { - buffer.add(f); - Map values = map.computeIfAbsent(f.name, k -> new HashMap<>()); - values.put(f.value, counter++); - } - - HeaderField get(int index) { - return buffer.get(index - 1); - } - - int indexOf(String name, String value) { - Map values = map.get(name); - if (values == null) { - return 0; - } - Long index = values.get(value); - if (index != null) { - return (int) (counter - index); - } else { - assert !values.isEmpty(); - Long any = values.values().iterator().next(); // Iterator allocation - return -(int) (counter - any); - } - } - - HeaderField remove() { - HeaderField f = buffer.remove(); - Map values = map.get(f.name); - Long index = values.remove(f.value); - assert index != null; - if (values.isEmpty()) { - map.remove(f.name); - } - return f; - } - - int size() { - return buffer.size; - } - - public void setCapacity(int capacity) { - buffer.resize(capacity); - } - } - - // head - // v - // [ ][ ][A][B][C][D][ ][ ][ ] - // ^ - // tail - // - // |<- size ->| (4) - // |<------ capacity ------->| (9) - // - static final class CircularBuffer { - - int tail, head, size, capacity; - Object[] elements; - - CircularBuffer(int capacity) { - this.capacity = capacity; - elements = new Object[capacity]; - } - - void add(E elem) { - if (size == capacity) { - throw new IllegalStateException( - format("No room for '%s': capacity=%s", elem, capacity)); - } - elements[head] = elem; - head = (head + 1) % capacity; - size++; - } - - @SuppressWarnings("unchecked") - E remove() { - if (size == 0) { - throw new NoSuchElementException("Empty"); - } - E elem = (E) elements[tail]; - elements[tail] = null; - tail = (tail + 1) % capacity; - size--; - return elem; - } - - @SuppressWarnings("unchecked") - E get(int index) { - if (index < 0 || index >= size) { - throw new IndexOutOfBoundsException( - format("0 <= index <= capacity: index=%s, capacity=%s", - index, capacity)); - } - int idx = (tail + (size - index - 1)) % capacity; - return (E) elements[idx]; - } - - public void resize(int newCapacity) { - if (newCapacity < size) { - throw new IllegalStateException( - format("newCapacity >= size: newCapacity=%s, size=%s", - newCapacity, size)); - } - - Object[] newElements = new Object[newCapacity]; - - if (tail < head || size == 0) { - System.arraycopy(elements, tail, newElements, 0, size); - } else { - System.arraycopy(elements, tail, newElements, 0, elements.length - tail); - System.arraycopy(elements, 0, newElements, elements.length - tail, head); - } - - elements = newElements; - tail = 0; - head = size; - this.capacity = newCapacity; - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/Huffman.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/Huffman.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,681 +0,0 @@ -/* - * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package java.net.http.internal.hpack; - -import java.io.IOException; -import java.nio.ByteBuffer; - -import static java.lang.String.format; - -/** - * Huffman coding table. - * - *

Instances of this class are safe for use by multiple threads. - * - * @since 9 - */ -public final class Huffman { - - // TODO: check if reset is done in both reader and writer - - static final class Reader { - - private Node curr; // position in the trie - private int len; // length of the path from the root to 'curr' - private int p; // byte probe - - { - reset(); - } - - public void read(ByteBuffer source, - Appendable destination, - boolean isLast) throws IOException { - read(source, destination, true, isLast); - } - - // Takes 'isLast' rather than returns whether the reading is done or - // not, for more informative exceptions. - void read(ByteBuffer source, - Appendable destination, - boolean reportEOS, /* reportEOS is exposed for tests */ - boolean isLast) throws IOException { - Node c = curr; - int l = len; - /* - Since ByteBuffer is itself stateful, its position is - remembered here NOT as a part of Reader's state, - but to set it back in the case of a failure - */ - int pos = source.position(); - - while (source.hasRemaining()) { - int d = source.get(); - for (; p != 0; p >>= 1) { - c = c.getChild(p & d); - l++; - if (c.isLeaf()) { - if (reportEOS && c.isEOSPath) { - throw new IOException("Encountered EOS"); - } - char ch; - try { - ch = c.getChar(); - } catch (IllegalStateException e) { - source.position(pos); // do we need this? - throw new IOException(e); - } - try { - destination.append(ch); - } catch (IOException e) { - source.position(pos); // do we need this? - throw e; - } - c = INSTANCE.root; - l = 0; - } - curr = c; - len = l; - } - resetProbe(); - pos++; - } - if (!isLast) { - return; // it's too early to jump to any conclusions, let's wait - } - if (c.isLeaf()) { - return; // it's perfectly ok, no extra padding bits - } - if (c.isEOSPath && len <= 7) { - return; // it's ok, some extra padding bits - } - if (c.isEOSPath) { - throw new IOException( - "Padding is too long (len=" + len + ") " + - "or unexpected end of data"); - } - throw new IOException( - "Not a EOS prefix padding or unexpected end of data"); - } - - public void reset() { - curr = INSTANCE.root; - len = 0; - resetProbe(); - } - - private void resetProbe() { - p = 0x80; - } - } - - static final class Writer { - - private int pos; // position in 'source' - private int avail = 8; // number of least significant bits available in 'curr' - private int curr; // next byte to put to the destination - private int rem; // number of least significant bits in 'code' yet to be processed - private int code; // current code being written - - private CharSequence source; - private int end; - - public Writer from(CharSequence input, int start, int end) { - if (start < 0 || end < 0 || end > input.length() || start > end) { - throw new IndexOutOfBoundsException( - String.format("input.length()=%s, start=%s, end=%s", - input.length(), start, end)); - } - pos = start; - this.end = end; - this.source = input; - return this; - } - - public boolean write(ByteBuffer destination) { - for (; pos < end; pos++) { - if (rem == 0) { - Code desc = INSTANCE.codeOf(source.charAt(pos)); - rem = desc.length; - code = desc.code; - } - while (rem > 0) { - if (rem < avail) { - curr |= (code << (avail - rem)); - avail -= rem; - rem = 0; - } else { - int c = (curr | (code >>> (rem - avail))); - if (destination.hasRemaining()) { - destination.put((byte) c); - } else { - return false; - } - curr = c; - code <<= (32 - rem + avail); // throw written bits off the cliff (is this Sparta?) - code >>>= (32 - rem + avail); // return to the position - rem -= avail; - curr = 0; - avail = 8; - } - } - } - - if (avail < 8) { // have to pad - if (destination.hasRemaining()) { - destination.put((byte) (curr | (INSTANCE.EOS.code >>> (INSTANCE.EOS.length - avail)))); - avail = 8; - } else { - return false; - } - } - - return true; - } - - public Writer reset() { - source = null; - end = -1; - pos = -1; - avail = 8; - curr = 0; - code = 0; - return this; - } - } - - /** - * Shared instance. - */ - public static final Huffman INSTANCE = new Huffman(); - - private final Code EOS = new Code(0x3fffffff, 30); - private final Code[] codes = new Code[257]; - private final Node root = new Node() { - @Override - public String toString() { return "root"; } - }; - - // TODO: consider builder and immutable trie - private Huffman() { - // @formatter:off - addChar(0, 0x1ff8, 13); - addChar(1, 0x7fffd8, 23); - addChar(2, 0xfffffe2, 28); - addChar(3, 0xfffffe3, 28); - addChar(4, 0xfffffe4, 28); - addChar(5, 0xfffffe5, 28); - addChar(6, 0xfffffe6, 28); - addChar(7, 0xfffffe7, 28); - addChar(8, 0xfffffe8, 28); - addChar(9, 0xffffea, 24); - addChar(10, 0x3ffffffc, 30); - addChar(11, 0xfffffe9, 28); - addChar(12, 0xfffffea, 28); - addChar(13, 0x3ffffffd, 30); - addChar(14, 0xfffffeb, 28); - addChar(15, 0xfffffec, 28); - addChar(16, 0xfffffed, 28); - addChar(17, 0xfffffee, 28); - addChar(18, 0xfffffef, 28); - addChar(19, 0xffffff0, 28); - addChar(20, 0xffffff1, 28); - addChar(21, 0xffffff2, 28); - addChar(22, 0x3ffffffe, 30); - addChar(23, 0xffffff3, 28); - addChar(24, 0xffffff4, 28); - addChar(25, 0xffffff5, 28); - addChar(26, 0xffffff6, 28); - addChar(27, 0xffffff7, 28); - addChar(28, 0xffffff8, 28); - addChar(29, 0xffffff9, 28); - addChar(30, 0xffffffa, 28); - addChar(31, 0xffffffb, 28); - addChar(32, 0x14, 6); - addChar(33, 0x3f8, 10); - addChar(34, 0x3f9, 10); - addChar(35, 0xffa, 12); - addChar(36, 0x1ff9, 13); - addChar(37, 0x15, 6); - addChar(38, 0xf8, 8); - addChar(39, 0x7fa, 11); - addChar(40, 0x3fa, 10); - addChar(41, 0x3fb, 10); - addChar(42, 0xf9, 8); - addChar(43, 0x7fb, 11); - addChar(44, 0xfa, 8); - addChar(45, 0x16, 6); - addChar(46, 0x17, 6); - addChar(47, 0x18, 6); - addChar(48, 0x0, 5); - addChar(49, 0x1, 5); - addChar(50, 0x2, 5); - addChar(51, 0x19, 6); - addChar(52, 0x1a, 6); - addChar(53, 0x1b, 6); - addChar(54, 0x1c, 6); - addChar(55, 0x1d, 6); - addChar(56, 0x1e, 6); - addChar(57, 0x1f, 6); - addChar(58, 0x5c, 7); - addChar(59, 0xfb, 8); - addChar(60, 0x7ffc, 15); - addChar(61, 0x20, 6); - addChar(62, 0xffb, 12); - addChar(63, 0x3fc, 10); - addChar(64, 0x1ffa, 13); - addChar(65, 0x21, 6); - addChar(66, 0x5d, 7); - addChar(67, 0x5e, 7); - addChar(68, 0x5f, 7); - addChar(69, 0x60, 7); - addChar(70, 0x61, 7); - addChar(71, 0x62, 7); - addChar(72, 0x63, 7); - addChar(73, 0x64, 7); - addChar(74, 0x65, 7); - addChar(75, 0x66, 7); - addChar(76, 0x67, 7); - addChar(77, 0x68, 7); - addChar(78, 0x69, 7); - addChar(79, 0x6a, 7); - addChar(80, 0x6b, 7); - addChar(81, 0x6c, 7); - addChar(82, 0x6d, 7); - addChar(83, 0x6e, 7); - addChar(84, 0x6f, 7); - addChar(85, 0x70, 7); - addChar(86, 0x71, 7); - addChar(87, 0x72, 7); - addChar(88, 0xfc, 8); - addChar(89, 0x73, 7); - addChar(90, 0xfd, 8); - addChar(91, 0x1ffb, 13); - addChar(92, 0x7fff0, 19); - addChar(93, 0x1ffc, 13); - addChar(94, 0x3ffc, 14); - addChar(95, 0x22, 6); - addChar(96, 0x7ffd, 15); - addChar(97, 0x3, 5); - addChar(98, 0x23, 6); - addChar(99, 0x4, 5); - addChar(100, 0x24, 6); - addChar(101, 0x5, 5); - addChar(102, 0x25, 6); - addChar(103, 0x26, 6); - addChar(104, 0x27, 6); - addChar(105, 0x6, 5); - addChar(106, 0x74, 7); - addChar(107, 0x75, 7); - addChar(108, 0x28, 6); - addChar(109, 0x29, 6); - addChar(110, 0x2a, 6); - addChar(111, 0x7, 5); - addChar(112, 0x2b, 6); - addChar(113, 0x76, 7); - addChar(114, 0x2c, 6); - addChar(115, 0x8, 5); - addChar(116, 0x9, 5); - addChar(117, 0x2d, 6); - addChar(118, 0x77, 7); - addChar(119, 0x78, 7); - addChar(120, 0x79, 7); - addChar(121, 0x7a, 7); - addChar(122, 0x7b, 7); - addChar(123, 0x7ffe, 15); - addChar(124, 0x7fc, 11); - addChar(125, 0x3ffd, 14); - addChar(126, 0x1ffd, 13); - addChar(127, 0xffffffc, 28); - addChar(128, 0xfffe6, 20); - addChar(129, 0x3fffd2, 22); - addChar(130, 0xfffe7, 20); - addChar(131, 0xfffe8, 20); - addChar(132, 0x3fffd3, 22); - addChar(133, 0x3fffd4, 22); - addChar(134, 0x3fffd5, 22); - addChar(135, 0x7fffd9, 23); - addChar(136, 0x3fffd6, 22); - addChar(137, 0x7fffda, 23); - addChar(138, 0x7fffdb, 23); - addChar(139, 0x7fffdc, 23); - addChar(140, 0x7fffdd, 23); - addChar(141, 0x7fffde, 23); - addChar(142, 0xffffeb, 24); - addChar(143, 0x7fffdf, 23); - addChar(144, 0xffffec, 24); - addChar(145, 0xffffed, 24); - addChar(146, 0x3fffd7, 22); - addChar(147, 0x7fffe0, 23); - addChar(148, 0xffffee, 24); - addChar(149, 0x7fffe1, 23); - addChar(150, 0x7fffe2, 23); - addChar(151, 0x7fffe3, 23); - addChar(152, 0x7fffe4, 23); - addChar(153, 0x1fffdc, 21); - addChar(154, 0x3fffd8, 22); - addChar(155, 0x7fffe5, 23); - addChar(156, 0x3fffd9, 22); - addChar(157, 0x7fffe6, 23); - addChar(158, 0x7fffe7, 23); - addChar(159, 0xffffef, 24); - addChar(160, 0x3fffda, 22); - addChar(161, 0x1fffdd, 21); - addChar(162, 0xfffe9, 20); - addChar(163, 0x3fffdb, 22); - addChar(164, 0x3fffdc, 22); - addChar(165, 0x7fffe8, 23); - addChar(166, 0x7fffe9, 23); - addChar(167, 0x1fffde, 21); - addChar(168, 0x7fffea, 23); - addChar(169, 0x3fffdd, 22); - addChar(170, 0x3fffde, 22); - addChar(171, 0xfffff0, 24); - addChar(172, 0x1fffdf, 21); - addChar(173, 0x3fffdf, 22); - addChar(174, 0x7fffeb, 23); - addChar(175, 0x7fffec, 23); - addChar(176, 0x1fffe0, 21); - addChar(177, 0x1fffe1, 21); - addChar(178, 0x3fffe0, 22); - addChar(179, 0x1fffe2, 21); - addChar(180, 0x7fffed, 23); - addChar(181, 0x3fffe1, 22); - addChar(182, 0x7fffee, 23); - addChar(183, 0x7fffef, 23); - addChar(184, 0xfffea, 20); - addChar(185, 0x3fffe2, 22); - addChar(186, 0x3fffe3, 22); - addChar(187, 0x3fffe4, 22); - addChar(188, 0x7ffff0, 23); - addChar(189, 0x3fffe5, 22); - addChar(190, 0x3fffe6, 22); - addChar(191, 0x7ffff1, 23); - addChar(192, 0x3ffffe0, 26); - addChar(193, 0x3ffffe1, 26); - addChar(194, 0xfffeb, 20); - addChar(195, 0x7fff1, 19); - addChar(196, 0x3fffe7, 22); - addChar(197, 0x7ffff2, 23); - addChar(198, 0x3fffe8, 22); - addChar(199, 0x1ffffec, 25); - addChar(200, 0x3ffffe2, 26); - addChar(201, 0x3ffffe3, 26); - addChar(202, 0x3ffffe4, 26); - addChar(203, 0x7ffffde, 27); - addChar(204, 0x7ffffdf, 27); - addChar(205, 0x3ffffe5, 26); - addChar(206, 0xfffff1, 24); - addChar(207, 0x1ffffed, 25); - addChar(208, 0x7fff2, 19); - addChar(209, 0x1fffe3, 21); - addChar(210, 0x3ffffe6, 26); - addChar(211, 0x7ffffe0, 27); - addChar(212, 0x7ffffe1, 27); - addChar(213, 0x3ffffe7, 26); - addChar(214, 0x7ffffe2, 27); - addChar(215, 0xfffff2, 24); - addChar(216, 0x1fffe4, 21); - addChar(217, 0x1fffe5, 21); - addChar(218, 0x3ffffe8, 26); - addChar(219, 0x3ffffe9, 26); - addChar(220, 0xffffffd, 28); - addChar(221, 0x7ffffe3, 27); - addChar(222, 0x7ffffe4, 27); - addChar(223, 0x7ffffe5, 27); - addChar(224, 0xfffec, 20); - addChar(225, 0xfffff3, 24); - addChar(226, 0xfffed, 20); - addChar(227, 0x1fffe6, 21); - addChar(228, 0x3fffe9, 22); - addChar(229, 0x1fffe7, 21); - addChar(230, 0x1fffe8, 21); - addChar(231, 0x7ffff3, 23); - addChar(232, 0x3fffea, 22); - addChar(233, 0x3fffeb, 22); - addChar(234, 0x1ffffee, 25); - addChar(235, 0x1ffffef, 25); - addChar(236, 0xfffff4, 24); - addChar(237, 0xfffff5, 24); - addChar(238, 0x3ffffea, 26); - addChar(239, 0x7ffff4, 23); - addChar(240, 0x3ffffeb, 26); - addChar(241, 0x7ffffe6, 27); - addChar(242, 0x3ffffec, 26); - addChar(243, 0x3ffffed, 26); - addChar(244, 0x7ffffe7, 27); - addChar(245, 0x7ffffe8, 27); - addChar(246, 0x7ffffe9, 27); - addChar(247, 0x7ffffea, 27); - addChar(248, 0x7ffffeb, 27); - addChar(249, 0xffffffe, 28); - addChar(250, 0x7ffffec, 27); - addChar(251, 0x7ffffed, 27); - addChar(252, 0x7ffffee, 27); - addChar(253, 0x7ffffef, 27); - addChar(254, 0x7fffff0, 27); - addChar(255, 0x3ffffee, 26); - addEOS (256, EOS.code, EOS.length); - // @formatter:on - } - - - /** - * Calculates the number of bytes required to represent the given {@code - * CharSequence} with the Huffman coding. - * - * @param value - * characters - * - * @return number of bytes - * - * @throws NullPointerException - * if the value is null - */ - public int lengthOf(CharSequence value) { - return lengthOf(value, 0, value.length()); - } - - /** - * Calculates the number of bytes required to represent a subsequence of the - * given {@code CharSequence} with the Huffman coding. - * - * @param value - * characters - * @param start - * the start index, inclusive - * @param end - * the end index, exclusive - * - * @return number of bytes - * - * @throws NullPointerException - * if the value is null - * @throws IndexOutOfBoundsException - * if any invocation of {@code value.charAt(i)}, where - * {@code start <= i < end} would throw an IndexOutOfBoundsException - */ - public int lengthOf(CharSequence value, int start, int end) { - int len = 0; - for (int i = start; i < end; i++) { - char c = value.charAt(i); - len += INSTANCE.codeOf(c).length; - } - // Integer division with ceiling, assumption: - assert (len / 8 + (len % 8 != 0 ? 1 : 0)) == (len + 7) / 8 : len; - return (len + 7) / 8; - } - - private void addChar(int c, int code, int bitLength) { - addLeaf(c, code, bitLength, false); - codes[c] = new Code(code, bitLength); - } - - private void addEOS(int c, int code, int bitLength) { - addLeaf(c, code, bitLength, true); - codes[c] = new Code(code, bitLength); - } - - private void addLeaf(int c, int code, int bitLength, boolean isEOS) { - if (bitLength < 1) { - throw new IllegalArgumentException("bitLength < 1"); - } - Node curr = root; - for (int p = 1 << bitLength - 1; p != 0 && !curr.isLeaf(); p = p >> 1) { - curr.isEOSPath |= isEOS; // If it's already true, it can't become false - curr = curr.addChildIfAbsent(p & code); - } - curr.isEOSPath |= isEOS; // The last one needs to have this property as well - if (curr.isLeaf()) { - throw new IllegalStateException("Specified code is already taken"); - } - curr.setChar((char) c); - } - - private Code codeOf(char c) { - if (c > 255) { - throw new IllegalArgumentException("char=" + ((int) c)); - } - return codes[c]; - } - - // - // For debugging/testing purposes - // - Node getRoot() { - return root; - } - - // - // Guarantees: - // - // if (isLeaf() == true) => getChar() is a legal call - // if (isLeaf() == false) => getChild(i) is a legal call (though it can - // return null) - // - static class Node { - - Node left; - Node right; - boolean isEOSPath; - - boolean charIsSet; - char c; - - Node getChild(int selector) { - if (isLeaf()) { - throw new IllegalStateException("This is a leaf node"); - } - Node result = selector == 0 ? left : right; - if (result == null) { - throw new IllegalStateException(format( - "Node doesn't have a child (selector=%s)", selector)); - } - return result; - } - - boolean isLeaf() { - return charIsSet; - } - - char getChar() { - if (!isLeaf()) { - throw new IllegalStateException("This node is not a leaf node"); - } - return c; - } - - void setChar(char c) { - if (charIsSet) { - throw new IllegalStateException( - "This node has been taken already"); - } - if (left != null || right != null) { - throw new IllegalStateException("The node cannot be made " - + "a leaf as it's already has a child"); - } - this.c = c; - charIsSet = true; - } - - Node addChildIfAbsent(int i) { - if (charIsSet) { - throw new IllegalStateException("The node cannot have a child " - + "as it's already a leaf node"); - } - Node child; - if (i == 0) { - if ((child = left) == null) { - child = left = new Node(); - } - } else { - if ((child = right) == null) { - child = right = new Node(); - } - } - return child; - } - - @Override - public String toString() { - if (isLeaf()) { - if (isEOSPath) { - return "EOS"; - } else { - return format("char: (%3s) '%s'", (int) c, c); - } - } - return "/\\"; - } - } - - // TODO: value-based class? - // FIXME: can we re-use Node instead of this class? - private static final class Code { - - final int code; - final int length; - - private Code(int code, int length) { - this.code = code; - this.length = length; - } - - public int getCode() { - return code; - } - - public int getLength() { - return length; - } - - @Override - public String toString() { - long p = 1 << length; - return Long.toBinaryString(code + p).substring(1) - + ", length=" + length; - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/ISO_8859_1.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/ISO_8859_1.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,103 +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 java.net.http.internal.hpack; - -import java.io.IOException; -import java.nio.ByteBuffer; - -// -// Custom implementation of ISO/IEC 8859-1:1998 -// -// The rationale behind this is not to deal with CharsetEncoder/CharsetDecoder, -// basically because it would require wrapping every single CharSequence into a -// CharBuffer and then copying it back. -// -// But why not to give a CharBuffer instead of Appendable? Because I can choose -// an Appendable (e.g. StringBuilder) that adjusts its length when needed and -// therefore not to deal with pre-sized CharBuffers or copying. -// -// The encoding is simple and well known: 1 byte <-> 1 char -// -final class ISO_8859_1 { - - private ISO_8859_1() { } - - public static final class Reader { - - public void read(ByteBuffer source, Appendable destination) - throws IOException { - for (int i = 0, len = source.remaining(); i < len; i++) { - char c = (char) (source.get() & 0xff); - try { - destination.append(c); - } catch (IOException e) { - throw new IOException( - "Error appending to the destination", e); - } - } - } - - public Reader reset() { - return this; - } - } - - public static final class Writer { - - private CharSequence source; - private int pos; - private int end; - - public Writer configure(CharSequence source, int start, int end) { - this.source = source; - this.pos = start; - this.end = end; - return this; - } - - public boolean write(ByteBuffer destination) { - for (; pos < end; pos++) { - char c = source.charAt(pos); - if (c > '\u00FF') { - throw new IllegalArgumentException( - "Illegal ISO-8859-1 char: " + (int) c); - } - if (destination.hasRemaining()) { - destination.put((byte) c); - } else { - return false; - } - } - return true; - } - - public Writer reset() { - source = null; - pos = -1; - end = -1; - return this; - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/IndexNameValueWriter.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/IndexNameValueWriter.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,102 +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 java.net.http.internal.hpack; - -import java.nio.ByteBuffer; - -abstract class IndexNameValueWriter implements BinaryRepresentationWriter { - - private final int pattern; - private final int prefix; - private final IntegerWriter intWriter = new IntegerWriter(); - private final StringWriter nameWriter = new StringWriter(); - private final StringWriter valueWriter = new StringWriter(); - - protected boolean indexedRepresentation; - - private static final int NEW = 0; - private static final int NAME_PART_WRITTEN = 1; - private static final int VALUE_WRITTEN = 2; - - private int state = NEW; - - protected IndexNameValueWriter(int pattern, int prefix) { - this.pattern = pattern; - this.prefix = prefix; - } - - IndexNameValueWriter index(int index) { - indexedRepresentation = true; - intWriter.configure(index, prefix, pattern); - return this; - } - - IndexNameValueWriter name(CharSequence name, boolean useHuffman) { - indexedRepresentation = false; - intWriter.configure(0, prefix, pattern); - nameWriter.configure(name, useHuffman); - return this; - } - - IndexNameValueWriter value(CharSequence value, boolean useHuffman) { - valueWriter.configure(value, useHuffman); - return this; - } - - @Override - public boolean write(HeaderTable table, ByteBuffer destination) { - if (state < NAME_PART_WRITTEN) { - if (indexedRepresentation) { - if (!intWriter.write(destination)) { - return false; - } - } else { - if (!intWriter.write(destination) || - !nameWriter.write(destination)) { - return false; - } - } - state = NAME_PART_WRITTEN; - } - if (state < VALUE_WRITTEN) { - if (!valueWriter.write(destination)) { - return false; - } - state = VALUE_WRITTEN; - } - return state == VALUE_WRITTEN; - } - - @Override - public IndexNameValueWriter reset() { - intWriter.reset(); - if (!indexedRepresentation) { - nameWriter.reset(); - } - valueWriter.reset(); - state = NEW; - return this; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/IndexedWriter.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/IndexedWriter.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +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 java.net.http.internal.hpack; - -import java.nio.ByteBuffer; - -final class IndexedWriter implements BinaryRepresentationWriter { - - private final IntegerWriter intWriter = new IntegerWriter(); - - IndexedWriter() { } - - IndexedWriter index(int index) { - intWriter.configure(index, 7, 0b1000_0000); - return this; - } - - @Override - public boolean write(HeaderTable table, ByteBuffer destination) { - return intWriter.write(destination); - } - - @Override - public BinaryRepresentationWriter reset() { - intWriter.reset(); - return this; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/IntegerReader.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/IntegerReader.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,143 +0,0 @@ -/* - * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package java.net.http.internal.hpack; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Arrays; - -import static java.lang.String.format; - -final class IntegerReader { - - private static final int NEW = 0; - private static final int CONFIGURED = 1; - private static final int FIRST_BYTE_READ = 2; - private static final int DONE = 4; - - private int state = NEW; - - private int N; - private int maxValue; - private int value; - private long r; - private long b = 1; - - public IntegerReader configure(int N) { - return configure(N, Integer.MAX_VALUE); - } - - // - // Why is it important to configure 'maxValue' here. After all we can wait - // for the integer to be fully read and then check it. Can't we? - // - // Two reasons. - // - // 1. Value wraps around long won't be unnoticed. - // 2. It can spit out an exception as soon as it becomes clear there's - // an overflow. Therefore, no need to wait for the value to be fully read. - // - public IntegerReader configure(int N, int maxValue) { - if (state != NEW) { - throw new IllegalStateException("Already configured"); - } - checkPrefix(N); - if (maxValue < 0) { - throw new IllegalArgumentException( - "maxValue >= 0: maxValue=" + maxValue); - } - this.maxValue = maxValue; - this.N = N; - state = CONFIGURED; - return this; - } - - public boolean read(ByteBuffer input) throws IOException { - if (state == NEW) { - throw new IllegalStateException("Configure first"); - } - if (state == DONE) { - return true; - } - if (!input.hasRemaining()) { - return false; - } - if (state == CONFIGURED) { - int max = (2 << (N - 1)) - 1; - int n = input.get() & max; - if (n != max) { - value = n; - state = DONE; - return true; - } else { - r = max; - } - state = FIRST_BYTE_READ; - } - if (state == FIRST_BYTE_READ) { - // variable-length quantity (VLQ) - byte i; - do { - if (!input.hasRemaining()) { - return false; - } - i = input.get(); - long increment = b * (i & 127); - if (r + increment > maxValue) { - throw new IOException(format( - "Integer overflow: maxValue=%,d, value=%,d", - maxValue, r + increment)); - } - r += increment; - b *= 128; - } while ((128 & i) == 128); - - value = (int) r; - state = DONE; - return true; - } - throw new InternalError(Arrays.toString( - new Object[]{state, N, maxValue, value, r, b})); - } - - public int get() throws IllegalStateException { - if (state != DONE) { - throw new IllegalStateException("Has not been fully read yet"); - } - return value; - } - - private static void checkPrefix(int N) { - if (N < 1 || N > 8) { - throw new IllegalArgumentException("1 <= N <= 8: N= " + N); - } - } - - public IntegerReader reset() { - b = 1; - state = NEW; - return this; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/IntegerWriter.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/IntegerWriter.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,117 +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 java.net.http.internal.hpack; - -import java.nio.ByteBuffer; -import java.util.Arrays; - -final class IntegerWriter { - - private static final int NEW = 0; - private static final int CONFIGURED = 1; - private static final int FIRST_BYTE_WRITTEN = 2; - private static final int DONE = 4; - - private int state = NEW; - - private int payload; - private int N; - private int value; - - // - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | | | | | | | | | - // +---+---+---+-------------------+ - // |<--------->|<----------------->| - // payload N=5 - // - // payload is the contents of the left-hand side part of the octet; - // it is truncated to fit into 8-N bits, where 1 <= N <= 8; - // - public IntegerWriter configure(int value, int N, int payload) { - if (state != NEW) { - throw new IllegalStateException("Already configured"); - } - if (value < 0) { - throw new IllegalArgumentException("value >= 0: value=" + value); - } - checkPrefix(N); - this.value = value; - this.N = N; - this.payload = payload & 0xFF & (0xFFFFFFFF << N); - state = CONFIGURED; - return this; - } - - public boolean write(ByteBuffer output) { - if (state == NEW) { - throw new IllegalStateException("Configure first"); - } - if (state == DONE) { - return true; - } - - if (!output.hasRemaining()) { - return false; - } - if (state == CONFIGURED) { - int max = (2 << (N - 1)) - 1; - if (value < max) { - output.put((byte) (payload | value)); - state = DONE; - return true; - } - output.put((byte) (payload | max)); - value -= max; - state = FIRST_BYTE_WRITTEN; - } - if (state == FIRST_BYTE_WRITTEN) { - while (value >= 128 && output.hasRemaining()) { - output.put((byte) (value % 128 + 128)); - value /= 128; - } - if (!output.hasRemaining()) { - return false; - } - output.put((byte) value); - state = DONE; - return true; - } - throw new InternalError(Arrays.toString( - new Object[]{state, payload, N, value})); - } - - private static void checkPrefix(int N) { - if (N < 1 || N > 8) { - throw new IllegalArgumentException("1 <= N <= 8: N= " + N); - } - } - - public IntegerWriter reset() { - state = NEW; - return this; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/LiteralNeverIndexedWriter.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/LiteralNeverIndexedWriter.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,32 +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 java.net.http.internal.hpack; - -final class LiteralNeverIndexedWriter extends IndexNameValueWriter { - - LiteralNeverIndexedWriter() { - super(0b0001_0000, 4); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/LiteralWithIndexingWriter.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/LiteralWithIndexingWriter.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,85 +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 java.net.http.internal.hpack; - -import java.nio.ByteBuffer; - -final class LiteralWithIndexingWriter extends IndexNameValueWriter { - - private boolean tableUpdated; - - private CharSequence name; - private CharSequence value; - private int index; - - LiteralWithIndexingWriter() { - super(0b0100_0000, 6); - } - - @Override - LiteralWithIndexingWriter index(int index) { - super.index(index); - this.index = index; - return this; - } - - @Override - LiteralWithIndexingWriter name(CharSequence name, boolean useHuffman) { - super.name(name, useHuffman); - this.name = name; - return this; - } - - @Override - LiteralWithIndexingWriter value(CharSequence value, boolean useHuffman) { - super.value(value, useHuffman); - this.value = value; - return this; - } - - @Override - public boolean write(HeaderTable table, ByteBuffer destination) { - if (!tableUpdated) { - CharSequence n; - if (indexedRepresentation) { - n = table.get(index).name; - } else { - n = name; - } - table.put(n, value); - tableUpdated = true; - } - return super.write(table, destination); - } - - @Override - public IndexNameValueWriter reset() { - tableUpdated = false; - name = null; - value = null; - index = -1; - return super.reset(); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/LiteralWriter.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/LiteralWriter.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,32 +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 java.net.http.internal.hpack; - -final class LiteralWriter extends IndexNameValueWriter { - - LiteralWriter() { - super(0b0000_0000, 4); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/SizeUpdateWriter.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/SizeUpdateWriter.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,59 +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 java.net.http.internal.hpack; - -import java.nio.ByteBuffer; - -final class SizeUpdateWriter implements BinaryRepresentationWriter { - - private final IntegerWriter intWriter = new IntegerWriter(); - private int maxSize; - private boolean tableUpdated; - - SizeUpdateWriter() { } - - SizeUpdateWriter maxHeaderTableSize(int size) { - intWriter.configure(size, 5, 0b0010_0000); - this.maxSize = size; - return this; - } - - @Override - public boolean write(HeaderTable table, ByteBuffer destination) { - if (!tableUpdated) { - table.setMaxSize(maxSize); - tableUpdated = true; - } - return intWriter.write(destination); - } - - @Override - public BinaryRepresentationWriter reset() { - intWriter.reset(); - maxSize = -1; - tableUpdated = false; - return this; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/StringReader.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/StringReader.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,114 +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 java.net.http.internal.hpack; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Arrays; - -// -// 0 1 2 3 4 5 6 7 -// +---+---+---+---+---+---+---+---+ -// | H | String Length (7+) | -// +---+---------------------------+ -// | String Data (Length octets) | -// +-------------------------------+ -// -final class StringReader { - - private static final int NEW = 0; - private static final int FIRST_BYTE_READ = 1; - private static final int LENGTH_READ = 2; - private static final int DONE = 4; - - private final IntegerReader intReader = new IntegerReader(); - private final Huffman.Reader huffmanReader = new Huffman.Reader(); - private final ISO_8859_1.Reader plainReader = new ISO_8859_1.Reader(); - - private int state = NEW; - - private boolean huffman; - private int remainingLength; - - boolean read(ByteBuffer input, Appendable output) throws IOException { - if (state == DONE) { - return true; - } - if (!input.hasRemaining()) { - return false; - } - if (state == NEW) { - int p = input.position(); - huffman = (input.get(p) & 0b10000000) != 0; - state = FIRST_BYTE_READ; - intReader.configure(7); - } - if (state == FIRST_BYTE_READ) { - boolean lengthRead = intReader.read(input); - if (!lengthRead) { - return false; - } - remainingLength = intReader.get(); - state = LENGTH_READ; - } - if (state == LENGTH_READ) { - boolean isLast = input.remaining() >= remainingLength; - int oldLimit = input.limit(); - if (isLast) { - input.limit(input.position() + remainingLength); - } - remainingLength -= Math.min(input.remaining(), remainingLength); - if (huffman) { - huffmanReader.read(input, output, isLast); - } else { - plainReader.read(input, output); - } - if (isLast) { - input.limit(oldLimit); - state = DONE; - } - return isLast; - } - throw new InternalError(Arrays.toString( - new Object[]{state, huffman, remainingLength})); - } - - boolean isHuffmanEncoded() { - if (state < FIRST_BYTE_READ) { - throw new IllegalStateException("Has not been fully read yet"); - } - return huffman; - } - - void reset() { - if (huffman) { - huffmanReader.reset(); - } else { - plainReader.reset(); - } - intReader.reset(); - state = NEW; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/StringWriter.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/StringWriter.java Wed Feb 07 15:46:30 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 java.net.http.internal.hpack; - -import java.nio.ByteBuffer; -import java.util.Arrays; - -// -// 0 1 2 3 4 5 6 7 -// +---+---+---+---+---+---+---+---+ -// | H | String Length (7+) | -// +---+---------------------------+ -// | String Data (Length octets) | -// +-------------------------------+ -// -// StringWriter does not require a notion of endOfInput (isLast) in 'write' -// methods due to the nature of string representation in HPACK. Namely, the -// length of the string is put before string's contents. Therefore the length is -// always known beforehand. -// -// Expected use: -// -// configure write* (reset configure write*)* -// -final class StringWriter { - - private static final int NEW = 0; - private static final int CONFIGURED = 1; - private static final int LENGTH_WRITTEN = 2; - private static final int DONE = 4; - - private final IntegerWriter intWriter = new IntegerWriter(); - private final Huffman.Writer huffmanWriter = new Huffman.Writer(); - private final ISO_8859_1.Writer plainWriter = new ISO_8859_1.Writer(); - - private int state = NEW; - private boolean huffman; - - StringWriter configure(CharSequence input, boolean huffman) { - return configure(input, 0, input.length(), huffman); - } - - StringWriter configure(CharSequence input, - int start, - int end, - boolean huffman) { - if (start < 0 || end < 0 || end > input.length() || start > end) { - throw new IndexOutOfBoundsException( - String.format("input.length()=%s, start=%s, end=%s", - input.length(), start, end)); - } - if (!huffman) { - plainWriter.configure(input, start, end); - intWriter.configure(end - start, 7, 0b0000_0000); - } else { - huffmanWriter.from(input, start, end); - intWriter.configure(Huffman.INSTANCE.lengthOf(input, start, end), - 7, 0b1000_0000); - } - - this.huffman = huffman; - state = CONFIGURED; - return this; - } - - boolean write(ByteBuffer output) { - if (state == DONE) { - return true; - } - if (state == NEW) { - throw new IllegalStateException("Configure first"); - } - if (!output.hasRemaining()) { - return false; - } - if (state == CONFIGURED) { - if (intWriter.write(output)) { - state = LENGTH_WRITTEN; - } else { - return false; - } - } - if (state == LENGTH_WRITTEN) { - boolean written = huffman - ? huffmanWriter.write(output) - : plainWriter.write(output); - if (written) { - state = DONE; - return true; - } else { - return false; - } - } - throw new InternalError(Arrays.toString(new Object[]{state, huffman})); - } - - void reset() { - intWriter.reset(); - if (huffman) { - huffmanWriter.reset(); - } else { - plainWriter.reset(); - } - state = NEW; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/hpack/package-info.java --- a/src/java.net.http/share/classes/java/net/http/internal/hpack/package-info.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,34 +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. - */ -/** - * HPACK (Header Compression for HTTP/2) implementation conforming to - * RFC 7541. - * - *

Headers can be decoded and encoded by {@link java.net.http.internal.hpack.Decoder} - * and {@link java.net.http.internal.hpack.Encoder} respectively. - * - *

Instances of these classes are not safe for use by multiple threads. - */ -package java.net.http.internal.hpack; diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/BuilderImpl.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/BuilderImpl.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,149 +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 java.net.http.internal.websocket; - -import java.net.http.HttpClient; -import java.net.http.WebSocket; -import java.net.http.WebSocket.Builder; -import java.net.http.WebSocket.Listener; -import java.net.http.internal.common.Pair; - -import java.net.ProxySelector; -import java.net.URI; -import java.time.Duration; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -import static java.util.Objects.requireNonNull; -import static java.net.http.internal.common.Pair.pair; - -public final class BuilderImpl implements Builder { - - private final HttpClient client; - private URI uri; - private Listener listener; - private final Optional proxySelector; - private final Collection> headers; - private final Collection subprotocols; - private Duration timeout; - - public BuilderImpl(HttpClient client, ProxySelector proxySelector) - { - this(client, null, null, Optional.ofNullable(proxySelector), - new LinkedList<>(), new LinkedList<>(), null); - } - - private BuilderImpl(HttpClient client, - URI uri, - Listener listener, - Optional proxySelector, - Collection> headers, - Collection subprotocols, - Duration timeout) { - this.client = client; - this.uri = uri; - this.listener = listener; - this.proxySelector = proxySelector; - // If a proxy selector was supplied by the user, it should be present - // on the client and should be the same that what we got as an argument - assert !client.proxy().isPresent() - || client.proxy().equals(proxySelector); - this.headers = headers; - this.subprotocols = subprotocols; - this.timeout = timeout; - } - - @Override - public Builder header(String name, String value) { - requireNonNull(name, "name"); - requireNonNull(value, "value"); - headers.add(pair(name, value)); - return this; - } - - @Override - public Builder subprotocols(String mostPreferred, String... lesserPreferred) - { - requireNonNull(mostPreferred, "mostPreferred"); - requireNonNull(lesserPreferred, "lesserPreferred"); - List subprotocols = new LinkedList<>(); - subprotocols.add(mostPreferred); - for (int i = 0; i < lesserPreferred.length; i++) { - String p = lesserPreferred[i]; - requireNonNull(p, "lesserPreferred[" + i + "]"); - subprotocols.add(p); - } - this.subprotocols.clear(); - this.subprotocols.addAll(subprotocols); - return this; - } - - @Override - public Builder connectTimeout(Duration timeout) { - this.timeout = requireNonNull(timeout, "timeout"); - return this; - } - - @Override - public CompletableFuture buildAsync(URI uri, Listener listener) { - this.uri = requireNonNull(uri, "uri"); - this.listener = requireNonNull(listener, "listener"); - // A snapshot of builder inaccessible for further modification - // from the outside - BuilderImpl copy = immutableCopy(); - return WebSocketImpl.newInstanceAsync(copy); - } - - HttpClient getClient() { return client; } - - URI getUri() { return uri; } - - Listener getListener() { return listener; } - - Collection> getHeaders() { return headers; } - - Collection getSubprotocols() { return subprotocols; } - - Duration getConnectTimeout() { return timeout; } - - Optional getProxySelector() { return proxySelector; } - - private BuilderImpl immutableCopy() { - @SuppressWarnings({"unchecked", "rawtypes"}) - BuilderImpl copy = new BuilderImpl( - client, - uri, - listener, - proxySelector, - List.of(this.headers.toArray(new Pair[0])), - List.of(this.subprotocols.toArray(new String[0])), - timeout); - return copy; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/CheckFailedException.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/CheckFailedException.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,40 +0,0 @@ -/* - * 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 java.net.http.internal.websocket; - -/* - * Used as a context-neutral exception which can be wrapped into (for example) - * a `ProtocolException` or an `IllegalArgumentException` depending on who's - * doing the check. - */ -final class CheckFailedException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - CheckFailedException(String message) { - super(message); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/FailWebSocketException.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/FailWebSocketException.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,59 +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 java.net.http.internal.websocket; - -import static java.net.http.internal.websocket.StatusCodes.PROTOCOL_ERROR; - -/* - * Used as a marker for protocol issues in the incoming data, so that the - * implementation could "Fail the WebSocket Connection" with a status code in - * the Close message that fits the situation the most. - * - * https://tools.ietf.org/html/rfc6455#section-7.1.7 - */ -final class FailWebSocketException extends RuntimeException { - - private static final long serialVersionUID = 1L; - private final int statusCode; - - FailWebSocketException(String detail) { - this(detail, PROTOCOL_ERROR); - } - - FailWebSocketException(String detail, int statusCode) { - super(detail); - this.statusCode = statusCode; - } - - int getStatusCode() { - return statusCode; - } - - @Override - public FailWebSocketException initCause(Throwable cause) { - return (FailWebSocketException) super.initCause(cause); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/Frame.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/Frame.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,499 +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 java.net.http.internal.websocket; - -import jdk.internal.vm.annotation.Stable; - -import java.nio.ByteBuffer; - -import static java.net.http.internal.common.Utils.dump; -import static java.net.http.internal.websocket.Frame.Opcode.ofCode; - -/* - * A collection of utilities for reading, writing, and masking frames. - */ -final class Frame { - - private Frame() { } - - static final int MAX_HEADER_SIZE_BYTES = 2 + 8 + 4; - - enum Opcode { - - CONTINUATION (0x0), - TEXT (0x1), - BINARY (0x2), - NON_CONTROL_0x3(0x3), - NON_CONTROL_0x4(0x4), - NON_CONTROL_0x5(0x5), - NON_CONTROL_0x6(0x6), - NON_CONTROL_0x7(0x7), - CLOSE (0x8), - PING (0x9), - PONG (0xA), - CONTROL_0xB (0xB), - CONTROL_0xC (0xC), - CONTROL_0xD (0xD), - CONTROL_0xE (0xE), - CONTROL_0xF (0xF); - - @Stable - private static final Opcode[] opcodes; - - static { - Opcode[] values = values(); - opcodes = new Opcode[values.length]; - for (Opcode c : values) { - opcodes[c.code] = c; - } - } - - private final byte code; - - Opcode(int code) { - this.code = (byte) code; - } - - boolean isControl() { - return (code & 0x8) != 0; - } - - static Opcode ofCode(int code) { - return opcodes[code & 0xF]; - } - } - - /* - * A utility for masking frame payload data. - */ - static final class Masker { - - // Exploiting ByteBuffer's ability to read/write multi-byte integers - private final ByteBuffer acc = ByteBuffer.allocate(8); - private final int[] maskBytes = new int[4]; - private int offset; - private long maskLong; - - /* - * Reads all remaining bytes from the given input buffer, masks them - * with the supplied mask and writes the resulting bytes to the given - * output buffer. - * - * The source and the destination buffers may be the same instance. - */ - static void transferMasking(ByteBuffer src, ByteBuffer dst, int mask) { - if (src.remaining() > dst.remaining()) { - throw new IllegalArgumentException(dump(src, dst)); - } - new Masker().mask(mask).transferMasking(src, dst); - } - - /* - * Clears this instance's state and sets the mask. - * - * The behaviour is as if the mask was set on a newly created instance. - */ - Masker mask(int value) { - acc.clear().putInt(value).putInt(value).flip(); - for (int i = 0; i < maskBytes.length; i++) { - maskBytes[i] = acc.get(i); - } - offset = 0; - maskLong = acc.getLong(0); - return this; - } - - /* - * Reads as many remaining bytes as possible from the given input - * buffer, masks them with the previously set mask and writes the - * resulting bytes to the given output buffer. - * - * The source and the destination buffers may be the same instance. If - * the mask hasn't been previously set it is assumed to be 0. - */ - Masker transferMasking(ByteBuffer src, ByteBuffer dst) { - begin(src, dst); - loop(src, dst); - end(src, dst); - return this; - } - - /* - * Applies up to 3 remaining from the previous pass bytes of the mask. - */ - private void begin(ByteBuffer src, ByteBuffer dst) { - if (offset == 0) { // No partially applied mask from the previous invocation - return; - } - int i = src.position(), j = dst.position(); - final int srcLim = src.limit(), dstLim = dst.limit(); - for (; offset < 4 && i < srcLim && j < dstLim; i++, j++, offset++) - { - dst.put(j, (byte) (src.get(i) ^ maskBytes[offset])); - } - offset &= 3; // Will become 0 if the mask has been fully applied - src.position(i); - dst.position(j); - } - - /* - * Gallops one long (mask + mask) at a time. - */ - private void loop(ByteBuffer src, ByteBuffer dst) { - int i = src.position(); - int j = dst.position(); - final int srcLongLim = src.limit() - 7, dstLongLim = dst.limit() - 7; - for (; i < srcLongLim && j < dstLongLim; i += 8, j += 8) { - dst.putLong(j, src.getLong(i) ^ maskLong); - } - if (i > src.limit()) { - src.position(i - 8); - } else { - src.position(i); - } - if (j > dst.limit()) { - dst.position(j - 8); - } else { - dst.position(j); - } - } - - /* - * Applies up to 7 remaining from the "galloping" phase bytes of the - * mask. - */ - private void end(ByteBuffer src, ByteBuffer dst) { - assert Math.min(src.remaining(), dst.remaining()) < 8; - final int srcLim = src.limit(), dstLim = dst.limit(); - int i = src.position(), j = dst.position(); - for (; i < srcLim && j < dstLim; - i++, j++, offset = (offset + 1) & 3) // offset cycles through 0..3 - { - dst.put(j, (byte) (src.get(i) ^ maskBytes[offset])); - } - src.position(i); - dst.position(j); - } - } - - /* - * A builder-style writer of frame headers. - * - * The writer does not enforce any protocol-level rules, it simply writes a - * header structure to the given buffer. The order of calls to intermediate - * methods is NOT significant. - */ - static final class HeaderWriter { - - private char firstChar; - private long payloadLen; - private int maskingKey; - private boolean mask; - - HeaderWriter fin(boolean value) { - if (value) { - firstChar |= 0b10000000_00000000; - } else { - firstChar &= ~0b10000000_00000000; - } - return this; - } - - HeaderWriter rsv1(boolean value) { - if (value) { - firstChar |= 0b01000000_00000000; - } else { - firstChar &= ~0b01000000_00000000; - } - return this; - } - - HeaderWriter rsv2(boolean value) { - if (value) { - firstChar |= 0b00100000_00000000; - } else { - firstChar &= ~0b00100000_00000000; - } - return this; - } - - HeaderWriter rsv3(boolean value) { - if (value) { - firstChar |= 0b00010000_00000000; - } else { - firstChar &= ~0b00010000_00000000; - } - return this; - } - - HeaderWriter opcode(Opcode value) { - firstChar = (char) ((firstChar & 0xF0FF) | (value.code << 8)); - return this; - } - - HeaderWriter payloadLen(long value) { - if (value < 0) { - throw new IllegalArgumentException("Negative: " + value); - } - payloadLen = value; - firstChar &= 0b11111111_10000000; // Clear previous payload length leftovers - if (payloadLen < 126) { - firstChar |= payloadLen; - } else if (payloadLen < 65536) { - firstChar |= 126; - } else { - firstChar |= 127; - } - return this; - } - - HeaderWriter mask(int value) { - firstChar |= 0b00000000_10000000; - maskingKey = value; - mask = true; - return this; - } - - HeaderWriter noMask() { - firstChar &= ~0b00000000_10000000; - mask = false; - return this; - } - - /* - * Writes the header to the given buffer. - * - * The buffer must have at least MAX_HEADER_SIZE_BYTES remaining. The - * buffer's position is incremented by the number of bytes written. - */ - void write(ByteBuffer buffer) { - buffer.putChar(firstChar); - if (payloadLen >= 126) { - if (payloadLen < 65536) { - buffer.putChar((char) payloadLen); - } else { - buffer.putLong(payloadLen); - } - } - if (mask) { - buffer.putInt(maskingKey); - } - } - } - - /* - * A consumer of frame parts. - * - * Frame.Reader invokes the consumer's methods in the following order: - * - * fin rsv1 rsv2 rsv3 opcode mask payloadLength maskingKey? payloadData+ endFrame - */ - interface Consumer { - - void fin(boolean value); - - void rsv1(boolean value); - - void rsv2(boolean value); - - void rsv3(boolean value); - - void opcode(Opcode value); - - void mask(boolean value); - - void payloadLen(long value); - - void maskingKey(int value); - - /* - * Called by the Frame.Reader when a part of the (or a complete) payload - * is ready to be consumed. - * - * The sum of numbers of bytes consumed in each invocation of this - * method corresponding to the given frame WILL be equal to - * 'payloadLen', reported to `void payloadLen(long value)` before that. - * - * In particular, if `payloadLen` is 0, then there WILL be a single - * invocation to this method. - * - * No unmasking is done. - */ - void payloadData(ByteBuffer data); - - void endFrame(); - } - - /* - * A Reader of frames. - * - * No protocol-level rules are checked. - */ - static final class Reader { - - private static final int AWAITING_FIRST_BYTE = 1; - private static final int AWAITING_SECOND_BYTE = 2; - private static final int READING_16_LENGTH = 4; - private static final int READING_64_LENGTH = 8; - private static final int READING_MASK = 16; - private static final int READING_PAYLOAD = 32; - - // Exploiting ByteBuffer's ability to read multi-byte integers - private final ByteBuffer accumulator = ByteBuffer.allocate(8); - private int state = AWAITING_FIRST_BYTE; - private boolean mask; - private long remainingPayloadLength; - - /* - * Reads at most one frame from the given buffer invoking the consumer's - * methods corresponding to the frame parts found. - * - * As much of the frame's payload, if any, is read. The buffer's - * position is updated to reflect the number of bytes read. - * - * Throws FailWebSocketException if detects the frame is malformed. - */ - void readFrame(ByteBuffer input, Consumer consumer) { - loop: - while (true) { - byte b; - switch (state) { - case AWAITING_FIRST_BYTE: - if (!input.hasRemaining()) { - break loop; - } - b = input.get(); - consumer.fin( (b & 0b10000000) != 0); - consumer.rsv1((b & 0b01000000) != 0); - consumer.rsv2((b & 0b00100000) != 0); - consumer.rsv3((b & 0b00010000) != 0); - consumer.opcode(ofCode(b)); - state = AWAITING_SECOND_BYTE; - continue loop; - case AWAITING_SECOND_BYTE: - if (!input.hasRemaining()) { - break loop; - } - b = input.get(); - consumer.mask(mask = (b & 0b10000000) != 0); - byte p1 = (byte) (b & 0b01111111); - if (p1 < 126) { - assert p1 >= 0 : p1; - consumer.payloadLen(remainingPayloadLength = p1); - state = mask ? READING_MASK : READING_PAYLOAD; - } else if (p1 < 127) { - state = READING_16_LENGTH; - } else { - state = READING_64_LENGTH; - } - continue loop; - case READING_16_LENGTH: - if (!input.hasRemaining()) { - break loop; - } - b = input.get(); - if (accumulator.put(b).position() < 2) { - continue loop; - } - remainingPayloadLength = accumulator.flip().getChar(); - if (remainingPayloadLength < 126) { - throw notMinimalEncoding(remainingPayloadLength); - } - consumer.payloadLen(remainingPayloadLength); - accumulator.clear(); - state = mask ? READING_MASK : READING_PAYLOAD; - continue loop; - case READING_64_LENGTH: - if (!input.hasRemaining()) { - break loop; - } - b = input.get(); - if (accumulator.put(b).position() < 8) { - continue loop; - } - remainingPayloadLength = accumulator.flip().getLong(); - if (remainingPayloadLength < 0) { - throw negativePayload(remainingPayloadLength); - } else if (remainingPayloadLength < 65536) { - throw notMinimalEncoding(remainingPayloadLength); - } - consumer.payloadLen(remainingPayloadLength); - accumulator.clear(); - state = mask ? READING_MASK : READING_PAYLOAD; - continue loop; - case READING_MASK: - if (!input.hasRemaining()) { - break loop; - } - b = input.get(); - if (accumulator.put(b).position() != 4) { - continue loop; - } - consumer.maskingKey(accumulator.flip().getInt()); - accumulator.clear(); - state = READING_PAYLOAD; - continue loop; - case READING_PAYLOAD: - // This state does not require any bytes to be available - // in the input buffer in order to proceed - int deliverable = (int) Math.min(remainingPayloadLength, - input.remaining()); - int oldLimit = input.limit(); - input.limit(input.position() + deliverable); - if (deliverable != 0 || remainingPayloadLength == 0) { - consumer.payloadData(input); - } - int consumed = deliverable - input.remaining(); - if (consumed < 0) { - // Consumer cannot consume more than there was available - throw new InternalError(); - } - input.limit(oldLimit); - remainingPayloadLength -= consumed; - if (remainingPayloadLength == 0) { - consumer.endFrame(); - state = AWAITING_FIRST_BYTE; - } - break loop; - default: - throw new InternalError(String.valueOf(state)); - } - } - } - - private static FailWebSocketException negativePayload(long payloadLength) - { - return new FailWebSocketException( - "Negative payload length: " + payloadLength); - } - - private static FailWebSocketException notMinimalEncoding(long payloadLength) - { - return new FailWebSocketException( - "Not minimally-encoded payload length:" + payloadLength); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/FrameConsumer.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/FrameConsumer.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,288 +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 java.net.http.internal.websocket; - -import java.net.http.WebSocket.MessagePart; -import java.net.http.internal.websocket.Frame.Opcode; - -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharacterCodingException; - -import static java.lang.String.format; -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Objects.requireNonNull; -import static java.net.http.internal.common.Utils.dump; -import static java.net.http.internal.websocket.StatusCodes.NO_STATUS_CODE; -import static java.net.http.internal.websocket.StatusCodes.isLegalToReceiveFromServer; - -/* - * Consumes frame parts and notifies a message consumer, when there is - * sufficient data to produce a message, or part thereof. - * - * Data consumed but not yet translated is accumulated until it's sufficient to - * form a message. - */ -/* Non-final for testing purposes only */ -class FrameConsumer implements Frame.Consumer { - - private final static boolean DEBUG = false; - private final MessageStreamConsumer output; - private final UTF8AccumulatingDecoder decoder = new UTF8AccumulatingDecoder(); - private boolean fin; - private Opcode opcode, originatingOpcode; - private MessagePart part = MessagePart.WHOLE; - private long payloadLen; - private long unconsumedPayloadLen; - private ByteBuffer binaryData; - - FrameConsumer(MessageStreamConsumer output) { - this.output = requireNonNull(output); - } - - /* Exposed for testing purposes only */ - MessageStreamConsumer getOutput() { - return output; - } - - @Override - public void fin(boolean value) { - if (DEBUG) { - System.out.printf("Reading fin: %s%n", value); - } - fin = value; - } - - @Override - public void rsv1(boolean value) { - if (DEBUG) { - System.out.printf("Reading rsv1: %s%n", value); - } - if (value) { - throw new FailWebSocketException("Unexpected rsv1 bit"); - } - } - - @Override - public void rsv2(boolean value) { - if (DEBUG) { - System.out.printf("Reading rsv2: %s%n", value); - } - if (value) { - throw new FailWebSocketException("Unexpected rsv2 bit"); - } - } - - @Override - public void rsv3(boolean value) { - if (DEBUG) { - System.out.printf("Reading rsv3: %s%n", value); - } - if (value) { - throw new FailWebSocketException("Unexpected rsv3 bit"); - } - } - - @Override - public void opcode(Opcode v) { - if (DEBUG) { - System.out.printf("Reading opcode: %s%n", v); - } - if (v == Opcode.PING || v == Opcode.PONG || v == Opcode.CLOSE) { - if (!fin) { - throw new FailWebSocketException("Fragmented control frame " + v); - } - opcode = v; - } else if (v == Opcode.TEXT || v == Opcode.BINARY) { - if (originatingOpcode != null) { - throw new FailWebSocketException( - format("Unexpected frame %s (fin=%s)", v, fin)); - } - opcode = v; - if (!fin) { - originatingOpcode = v; - } - } else if (v == Opcode.CONTINUATION) { - if (originatingOpcode == null) { - throw new FailWebSocketException( - format("Unexpected frame %s (fin=%s)", v, fin)); - } - opcode = v; - } else { - throw new FailWebSocketException("Unexpected opcode " + v); - } - } - - @Override - public void mask(boolean value) { - if (DEBUG) { - System.out.printf("Reading mask: %s%n", value); - } - if (value) { - throw new FailWebSocketException("Masked frame received"); - } - } - - @Override - public void payloadLen(long value) { - if (DEBUG) { - System.out.printf("Reading payloadLen: %s%n", value); - } - if (opcode.isControl()) { - if (value > 125) { - throw new FailWebSocketException( - format("%s's payload length %s", opcode, value)); - } - assert Opcode.CLOSE.isControl(); - if (opcode == Opcode.CLOSE && value == 1) { - throw new FailWebSocketException("Incomplete status code"); - } - } - payloadLen = value; - unconsumedPayloadLen = value; - } - - @Override - public void maskingKey(int value) { - // `FrameConsumer.mask(boolean)` is where a masked frame is detected and - // reported on; `FrameConsumer.mask(boolean)` MUST be invoked before - // this method; - // So this method (`maskingKey`) is not supposed to be invoked while - // reading a frame that has came from the server. If this method is - // invoked, then it's an error in implementation, thus InternalError - throw new InternalError(); - } - - @Override - public void payloadData(ByteBuffer data) { - if (DEBUG) { - System.out.printf("Reading payloadData: %s%n", data); - } - unconsumedPayloadLen -= data.remaining(); - boolean isLast = unconsumedPayloadLen == 0; - if (opcode.isControl()) { - if (binaryData != null) { // An intermediate or the last chunk - binaryData.put(data); - } else if (!isLast) { // The first chunk - int remaining = data.remaining(); - // It shouldn't be 125, otherwise the next chunk will be of size - // 0, which is not what Reader promises to deliver (eager - // reading) - assert remaining < 125 : dump(remaining); - binaryData = ByteBuffer.allocate(125).put(data); - } else { // The only chunk - binaryData = ByteBuffer.allocate(data.remaining()).put(data); - } - } else { - part = determinePart(isLast); - boolean text = opcode == Opcode.TEXT || originatingOpcode == Opcode.TEXT; - if (!text) { - output.onBinary(data.slice(), part); - data.position(data.limit()); // Consume - } else { - boolean binaryNonEmpty = data.hasRemaining(); - CharBuffer textData; - try { - textData = decoder.decode(data, part == MessagePart.WHOLE || part == MessagePart.LAST); - } catch (CharacterCodingException e) { - throw new FailWebSocketException( - "Invalid UTF-8 in frame " + opcode, StatusCodes.NOT_CONSISTENT) - .initCause(e); - } - if (!(binaryNonEmpty && !textData.hasRemaining())) { - // If there's a binary data, that result in no text, then we - // don't deliver anything - output.onText(textData, part); - } - } - } - } - - @Override - public void endFrame() { - if (DEBUG) { - System.out.println("End frame"); - } - if (opcode.isControl()) { - binaryData.flip(); - } - switch (opcode) { - case CLOSE: - char statusCode = NO_STATUS_CODE; - String reason = ""; - if (payloadLen != 0) { - int len = binaryData.remaining(); - assert 2 <= len && len <= 125 : dump(len, payloadLen); - statusCode = binaryData.getChar(); - if (!isLegalToReceiveFromServer(statusCode)) { - throw new FailWebSocketException( - "Illegal status code: " + statusCode); - } - try { - reason = UTF_8.newDecoder().decode(binaryData).toString(); - } catch (CharacterCodingException e) { - throw new FailWebSocketException("Illegal close reason") - .initCause(e); - } - } - output.onClose(statusCode, reason); - break; - case PING: - output.onPing(binaryData); - binaryData = null; - break; - case PONG: - output.onPong(binaryData); - binaryData = null; - break; - default: - assert opcode == Opcode.TEXT || opcode == Opcode.BINARY - || opcode == Opcode.CONTINUATION : dump(opcode); - if (fin) { - // It is always the last chunk: - // either TEXT(FIN=TRUE)/BINARY(FIN=TRUE) or CONT(FIN=TRUE) - originatingOpcode = null; - } - break; - } - payloadLen = 0; - opcode = null; - } - - private MessagePart determinePart(boolean isLast) { - boolean lastChunk = fin && isLast; - switch (part) { - case LAST: - case WHOLE: - return lastChunk ? MessagePart.WHOLE : MessagePart.FIRST; - case FIRST: - case PART: - return lastChunk ? MessagePart.LAST : MessagePart.PART; - default: - throw new InternalError(String.valueOf(part)); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/MessageStreamConsumer.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/MessageStreamConsumer.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +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 java.net.http.internal.websocket; - -import java.net.http.WebSocket.MessagePart; - -import java.nio.ByteBuffer; - -/* - * A callback for consuming messages and related events on the stream. - */ -interface MessageStreamConsumer { - - void onText(CharSequence data, MessagePart part); - - void onBinary(ByteBuffer data, MessagePart part); - - void onPing(ByteBuffer data); - - void onPong(ByteBuffer data); - - void onClose(int statusCode, CharSequence reason); - - /* - * Indicates the end of stream has been reached and there will be no further - * messages. - */ - void onComplete(); - - void onError(Throwable e); -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/OpeningHandshake.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/OpeningHandshake.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,392 +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 java.net.http.internal.websocket; - -import java.net.http.HttpClient; -import java.net.http.HttpClient.Version; -import java.net.http.HttpHeaders; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandler; -import java.net.http.WebSocketHandshakeException; -import java.net.http.internal.common.MinimalFuture; -import java.net.http.internal.common.Pair; -import java.net.http.internal.common.Utils; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.net.ProxySelector; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLPermission; -import java.nio.charset.StandardCharsets; -import java.security.AccessController; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.PrivilegedAction; -import java.security.SecureRandom; -import java.time.Duration; -import java.util.Base64; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static java.lang.String.format; -import static java.net.http.internal.common.Utils.isValidName; -import static java.net.http.internal.common.Utils.permissionForProxy; -import static java.net.http.internal.common.Utils.stringOf; - -public class OpeningHandshake { - - private static final String HEADER_CONNECTION = "Connection"; - private static final String HEADER_UPGRADE = "Upgrade"; - private static final String HEADER_ACCEPT = "Sec-WebSocket-Accept"; - private static final String HEADER_EXTENSIONS = "Sec-WebSocket-Extensions"; - private static final String HEADER_KEY = "Sec-WebSocket-Key"; - private static final String HEADER_PROTOCOL = "Sec-WebSocket-Protocol"; - private static final String HEADER_VERSION = "Sec-WebSocket-Version"; - private static final String VERSION = "13"; // WebSocket's lucky number - - private static final Set ILLEGAL_HEADERS; - - static { - ILLEGAL_HEADERS = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - ILLEGAL_HEADERS.addAll(List.of(HEADER_ACCEPT, - HEADER_EXTENSIONS, - HEADER_KEY, - HEADER_PROTOCOL, - HEADER_VERSION)); - } - - private static final SecureRandom random = new SecureRandom(); - - private final MessageDigest sha1; - private final HttpClient client; - - { - try { - sha1 = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - // Shouldn't happen: SHA-1 must be available in every Java platform - // implementation - throw new InternalError("Minimum requirements", e); - } - } - - private final HttpRequest request; - private final Collection subprotocols; - private final String nonce; - - public OpeningHandshake(BuilderImpl b) { - checkURI(b.getUri()); - Proxy proxy = proxyFor(b.getProxySelector(), b.getUri()); - checkPermissions(b, proxy); - this.client = b.getClient(); - URI httpURI = createRequestURI(b.getUri()); - HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(httpURI); - Duration connectTimeout = b.getConnectTimeout(); - if (connectTimeout != null) { - requestBuilder.timeout(connectTimeout); - } - for (Pair p : b.getHeaders()) { - if (ILLEGAL_HEADERS.contains(p.first)) { - throw illegal("Illegal header: " + p.first); - } - requestBuilder.header(p.first, p.second); - } - this.subprotocols = createRequestSubprotocols(b.getSubprotocols()); - if (!this.subprotocols.isEmpty()) { - String p = this.subprotocols.stream().collect(Collectors.joining(", ")); - requestBuilder.header(HEADER_PROTOCOL, p); - } - requestBuilder.header(HEADER_VERSION, VERSION); - this.nonce = createNonce(); - requestBuilder.header(HEADER_KEY, this.nonce); - // Setting request version to HTTP/1.1 forcibly, since it's not possible - // to upgrade from HTTP/2 to WebSocket (as of August 2016): - // - // https://tools.ietf.org/html/draft-hirano-httpbis-websocket-over-http2-00 - this.request = requestBuilder.version(Version.HTTP_1_1).GET().build(); - WebSocketRequest r = (WebSocketRequest) this.request; - r.isWebSocket(true); - r.setSystemHeader(HEADER_UPGRADE, "websocket"); - r.setSystemHeader(HEADER_CONNECTION, "Upgrade"); - r.setProxy(proxy); - } - - private static Collection createRequestSubprotocols( - Collection subprotocols) - { - LinkedHashSet sp = new LinkedHashSet<>(subprotocols.size(), 1); - for (String s : subprotocols) { - if (s.trim().isEmpty() || !isValidName(s)) { - throw illegal("Bad subprotocol syntax: " + s); - } - if (!sp.add(s)) { - throw illegal("Duplicating subprotocol: " + s); - } - } - return Collections.unmodifiableCollection(sp); - } - - /* - * Checks the given URI for being a WebSocket URI and translates it into a - * target HTTP URI for the Opening Handshake. - * - * https://tools.ietf.org/html/rfc6455#section-3 - */ - static URI createRequestURI(URI uri) { - String s = uri.getScheme(); - assert "ws".equalsIgnoreCase(s) || "wss".equalsIgnoreCase(s); - String scheme = "ws".equalsIgnoreCase(s) ? "http" : "https"; - try { - return new URI(scheme, - uri.getUserInfo(), - uri.getHost(), - uri.getPort(), - uri.getPath(), - uri.getQuery(), - null); // No fragment - } catch (URISyntaxException e) { - // Shouldn't happen: URI invariant - throw new InternalError(e); - } - } - - public CompletableFuture send() { - PrivilegedAction> pa = () -> - client.sendAsync(this.request, BodyHandler.discard()) - .thenCompose(this::resultFrom); - return AccessController.doPrivileged(pa); - } - - /* - * The result of the opening handshake. - */ - static final class Result { - - final String subprotocol; - final TransportFactory transport; - - private Result(String subprotocol, TransportFactory transport) { - this.subprotocol = subprotocol; - this.transport = transport; - } - } - - private CompletableFuture resultFrom(HttpResponse response) { - // Do we need a special treatment for SSLHandshakeException? - // Namely, invoking - // - // Listener.onClose(StatusCodes.TLS_HANDSHAKE_FAILURE, "") - // - // See https://tools.ietf.org/html/rfc6455#section-7.4.1 - Result result = null; - Exception exception = null; - try { - result = handleResponse(response); - } catch (IOException e) { - exception = e; - } catch (Exception e) { - exception = new WebSocketHandshakeException(response).initCause(e); - } - if (exception == null) { - return MinimalFuture.completedFuture(result); - } - try { - ((RawChannel.Provider) response).rawChannel().close(); - } catch (IOException e) { - exception.addSuppressed(e); - } - return MinimalFuture.failedFuture(exception); - } - - private Result handleResponse(HttpResponse response) throws IOException { - // By this point all redirects, authentications, etc. (if any) MUST have - // been done by the HttpClient used by the WebSocket; so only 101 is - // expected - int c = response.statusCode(); - if (c != 101) { - throw checkFailed("Unexpected HTTP response status code " + c); - } - HttpHeaders headers = response.headers(); - String upgrade = requireSingle(headers, HEADER_UPGRADE); - if (!upgrade.equalsIgnoreCase("websocket")) { - throw checkFailed("Bad response field: " + HEADER_UPGRADE); - } - String connection = requireSingle(headers, HEADER_CONNECTION); - if (!connection.equalsIgnoreCase("Upgrade")) { - throw checkFailed("Bad response field: " + HEADER_CONNECTION); - } - Optional version = requireAtMostOne(headers, HEADER_VERSION); - if (version.isPresent() && !version.get().equals(VERSION)) { - throw checkFailed("Bad response field: " + HEADER_VERSION); - } - requireAbsent(headers, HEADER_EXTENSIONS); - String x = this.nonce + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - this.sha1.update(x.getBytes(StandardCharsets.ISO_8859_1)); - String expected = Base64.getEncoder().encodeToString(this.sha1.digest()); - String actual = requireSingle(headers, HEADER_ACCEPT); - if (!actual.trim().equals(expected)) { - throw checkFailed("Bad " + HEADER_ACCEPT); - } - String subprotocol = checkAndReturnSubprotocol(headers); - RawChannel channel = ((RawChannel.Provider) response).rawChannel(); - return new Result(subprotocol, new TransportFactoryImpl(channel)); - } - - private String checkAndReturnSubprotocol(HttpHeaders responseHeaders) - throws CheckFailedException - { - Optional opt = responseHeaders.firstValue(HEADER_PROTOCOL); - if (!opt.isPresent()) { - // If there is no such header in the response, then the server - // doesn't want to use any subprotocol - return ""; - } - String s = requireSingle(responseHeaders, HEADER_PROTOCOL); - // An empty string as a subprotocol's name is not allowed by the spec - // and the check below will detect such responses too - if (this.subprotocols.contains(s)) { - return s; - } else { - throw checkFailed("Unexpected subprotocol: " + s); - } - } - - private static void requireAbsent(HttpHeaders responseHeaders, - String headerName) - { - List values = responseHeaders.allValues(headerName); - if (!values.isEmpty()) { - throw checkFailed(format("Response field '%s' present: %s", - headerName, - stringOf(values))); - } - } - - private static Optional requireAtMostOne(HttpHeaders responseHeaders, - String headerName) - { - List values = responseHeaders.allValues(headerName); - if (values.size() > 1) { - throw checkFailed(format("Response field '%s' multivalued: %s", - headerName, - stringOf(values))); - } - return values.stream().findFirst(); - } - - private static String requireSingle(HttpHeaders responseHeaders, - String headerName) - { - List values = responseHeaders.allValues(headerName); - if (values.isEmpty()) { - throw checkFailed("Response field missing: " + headerName); - } else if (values.size() > 1) { - throw checkFailed(format("Response field '%s' multivalued: %s", - headerName, - stringOf(values))); - } - return values.get(0); - } - - private static String createNonce() { - byte[] bytes = new byte[16]; - OpeningHandshake.random.nextBytes(bytes); - return Base64.getEncoder().encodeToString(bytes); - } - - private static CheckFailedException checkFailed(String message) { - throw new CheckFailedException(message); - } - - private static URI checkURI(URI uri) { - String scheme = uri.getScheme(); - if (!("ws".equalsIgnoreCase(scheme) || "wss".equalsIgnoreCase(scheme))) - throw illegal("invalid URI scheme: " + scheme); - if (uri.getHost() == null) - throw illegal("URI must contain a host: " + uri); - if (uri.getFragment() != null) - throw illegal("URI must not contain a fragment: " + uri); - return uri; - } - - private static IllegalArgumentException illegal(String message) { - return new IllegalArgumentException(message); - } - - /** - * Returns the proxy for the given URI when sent through the given client, - * or {@code null} if none is required or applicable. - */ - private static Proxy proxyFor(Optional selector, URI uri) { - if (!selector.isPresent()) { - return null; - } - URI requestURI = createRequestURI(uri); // Based on the HTTP scheme - List pl = selector.get().select(requestURI); - if (pl.isEmpty()) { - return null; - } - Proxy proxy = pl.get(0); - if (proxy.type() != Proxy.Type.HTTP) { - return null; - } - return proxy; - } - - /** - * Performs the necessary security permissions checks to connect ( possibly - * through a proxy ) to the builders WebSocket URI. - * - * @throws SecurityException if the security manager denies access - */ - static void checkPermissions(BuilderImpl b, Proxy proxy) { - SecurityManager sm = System.getSecurityManager(); - if (sm == null) { - return; - } - Stream headers = b.getHeaders().stream().map(p -> p.first).distinct(); - URLPermission perm1 = Utils.permissionForServer(b.getUri(), "", headers); - sm.checkPermission(perm1); - if (proxy == null) { - return; - } - URLPermission perm2 = permissionForProxy((InetSocketAddress) proxy.address()); - if (perm2 != null) { - sm.checkPermission(perm2); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/OutgoingMessage.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/OutgoingMessage.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,296 +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 java.net.http.internal.websocket; - -import java.net.http.internal.websocket.Frame.Opcode; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharacterCodingException; -import java.nio.charset.CharsetEncoder; -import java.nio.charset.CoderResult; -import java.security.SecureRandom; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Objects.requireNonNull; -import static java.net.http.internal.common.Utils.EMPTY_BYTEBUFFER; -import static java.net.http.internal.websocket.Frame.MAX_HEADER_SIZE_BYTES; -import static java.net.http.internal.websocket.Frame.Opcode.BINARY; -import static java.net.http.internal.websocket.Frame.Opcode.CLOSE; -import static java.net.http.internal.websocket.Frame.Opcode.CONTINUATION; -import static java.net.http.internal.websocket.Frame.Opcode.PING; -import static java.net.http.internal.websocket.Frame.Opcode.PONG; -import static java.net.http.internal.websocket.Frame.Opcode.TEXT; - -/* - * A stateful object that represents a WebSocket message being sent to the - * channel. - * - * Data provided to the constructors is copied. Otherwise we would have to deal - * with mutability, security, masking/unmasking, readonly status, etc. So - * copying greatly simplifies the implementation. - * - * In the case of memory-sensitive environments an alternative implementation - * could use an internal pool of buffers though at the cost of extra complexity - * and possible performance degradation. - */ -abstract class OutgoingMessage { - - // Share per WebSocket? - private static final SecureRandom maskingKeys = new SecureRandom(); - - protected ByteBuffer[] frame; - protected int offset; - - /* - * Performs contextualization. This method is not a part of the constructor - * so it would be possible to defer the work it does until the most - * convenient moment (up to the point where sentTo is invoked). - */ - protected boolean contextualize(Context context) { - // masking and charset decoding should be performed here rather than in - // the constructor (as of today) - if (context.isCloseSent()) { - throw new IllegalStateException("Close sent"); - } - return true; - } - - protected boolean sendTo(RawChannel channel) throws IOException { - while ((offset = nextUnwrittenIndex()) != -1) { - long n = channel.write(frame, offset, frame.length - offset); - if (n == 0) { - return false; - } - } - return true; - } - - private int nextUnwrittenIndex() { - for (int i = offset; i < frame.length; i++) { - if (frame[i].hasRemaining()) { - return i; - } - } - return -1; - } - - static final class Text extends OutgoingMessage { - - private final ByteBuffer payload; - private final boolean isLast; - - Text(CharSequence characters, boolean isLast) { - CharsetEncoder encoder = UTF_8.newEncoder(); // Share per WebSocket? - try { - payload = encoder.encode(CharBuffer.wrap(characters)); - } catch (CharacterCodingException e) { - throw new IllegalArgumentException( - "Malformed UTF-8 text message"); - } - this.isLast = isLast; - } - - @Override - protected boolean contextualize(Context context) { - super.contextualize(context); - if (context.isPreviousBinary() && !context.isPreviousLast()) { - throw new IllegalStateException("Unexpected text message"); - } - frame = getDataMessageBuffers( - TEXT, context.isPreviousLast(), isLast, payload, payload); - context.setPreviousBinary(false); - context.setPreviousText(true); - context.setPreviousLast(isLast); - return true; - } - } - - static final class Binary extends OutgoingMessage { - - private final ByteBuffer payload; - private final boolean isLast; - - Binary(ByteBuffer payload, boolean isLast) { - this.payload = requireNonNull(payload); - this.isLast = isLast; - } - - @Override - protected boolean contextualize(Context context) { - super.contextualize(context); - if (context.isPreviousText() && !context.isPreviousLast()) { - throw new IllegalStateException("Unexpected binary message"); - } - ByteBuffer newBuffer = ByteBuffer.allocate(payload.remaining()); - frame = getDataMessageBuffers( - BINARY, context.isPreviousLast(), isLast, payload, newBuffer); - context.setPreviousText(false); - context.setPreviousBinary(true); - context.setPreviousLast(isLast); - return true; - } - } - - static final class Ping extends OutgoingMessage { - - Ping(ByteBuffer payload) { - frame = getControlMessageBuffers(PING, payload); - } - } - - static final class Pong extends OutgoingMessage { - - Pong(ByteBuffer payload) { - frame = getControlMessageBuffers(PONG, payload); - } - } - - static final class Close extends OutgoingMessage { - - Close() { - frame = getControlMessageBuffers(CLOSE, EMPTY_BYTEBUFFER); - } - - Close(int statusCode, CharSequence reason) { - ByteBuffer payload = ByteBuffer.allocate(125) - .putChar((char) statusCode); - CoderResult result = UTF_8.newEncoder() - .encode(CharBuffer.wrap(reason), - payload, - true); - if (result.isOverflow()) { - throw new IllegalArgumentException("Long reason"); - } else if (result.isError()) { - try { - result.throwException(); - } catch (CharacterCodingException e) { - throw new IllegalArgumentException( - "Malformed UTF-8 reason", e); - } - } - payload.flip(); - frame = getControlMessageBuffers(CLOSE, payload); - } - - @Override - protected boolean contextualize(Context context) { - if (context.isCloseSent()) { - return false; - } else { - context.setCloseSent(); - return true; - } - } - } - - private static ByteBuffer[] getControlMessageBuffers(Opcode opcode, - ByteBuffer payload) { - assert opcode.isControl() : opcode; - int remaining = payload.remaining(); - if (remaining > 125) { - throw new IllegalArgumentException - ("Long message: " + remaining); - } - ByteBuffer frame = ByteBuffer.allocate(MAX_HEADER_SIZE_BYTES + remaining); - int mask = maskingKeys.nextInt(); - new Frame.HeaderWriter() - .fin(true) - .opcode(opcode) - .payloadLen(remaining) - .mask(mask) - .write(frame); - Frame.Masker.transferMasking(payload, frame, mask); - frame.flip(); - return new ByteBuffer[]{frame}; - } - - private static ByteBuffer[] getDataMessageBuffers(Opcode type, - boolean isPreviousLast, - boolean isLast, - ByteBuffer payloadSrc, - ByteBuffer payloadDst) { - assert !type.isControl() && type != CONTINUATION : type; - ByteBuffer header = ByteBuffer.allocate(MAX_HEADER_SIZE_BYTES); - int mask = maskingKeys.nextInt(); - new Frame.HeaderWriter() - .fin(isLast) - .opcode(isPreviousLast ? type : CONTINUATION) - .payloadLen(payloadDst.remaining()) - .mask(mask) - .write(header); - header.flip(); - Frame.Masker.transferMasking(payloadSrc, payloadDst, mask); - payloadDst.flip(); - return new ByteBuffer[]{header, payloadDst}; - } - - /* - * An instance of this class is passed sequentially between messages, so - * every message in a sequence can check the context it is in and update it - * if necessary. - */ - public static class Context { - - boolean previousLast = true; - boolean previousBinary; - boolean previousText; - boolean closeSent; - - private boolean isPreviousText() { - return this.previousText; - } - - private void setPreviousText(boolean value) { - this.previousText = value; - } - - private boolean isPreviousBinary() { - return this.previousBinary; - } - - private void setPreviousBinary(boolean value) { - this.previousBinary = value; - } - - private boolean isPreviousLast() { - return this.previousLast; - } - - private void setPreviousLast(boolean value) { - this.previousLast = value; - } - - private boolean isCloseSent() { - return closeSent; - } - - private void setCloseSent() { - closeSent = true; - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/RawChannel.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/RawChannel.java Wed Feb 07 15:46:30 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 java.net.http.internal.websocket; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.ClosedChannelException; - -/* - * I/O abstraction used to implement WebSocket. - * - * @since 9 - */ -public interface RawChannel extends Closeable { - - interface Provider { - - RawChannel rawChannel() throws IOException; - } - - interface RawEvent { - - /* - * Returns the selector op flags this event is interested in. - */ - int interestOps(); - - /* - * Called when event occurs. - */ - void handle(); - } - - /* - * Registers given event whose callback will be called once only (i.e. - * register new event for each callback). - * - * Memory consistency effects: actions in a thread calling registerEvent - * happen-before any subsequent actions in the thread calling event.handle - */ - void registerEvent(RawEvent event) throws IOException; - - /** - * Hands over the initial bytes. Once the bytes have been returned they are - * no longer available and the method will throw an {@link - * IllegalStateException} on each subsequent invocation. - * - * @return the initial bytes - * @throws IllegalStateException - * if the method has been already invoked - */ - ByteBuffer initialByteBuffer() throws IllegalStateException; - - /* - * Returns a ByteBuffer with the data read or null if EOF is reached. Has no - * remaining bytes if no data available at the moment. - */ - ByteBuffer read() throws IOException; - - /* - * Writes a sequence of bytes to this channel from a subsequence of the - * given buffers. - */ - long write(ByteBuffer[] srcs, int offset, int length) throws IOException; - - /** - * Shutdown the connection for reading without closing the channel. - * - *

Once shutdown for reading then further reads on the channel will - * return {@code null}, the end-of-stream indication. If the input side of - * the connection is already shutdown then invoking this method has no - * effect. - * - * @throws ClosedChannelException - * If this channel is closed - * @throws IOException - * If some other I/O error occurs - */ - void shutdownInput() throws IOException; - - /** - * Shutdown the connection for writing without closing the channel. - * - *

Once shutdown for writing then further attempts to write to the - * channel will throw {@link ClosedChannelException}. If the output side of - * the connection is already shutdown then invoking this method has no - * effect. - * - * @throws ClosedChannelException - * If this channel is closed - * @throws IOException - * If some other I/O error occurs - */ - void shutdownOutput() throws IOException; - - /** - * Closes this channel. - * - * @throws IOException - * If an I/O error occurs - */ - @Override - void close() throws IOException; -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/StatusCodes.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/StatusCodes.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -/* - * 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 java.net.http.internal.websocket; - -/* - * Utilities for WebSocket status codes. - * - * 1. https://tools.ietf.org/html/rfc6455#section-7.4 - * 2. http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number - */ -final class StatusCodes { - - static final int PROTOCOL_ERROR = 1002; - static final int NO_STATUS_CODE = 1005; - static final int CLOSED_ABNORMALLY = 1006; - static final int NOT_CONSISTENT = 1007; - - private StatusCodes() { } - - static boolean isLegalToSendFromClient(int code) { - if (!isLegal(code)) { - return false; - } - // Codes from unreserved range - if (code > 4999) { - return false; - } - // Codes below are not allowed to be sent using a WebSocket client API - switch (code) { - case PROTOCOL_ERROR: - case NOT_CONSISTENT: - case 1003: - case 1009: - case 1010: - case 1012: // code sent by servers - case 1013: // code sent by servers - case 1014: // code sent by servers - return false; - default: - return true; - } - } - - static boolean isLegalToReceiveFromServer(int code) { - if (!isLegal(code)) { - return false; - } - return code != 1010; // code sent by clients - } - - private static boolean isLegal(int code) { - // 2-byte unsigned integer excluding first 1000 numbers from the range - // [0, 999] which are never used - if (code < 1000 || code > 65535) { - return false; - } - // Codes from the range below has no known meaning under the WebSocket - // specification (i.e. unassigned/undefined) - if ((code >= 1016 && code <= 2999) || code == 1004) { - return false; - } - // Codes below cannot appear on the wire. It's always an error either - // to send a frame with such a code or to receive one. - switch (code) { - case NO_STATUS_CODE: - case CLOSED_ABNORMALLY: - case 1015: - return false; - default: - return true; - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/Transport.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/Transport.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,89 +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 java.net.http.internal.websocket; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.concurrent.CompletableFuture; - -/* - * Transport needs some way to asynchronously notify the send operation has been - * completed. It can have several different designs each of which has its own - * pros and cons: - * - * (1) void sendMessage(..., Callback) - * (2) CompletableFuture sendMessage(...) - * (3) CompletableFuture sendMessage(..., Callback) - * (4) boolean sendMessage(..., Callback) throws IOException - * ... - * - * If Transport's users use CFs, (1) forces these users to create CFs and pass - * them to the callback. If any additional (dependant) action needs to be - * attached to the returned CF, this means an extra object (CF) must be created - * in (2). (3) and (4) solves both issues, however (4) does not abstract out - * when exactly the operation has been performed. So the handling code needs to - * be repeated twice. And that leads to 2 different code paths (more bugs). - * Unless designed for this, the user should not assume any specific order of - * completion in (3) (e.g. callback first and then the returned CF). - * - * The only parametrization of Transport used is Transport. The - * type parameter T was introduced solely to avoid circular dependency between - * Transport and WebSocket. After all, instances of T are used solely to - * complete CompletableFutures. Transport doesn't care about the exact type of - * T. - * - * This way the Transport is fully in charge of creating CompletableFutures. - * On the one hand, Transport may use it to cache/reuse CompletableFutures. On - * the other hand, the class that uses Transport, may benefit by not converting - * from CompletableFuture returned from Transport, to CompletableFuture - * needed by the said class. - */ -public interface Transport { - - CompletableFuture sendText(CharSequence message, boolean isLast); - - CompletableFuture sendBinary(ByteBuffer message, boolean isLast); - - CompletableFuture sendPing(ByteBuffer message); - - CompletableFuture sendPong(ByteBuffer message); - - CompletableFuture sendClose(int statusCode, String reason); - - void request(long n); - - /* - * Why is this method needed? Since Receiver operates through callbacks - * this method allows to abstract out what constitutes as a message being - * received (i.e. to decide outside this type when exactly one should - * decrement the demand). - */ - void acknowledgeReception(); - - void closeOutput() throws IOException; - - void closeInput() throws IOException; -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/TransportFactory.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/TransportFactory.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,10 +0,0 @@ -package java.net.http.internal.websocket; - -import java.util.function.Supplier; - -@FunctionalInterface -public interface TransportFactory { - - Transport createTransport(Supplier sendResultSupplier, - MessageStreamConsumer consumer); -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/TransportFactoryImpl.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/TransportFactoryImpl.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +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 java.net.http.internal.websocket; - -import java.util.function.Supplier; - -public class TransportFactoryImpl implements TransportFactory { - - private final RawChannel channel; - - public TransportFactoryImpl(RawChannel channel) { - this.channel = channel; - } - - @Override - public Transport createTransport(Supplier sendResultSupplier, - MessageStreamConsumer consumer) { - return new TransportImpl(sendResultSupplier, consumer, channel); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/TransportImpl.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/TransportImpl.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,383 +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 java.net.http.internal.websocket; - -import java.net.http.internal.common.Demand; -import java.net.http.internal.common.MinimalFuture; -import java.net.http.internal.common.Pair; -import java.net.http.internal.common.SequentialScheduler; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.SelectionKey; -import java.util.Queue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import static java.util.Objects.requireNonNull; -import static java.net.http.internal.common.MinimalFuture.failedFuture; -import static java.net.http.internal.common.Pair.pair; - -public class TransportImpl implements Transport { - - /* This flag is used solely for assertions */ - private final AtomicBoolean busy = new AtomicBoolean(); - private OutgoingMessage message; - private Consumer completionHandler; - private final RawChannel channel; - private final RawChannel.RawEvent writeEvent = createWriteEvent(); - private final SequentialScheduler sendScheduler = new SequentialScheduler(new SendTask()); - private final Queue>> - queue = new ConcurrentLinkedQueue<>(); - private final OutgoingMessage.Context context = new OutgoingMessage.Context(); - private final Supplier resultSupplier; - - private final MessageStreamConsumer messageConsumer; - private final FrameConsumer frameConsumer; - private final Frame.Reader reader = new Frame.Reader(); - private final RawChannel.RawEvent readEvent = createReadEvent(); - private final Demand demand = new Demand(); - private final SequentialScheduler receiveScheduler; - - private ByteBuffer data; - private volatile int state; - - private static final int UNREGISTERED = 0; - private static final int AVAILABLE = 1; - private static final int WAITING = 2; - - private final Object lock = new Object(); - private boolean inputClosed; - private boolean outputClosed; - - public TransportImpl(Supplier sendResultSupplier, - MessageStreamConsumer consumer, - RawChannel channel) { - this.resultSupplier = sendResultSupplier; - this.messageConsumer = consumer; - this.channel = channel; - this.frameConsumer = new FrameConsumer(this.messageConsumer); - this.data = channel.initialByteBuffer(); - // To ensure the initial non-final `data` will be visible - // (happens-before) when `readEvent.handle()` invokes `receiveScheduler` - // the following assignment is done last: - receiveScheduler = new SequentialScheduler(new ReceiveTask()); - } - - /** - * The supplied handler may be invoked in the calling thread. - * A {@code StackOverflowError} may thus occur if there's a possibility - * that this method is called again by the supplied handler. - */ - public void send(OutgoingMessage message, - Consumer completionHandler) { - requireNonNull(message); - requireNonNull(completionHandler); - if (!busy.compareAndSet(false, true)) { - throw new IllegalStateException(); - } - send0(message, completionHandler); - } - - private RawChannel.RawEvent createWriteEvent() { - return new RawChannel.RawEvent() { - - @Override - public int interestOps() { - return SelectionKey.OP_WRITE; - } - - @Override - public void handle() { - // registerEvent(e) happens-before subsequent e.handle(), so - // we're fine reading the stored message and the completionHandler - send0(message, completionHandler); - } - }; - } - - private void send0(OutgoingMessage message, Consumer handler) { - boolean b = busy.get(); - assert b; // Please don't inline this, as busy.get() has memory - // visibility effects and we don't want the program behaviour - // to depend on whether the assertions are turned on - // or turned off - try { - boolean sent = message.sendTo(channel); - if (sent) { - busy.set(false); - handler.accept(null); - } else { - // The message has not been fully sent, the transmitter needs to - // remember the message until it can continue with sending it - this.message = message; - this.completionHandler = handler; - try { - channel.registerEvent(writeEvent); - } catch (IOException e) { - this.message = null; - this.completionHandler = null; - busy.set(false); - handler.accept(e); - } - } - } catch (IOException e) { - busy.set(false); - handler.accept(e); - } - } - - public CompletableFuture sendText(CharSequence message, - boolean isLast) { - OutgoingMessage.Text m; - try { - m = new OutgoingMessage.Text(message, isLast); - } catch (IllegalArgumentException e) { - return failedFuture(e); - } - return enqueue(m); - } - - public CompletableFuture sendBinary(ByteBuffer message, - boolean isLast) { - return enqueue(new OutgoingMessage.Binary(message, isLast)); - } - - public CompletableFuture sendPing(ByteBuffer message) { - OutgoingMessage.Ping m; - try { - m = new OutgoingMessage.Ping(message); - } catch (IllegalArgumentException e) { - return failedFuture(e); - } - return enqueue(m); - } - - public CompletableFuture sendPong(ByteBuffer message) { - OutgoingMessage.Pong m; - try { - m = new OutgoingMessage.Pong(message); - } catch (IllegalArgumentException e) { - return failedFuture(e); - } - return enqueue(m); - } - - public CompletableFuture sendClose(int statusCode, String reason) { - OutgoingMessage.Close m; - try { - m = new OutgoingMessage.Close(statusCode, reason); - } catch (IllegalArgumentException e) { - return failedFuture(e); - } - return enqueue(m); - } - - private CompletableFuture enqueue(OutgoingMessage m) { - CompletableFuture cf = new MinimalFuture<>(); - boolean added = queue.add(pair(m, cf)); - if (!added) { - // The queue is supposed to be unbounded - throw new InternalError(); - } - sendScheduler.runOrSchedule(); - return cf; - } - - /* - * This is a message sending task. It pulls messages from the queue one by - * one and sends them. It may be run in different threads, but never - * concurrently. - */ - private class SendTask implements SequentialScheduler.RestartableTask { - - @Override - public void run(SequentialScheduler.DeferredCompleter taskCompleter) { - Pair> p = queue.poll(); - if (p == null) { - taskCompleter.complete(); - return; - } - OutgoingMessage message = p.first; - CompletableFuture cf = p.second; - try { - if (!message.contextualize(context)) { // Do not send the message - cf.complete(resultSupplier.get()); - repeat(taskCompleter); - return; - } - Consumer h = e -> { - if (e == null) { - cf.complete(resultSupplier.get()); - } else { - cf.completeExceptionally(e); - } - repeat(taskCompleter); - }; - send(message, h); - } catch (Throwable t) { - cf.completeExceptionally(t); - repeat(taskCompleter); - } - } - - private void repeat(SequentialScheduler.DeferredCompleter taskCompleter) { - taskCompleter.complete(); - // More than a single message may have been enqueued while - // the task has been busy with the current message, but - // there is only a single signal recorded - sendScheduler.runOrSchedule(); - } - } - - private RawChannel.RawEvent createReadEvent() { - return new RawChannel.RawEvent() { - - @Override - public int interestOps() { - return SelectionKey.OP_READ; - } - - @Override - public void handle() { - state = AVAILABLE; - receiveScheduler.runOrSchedule(); - } - }; - } - - @Override - public void request(long n) { - if (demand.increase(n)) { - receiveScheduler.runOrSchedule(); - } - } - - @Override - public void acknowledgeReception() { - boolean decremented = demand.tryDecrement(); - if (!decremented) { - throw new InternalError(); - } - } - - private class ReceiveTask extends SequentialScheduler.CompleteRestartableTask { - - @Override - public void run() { - while (!receiveScheduler.isStopped()) { - if (data.hasRemaining()) { - if (!demand.isFulfilled()) { - try { - int oldPos = data.position(); - reader.readFrame(data, frameConsumer); - int newPos = data.position(); - assert oldPos != newPos : data; // reader always consumes bytes - } catch (Throwable e) { - receiveScheduler.stop(); - messageConsumer.onError(e); - } - continue; - } - break; - } - switch (state) { - case WAITING: - return; - case UNREGISTERED: - try { - state = WAITING; - channel.registerEvent(readEvent); - } catch (Throwable e) { - receiveScheduler.stop(); - messageConsumer.onError(e); - } - return; - case AVAILABLE: - try { - data = channel.read(); - } catch (Throwable e) { - receiveScheduler.stop(); - messageConsumer.onError(e); - return; - } - if (data == null) { // EOF - receiveScheduler.stop(); - messageConsumer.onComplete(); - return; - } else if (!data.hasRemaining()) { - // No data at the moment Pretty much a "goto", - // reusing the existing code path for registration - state = UNREGISTERED; - } - continue; - default: - throw new InternalError(String.valueOf(state)); - } - } - } - } - - /* - * Permanently stops reading from the channel and delivering messages - * regardless of the current demand and data availability. - */ - @Override - public void closeInput() throws IOException { - synchronized (lock) { - if (!inputClosed) { - inputClosed = true; - try { - receiveScheduler.stop(); - channel.shutdownInput(); - } finally { - if (outputClosed) { - channel.close(); - } - } - } - } - } - - @Override - public void closeOutput() throws IOException { - synchronized (lock) { - if (!outputClosed) { - outputClosed = true; - try { - channel.shutdownOutput(); - } finally { - if (inputClosed) { - channel.close(); - } - } - } - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/UTF8AccumulatingDecoder.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/UTF8AccumulatingDecoder.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +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 java.net.http.internal.websocket; - -import java.net.http.internal.common.Log; - -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharacterCodingException; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CoderResult; -import java.nio.charset.CodingErrorAction; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.net.http.internal.common.Utils.EMPTY_BYTEBUFFER; - -final class UTF8AccumulatingDecoder { - - private final CharsetDecoder decoder = UTF_8.newDecoder(); - - { - decoder.onMalformedInput(CodingErrorAction.REPORT); - decoder.onUnmappableCharacter(CodingErrorAction.REPORT); - } - - private ByteBuffer leftovers = EMPTY_BYTEBUFFER; - - CharBuffer decode(ByteBuffer in, boolean endOfInput) - throws CharacterCodingException - { - ByteBuffer b; - int rem = leftovers.remaining(); - if (rem != 0) { - // We won't need this wasteful allocation & copying when JDK-8155222 - // has been resolved - b = ByteBuffer.allocate(rem + in.remaining()); - b.put(leftovers).put(in).flip(); - } else { - b = in; - } - CharBuffer out = CharBuffer.allocate(b.remaining()); - CoderResult r = decoder.decode(b, out, endOfInput); - if (r.isError()) { - r.throwException(); - } - if (b.hasRemaining()) { - leftovers = ByteBuffer.allocate(b.remaining()).put(b).flip(); - } else { - leftovers = EMPTY_BYTEBUFFER; - } - // Since it's UTF-8, the assumption is leftovers.remaining() < 4 - // (i.e. small). Otherwise a shared buffer should be used - if (!(leftovers.remaining() < 4)) { - Log.logError("The size of decoding leftovers is greater than expected: {0}", - leftovers.remaining()); - } - b.position(b.limit()); // As if we always read to the end - // Decoder promises that in the case of endOfInput == true: - // "...any remaining undecoded input will be treated as being - // malformed" - assert !(endOfInput && leftovers.hasRemaining()) : endOfInput + ", " + leftovers; - if (endOfInput) { - r = decoder.flush(out); - decoder.reset(); - if (r.isOverflow()) { - // FIXME: for now I know flush() does nothing. But the - // implementation of UTF8 decoder might change. And if now - // flush() is a no-op, it is not guaranteed to remain so in - // the future - throw new InternalError("Not yet implemented"); - } - } - return out.flip(); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/WebSocketImpl.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/WebSocketImpl.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,533 +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 java.net.http.internal.websocket; - -import java.net.http.WebSocket; -import java.net.http.internal.common.Demand; -import java.net.http.internal.common.Log; -import java.net.http.internal.common.MinimalFuture; -import java.net.http.internal.common.SequentialScheduler; -import java.net.http.internal.common.Utils; -import java.net.http.internal.websocket.OpeningHandshake.Result; - -import java.io.IOException; -import java.lang.ref.Reference; -import java.net.ProtocolException; -import java.net.URI; -import java.nio.ByteBuffer; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; - -import static java.util.Objects.requireNonNull; -import static java.net.http.internal.common.MinimalFuture.failedFuture; -import static java.net.http.internal.websocket.StatusCodes.CLOSED_ABNORMALLY; -import static java.net.http.internal.websocket.StatusCodes.NO_STATUS_CODE; -import static java.net.http.internal.websocket.StatusCodes.isLegalToSendFromClient; -import static java.net.http.internal.websocket.WebSocketImpl.State.BINARY; -import static java.net.http.internal.websocket.WebSocketImpl.State.CLOSE; -import static java.net.http.internal.websocket.WebSocketImpl.State.ERROR; -import static java.net.http.internal.websocket.WebSocketImpl.State.IDLE; -import static java.net.http.internal.websocket.WebSocketImpl.State.OPEN; -import static java.net.http.internal.websocket.WebSocketImpl.State.PING; -import static java.net.http.internal.websocket.WebSocketImpl.State.PONG; -import static java.net.http.internal.websocket.WebSocketImpl.State.TEXT; -import static java.net.http.internal.websocket.WebSocketImpl.State.WAITING; - -/* - * A WebSocket client. - */ -public final class WebSocketImpl implements WebSocket { - - enum State { - OPEN, - IDLE, - WAITING, - TEXT, - BINARY, - PING, - PONG, - CLOSE, - ERROR; - } - - private volatile boolean inputClosed; - private volatile boolean outputClosed; - - private final AtomicReference state = new AtomicReference<>(OPEN); - - /* Components of calls to Listener's methods */ - private MessagePart part; - private ByteBuffer binaryData; - private CharSequence text; - private int statusCode; - private String reason; - private final AtomicReference error = new AtomicReference<>(); - - private final URI uri; - private final String subprotocol; - private final Listener listener; - - private final AtomicBoolean outstandingSend = new AtomicBoolean(); - private final Transport transport; - private final SequentialScheduler receiveScheduler = new SequentialScheduler(new ReceiveTask()); - private final Demand demand = new Demand(); - - public static CompletableFuture newInstanceAsync(BuilderImpl b) { - Function newWebSocket = r -> { - WebSocket ws = newInstance(b.getUri(), - r.subprotocol, - b.getListener(), - r.transport); - // Make sure we don't release the builder until this lambda - // has been executed. The builder has a strong reference to - // the HttpClientFacade, and we want to keep that live until - // after the raw channel is created and passed to WebSocketImpl. - Reference.reachabilityFence(b); - return ws; - }; - OpeningHandshake h; - try { - h = new OpeningHandshake(b); - } catch (Throwable e) { - return failedFuture(e); - } - return h.send().thenApply(newWebSocket); - } - - /* Exposed for testing purposes */ - static WebSocketImpl newInstance(URI uri, - String subprotocol, - Listener listener, - TransportFactory transport) { - WebSocketImpl ws = new WebSocketImpl(uri, subprotocol, listener, transport); - // This initialisation is outside of the constructor for the sake of - // safe publication of WebSocketImpl.this - ws.signalOpen(); - return ws; - } - - private WebSocketImpl(URI uri, - String subprotocol, - Listener listener, - TransportFactory transportFactory) { - this.uri = requireNonNull(uri); - this.subprotocol = requireNonNull(subprotocol); - this.listener = requireNonNull(listener); - this.transport = transportFactory.createTransport( - () -> WebSocketImpl.this, // What about escape of WebSocketImpl.this? - new SignallingMessageConsumer()); - } - - @Override - public CompletableFuture sendText(CharSequence message, - boolean isLast) { - Objects.requireNonNull(message); - if (!outstandingSend.compareAndSet(false, true)) { - return failedFuture(new IllegalStateException("Send pending")); - } - CompletableFuture cf = transport.sendText(message, isLast); - return cf.whenComplete((r, e) -> outstandingSend.set(false)); - } - - @Override - public CompletableFuture sendBinary(ByteBuffer message, - boolean isLast) { - Objects.requireNonNull(message); - if (!outstandingSend.compareAndSet(false, true)) { - return failedFuture(new IllegalStateException("Send pending")); - } - CompletableFuture cf = transport.sendBinary(message, isLast); - // Optimize? - // if (cf.isDone()) { - // outstandingSend.set(false); - // } else { - // cf.whenComplete((r, e) -> outstandingSend.set(false)); - // } - return cf.whenComplete((r, e) -> outstandingSend.set(false)); - } - - @Override - public CompletableFuture sendPing(ByteBuffer message) { - return transport.sendPing(message); - } - - @Override - public CompletableFuture sendPong(ByteBuffer message) { - return transport.sendPong(message); - } - - @Override - public CompletableFuture sendClose(int statusCode, String reason) { - Objects.requireNonNull(reason); - if (!isLegalToSendFromClient(statusCode)) { - return failedFuture(new IllegalArgumentException("statusCode")); - } - return sendClose0(statusCode, reason); - } - - /* - * Sends a Close message, then shuts down the output since no more - * messages are expected to be sent after this. - */ - private CompletableFuture sendClose0(int statusCode, String reason ) { - outputClosed = true; - return transport.sendClose(statusCode, reason) - .whenComplete((result, error) -> { - try { - transport.closeOutput(); - } catch (IOException e) { - Log.logError(e); - } - Throwable cause = Utils.getCompletionCause(error); - if (cause instanceof TimeoutException) { - try { - transport.closeInput(); - } catch (IOException e) { - Log.logError(e); - } - } - }); - } - - @Override - public void request(long n) { - if (demand.increase(n)) { - receiveScheduler.runOrSchedule(); - } - } - - @Override - public String getSubprotocol() { - return subprotocol; - } - - @Override - public boolean isOutputClosed() { - return outputClosed; - } - - @Override - public boolean isInputClosed() { - return inputClosed; - } - - @Override - public void abort() { - inputClosed = true; - outputClosed = true; - receiveScheduler.stop(); - close(); - } - - @Override - public String toString() { - return super.toString() - + "[uri=" + uri - + (!subprotocol.isEmpty() ? ", subprotocol=" + subprotocol : "") - + "]"; - } - - /* - * The assumptions about order is as follows: - * - * - state is never changed more than twice inside the `run` method: - * x --(1)--> IDLE --(2)--> y (otherwise we're loosing events, or - * overwriting parts of messages creating a mess since there's no - * queueing) - * - OPEN is always the first state - * - no messages are requested/delivered before onOpen is called (this - * is implemented by making WebSocket instance accessible first in - * onOpen) - * - after the state has been observed as CLOSE/ERROR, the scheduler - * is stopped - */ - private class ReceiveTask extends SequentialScheduler.CompleteRestartableTask { - - // Transport only asked here and nowhere else because we must make sure - // onOpen is invoked first and no messages become pending before onOpen - // finishes - - @Override - public void run() { - while (true) { - State s = state.get(); - try { - switch (s) { - case OPEN: - processOpen(); - tryChangeState(OPEN, IDLE); - break; - case TEXT: - processText(); - tryChangeState(TEXT, IDLE); - break; - case BINARY: - processBinary(); - tryChangeState(BINARY, IDLE); - break; - case PING: - processPing(); - tryChangeState(PING, IDLE); - break; - case PONG: - processPong(); - tryChangeState(PONG, IDLE); - break; - case CLOSE: - processClose(); - return; - case ERROR: - processError(); - return; - case IDLE: - if (demand.tryDecrement() - && tryChangeState(IDLE, WAITING)) { - transport.request(1); - } - return; - case WAITING: - // For debugging spurious signalling: when there was a - // signal, but apparently nothing has changed - return; - default: - throw new InternalError(String.valueOf(s)); - } - } catch (Throwable t) { - signalError(t); - } - } - } - - private void processError() throws IOException { - transport.closeInput(); - receiveScheduler.stop(); - Throwable err = error.get(); - if (err instanceof FailWebSocketException) { - int code1 = ((FailWebSocketException) err).getStatusCode(); - err = new ProtocolException().initCause(err); - sendClose0(code1, "") - .whenComplete( - (r, e) -> { - if (e != null) { - Log.logError(e); - } - }); - } - listener.onError(WebSocketImpl.this, err); - } - - private void processClose() throws IOException { - transport.closeInput(); - receiveScheduler.stop(); - CompletionStage readyToClose; - readyToClose = listener.onClose(WebSocketImpl.this, statusCode, reason); - if (readyToClose == null) { - readyToClose = MinimalFuture.completedFuture(null); - } - int code; - if (statusCode == NO_STATUS_CODE || statusCode == CLOSED_ABNORMALLY) { - code = NORMAL_CLOSURE; - } else { - code = statusCode; - } - readyToClose.whenComplete((r, e) -> { - sendClose0(code, "") - .whenComplete((r1, e1) -> { - if (e1 != null) { - Log.logError(e1); - } - }); - }); - } - - private void processPong() { - listener.onPong(WebSocketImpl.this, binaryData); - } - - private void processPing() { - // Let's make a full copy of this tiny data. What we want here - // is to rule out a possibility the shared data we send might be - // corrupted by processing in the listener. - ByteBuffer slice = binaryData.slice(); - ByteBuffer copy = ByteBuffer.allocate(binaryData.remaining()) - .put(binaryData) - .flip(); - // Non-exclusive send; - CompletableFuture pongSent = transport.sendPong(copy); - pongSent.whenComplete( - (r, e) -> { - if (e != null) { - signalError(Utils.getCompletionCause(e)); - } - } - ); - listener.onPing(WebSocketImpl.this, slice); - } - - private void processBinary() { - listener.onBinary(WebSocketImpl.this, binaryData, part); - } - - private void processText() { - listener.onText(WebSocketImpl.this, text, part); - } - - private void processOpen() { - listener.onOpen(WebSocketImpl.this); - } - } - - private void signalOpen() { - receiveScheduler.runOrSchedule(); - } - - private void signalError(Throwable error) { - inputClosed = true; - outputClosed = true; - if (!this.error.compareAndSet(null, error) || !trySetState(ERROR)) { - Log.logError(error); - } else { - close(); - } - } - - private void close() { - try { - try { - transport.closeInput(); - } finally { - transport.closeOutput(); - } - } catch (Throwable t) { - Log.logError(t); - } - } - - /* - * Signals a Close event (might not correspond to anything happened on the - * channel, i.e. might be synthetic). - */ - private void signalClose(int statusCode, String reason) { - inputClosed = true; - this.statusCode = statusCode; - this.reason = reason; - if (!trySetState(CLOSE)) { - Log.logTrace("Close: {0}, ''{1}''", statusCode, reason); - } else { - try { - transport.closeInput(); - } catch (Throwable t) { - Log.logError(t); - } - } - } - - private class SignallingMessageConsumer implements MessageStreamConsumer { - - @Override - public void onText(CharSequence data, MessagePart part) { - transport.acknowledgeReception(); - text = data; - WebSocketImpl.this.part = part; - tryChangeState(WAITING, TEXT); - } - - @Override - public void onBinary(ByteBuffer data, MessagePart part) { - transport.acknowledgeReception(); - binaryData = data; - WebSocketImpl.this.part = part; - tryChangeState(WAITING, BINARY); - } - - @Override - public void onPing(ByteBuffer data) { - transport.acknowledgeReception(); - binaryData = data; - tryChangeState(WAITING, PING); - } - - @Override - public void onPong(ByteBuffer data) { - transport.acknowledgeReception(); - binaryData = data; - tryChangeState(WAITING, PONG); - } - - @Override - public void onClose(int statusCode, CharSequence reason) { - transport.acknowledgeReception(); - signalClose(statusCode, reason.toString()); - } - - @Override - public void onComplete() { - transport.acknowledgeReception(); - signalClose(CLOSED_ABNORMALLY, ""); - } - - @Override - public void onError(Throwable error) { - signalError(error); - } - } - - private boolean trySetState(State newState) { - while (true) { - State currentState = state.get(); - if (currentState == ERROR || currentState == CLOSE) { - return false; - } else if (state.compareAndSet(currentState, newState)) { - receiveScheduler.runOrSchedule(); - return true; - } - } - } - - private boolean tryChangeState(State expectedState, State newState) { - State witness = state.compareAndExchange(expectedState, newState); - if (witness == expectedState) { - receiveScheduler.runOrSchedule(); - return true; - } - // This should be the only reason for inability to change the state from - // IDLE to WAITING: the state has changed to terminal - if (witness != ERROR && witness != CLOSE) { - throw new InternalError(); - } - return false; - } - - /* Exposed for testing purposes */ - protected final Transport transport() { - return transport; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/java/net/http/internal/websocket/WebSocketRequest.java --- a/src/java.net.http/share/classes/java/net/http/internal/websocket/WebSocketRequest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,51 +0,0 @@ -/* - * 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 java.net.http.internal.websocket; - -import java.net.Proxy; - -/* - * https://tools.ietf.org/html/rfc6455#section-4.1 - */ -public interface WebSocketRequest { - - /* - * If set to `true` and a proxy is used, instructs the implementation that - * a TCP tunnel must be opened. - */ - void isWebSocket(boolean flag); - - /* - * Needed for setting "Connection" and "Upgrade" headers as required by the - * WebSocket specification. - */ - void setSystemHeader(String name, String value); - - /* - * Sets the proxy for this request. - */ - void setProxy(Proxy proxy); -} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/AbstractAsyncSSLConnection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AbstractAsyncSSLConnection.java Wed Feb 07 21:45:37 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.internal.net.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.internal.net.http.common.SSLTube; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.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 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); + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/AbstractSubscription.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AbstractSubscription.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.util.concurrent.Flow; +import jdk.internal.net.http.common.Demand; + +/** + * A {@link Flow.Subscription} wrapping a {@link Demand} instance. + * + */ +abstract class AbstractSubscription implements Flow.Subscription { + + private final Demand demand = new Demand(); + + /** + * Returns the subscription's demand. + * @return the subscription's demand. + */ + protected Demand demand() { return demand; } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/AsyncEvent.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AsyncEvent.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.nio.channels.SelectableChannel; + +/** + * Event handling interface from HttpClientImpl's selector. + * + * If REPEATING is set then the event is not cancelled after being posted. + */ +abstract class AsyncEvent { + + public static final int REPEATING = 0x2; // one off event if not set + + protected final int flags; + + AsyncEvent() { + this(0); + } + + AsyncEvent(int flags) { + this.flags = flags; + } + + /** Returns the channel */ + public abstract SelectableChannel channel(); + + /** Returns the selector interest op flags OR'd */ + public abstract int interestOps(); + + /** Called when event occurs */ + public abstract void handle(); + + /** + * Called when an error occurs during registration, or when the selector has + * been shut down. Aborts all exchanges. + * + * @param ioe the IOException, or null + */ + public abstract void abort(IOException ioe); + + public boolean repeating() { + return (flags & REPEATING) != 0; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLConnection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLConnection.java Wed Feb 07 21:45:37 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.internal.net.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.internal.net.http.common.SSLTube; +import jdk.internal.net.http.common.Utils; + + +/** + * Asynchronous version of SSLConnection. + */ +class AsyncSSLConnection extends AbstractAsyncSSLConnection { + + final PlainHttpConnection plainConnection; + final PlainHttpPublisher writePublisher; + private volatile SSLTube flow; + + AsyncSSLConnection(InetSocketAddress addr, + HttpClientImpl client, + String[] alpn) { + super(addr, client, Utils.getServerName(addr), alpn); + plainConnection = new PlainHttpConnection(addr, client); + writePublisher = new PlainHttpPublisher(); + } + + @Override + PlainHttpConnection plainConnection() { + return plainConnection; + } + + @Override + public CompletableFuture 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; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLTunnelConnection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLTunnelConnection.java Wed Feb 07 21:45:37 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.internal.net.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 java.net.http.HttpHeaders; +import jdk.internal.net.http.common.SSLTube; +import jdk.internal.net.http.common.Utils; + +/** + * An SSL tunnel built on a Plain (CONNECT) TCP tunnel. + */ +class AsyncSSLTunnelConnection extends AbstractAsyncSSLConnection { + + final PlainTunnelingConnection plainConnection; + final PlainHttpPublisher writePublisher; + volatile SSLTube flow; + + AsyncSSLTunnelConnection(InetSocketAddress addr, + HttpClientImpl client, + String[] alpn, + InetSocketAddress proxy, + HttpHeaders proxyHeaders) + { + super(addr, client, Utils.getServerName(addr), alpn); + this.plainConnection = new PlainTunnelingConnection(addr, proxy, client, proxyHeaders); + this.writePublisher = new PlainHttpPublisher(); + } + + @Override + public CompletableFuture 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; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/AsyncTriggerEvent.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AsyncTriggerEvent.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.nio.channels.SelectableChannel; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * An asynchronous event which is triggered only once from the selector manager + * thread as soon as event registration are handled. + */ +final class AsyncTriggerEvent extends AsyncEvent{ + + private final Runnable trigger; + private final Consumer errorHandler; + AsyncTriggerEvent(Consumer 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; } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.PasswordAuthentication; +import java.net.URI; +import java.net.InetSocketAddress; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Base64; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.WeakHashMap; +import java.net.http.HttpHeaders; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Utils; +import static java.net.Authenticator.RequestorType.PROXY; +import static java.net.Authenticator.RequestorType.SERVER; +import static java.nio.charset.StandardCharsets.ISO_8859_1; + +/** + * Implementation of Http Basic authentication. + */ +class AuthenticationFilter implements HeaderFilter { + volatile MultiExchange exchange; + private static final Base64.Encoder encoder = Base64.getEncoder(); + + static final int DEFAULT_RETRY_LIMIT = 3; + + static final int retry_limit = Utils.getIntegerNetProperty( + "jdk.httpclient.auth.retrylimit", DEFAULT_RETRY_LIMIT); + + static final int UNAUTHORIZED = 401; + static final int PROXY_UNAUTHORIZED = 407; + + private static final List 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 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 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); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/BufferingSubscriber.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/BufferingSubscriber.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; +import java.net.http.HttpResponse.BodySubscriber; +import jdk.internal.net.http.common.Demand; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; + +/** + * A buffering BodySubscriber. When subscribed, accumulates ( buffers ) a given + * amount ( in bytes ) of a publisher's data before pushing it to a downstream + * subscriber. + */ +public class BufferingSubscriber implements BodySubscriber +{ + /** The downstream consumer of the data. */ + private final BodySubscriber downstreamSubscriber; + /** The amount of data to be accumulate before pushing downstream. */ + private final int bufferSize; + + /** The subscription, created lazily. */ + private volatile Flow.Subscription subscription; + /** The downstream subscription, created lazily. */ + private volatile DownstreamSubscription downstreamSubscription; + + /** Must be held when accessing the internal buffers. */ + private final Object buffersLock = new Object(); + /** The internal buffers holding the buffered data. */ + private ArrayList internalBuffers; + /** The actual accumulated remaining bytes in internalBuffers. */ + private int accumulatedBytes; + + /** Holds the Throwable from upstream's onError. */ + private volatile Throwable throwable; + + /** State of the buffering subscriber: + * 1) [UNSUBSCRIBED] when initially created + * 2) [ACTIVE] when subscribed and can receive data + * 3) [ERROR | CANCELLED | COMPLETE] (terminal state) + */ + static final int UNSUBSCRIBED = 0x01; + static final int ACTIVE = 0x02; + static final int ERROR = 0x04; + static final int CANCELLED = 0x08; + static final int COMPLETE = 0x10; + + private volatile int state; + + public BufferingSubscriber(BodySubscriber downstreamSubscriber, + int bufferSize) { + this.downstreamSubscriber = Objects.requireNonNull(downstreamSubscriber); + this.bufferSize = bufferSize; + synchronized (buffersLock) { + internalBuffers = new ArrayList<>(); + } + state = UNSUBSCRIBED; + } + + /** Returns the number of bytes remaining in the given buffers. */ + private static final long remaining(List buffers) { + return buffers.stream().mapToLong(ByteBuffer::remaining).sum(); + } + + /** + * Tells whether, or not, there is at least a sufficient number of bytes + * accumulated in the internal buffers. If the subscriber is COMPLETE, and + * has some buffered data, then there is always enough ( to pass downstream ). + */ + private final boolean hasEnoughAccumulatedBytes() { + assert Thread.holdsLock(buffersLock); + return accumulatedBytes >= bufferSize + || (state == COMPLETE && accumulatedBytes > 0); + } + + /** + * Returns a new, unmodifiable, List containing exactly the + * amount of data as required before pushing downstream. The amount of data + * may be less than required ( bufferSize ), in the case where the subscriber + * is COMPLETE. + */ + private List fromInternalBuffers() { + assert Thread.holdsLock(buffersLock); + int leftToFill = bufferSize; + int state = this.state; + assert (state == ACTIVE || state == CANCELLED) + ? accumulatedBytes >= leftToFill : true; + List dsts = new ArrayList<>(); + + ListIterator itr = internalBuffers.listIterator(); + while (itr.hasNext()) { + ByteBuffer b = itr.next(); + if (b.remaining() <= leftToFill) { + itr.remove(); + if (b.position() != 0) + b = b.slice(); // ensure position = 0 when propagated + dsts.add(b); + leftToFill -= b.remaining(); + accumulatedBytes -= b.remaining(); + if (leftToFill == 0) + break; + } else { + int prevLimit = b.limit(); + b.limit(b.position() + leftToFill); + ByteBuffer slice = b.slice(); + dsts.add(slice); + b.limit(prevLimit); + b.position(b.position() + leftToFill); + accumulatedBytes -= leftToFill; + leftToFill = 0; + break; + } + } + assert (state == ACTIVE || state == CANCELLED) + ? leftToFill == 0 : state == COMPLETE; + assert (state == ACTIVE || state == CANCELLED) + ? remaining(dsts) == bufferSize : state == COMPLETE; + assert accumulatedBytes >= 0; + assert dsts.stream().noneMatch(b -> b.position() != 0); + return Collections.unmodifiableList(dsts); + } + + /** Subscription that is passed to the downstream subscriber. */ + private class DownstreamSubscription implements Flow.Subscription { + private final AtomicBoolean cancelled = new AtomicBoolean(); // false + private final Demand demand = new Demand(); + private volatile boolean illegalArg; + + @Override + public void request(long n) { + if (cancelled.get() || illegalArg) { + return; + } + if (n <= 0L) { + // pass the "bad" value upstream so the Publisher can deal with + // it appropriately, i.e. invoke onError + illegalArg = true; + subscription.request(n); + return; + } + + demand.increase(n); + + pushDemanded(); + } + + private final SequentialScheduler pushDemandedScheduler = + new SequentialScheduler(new PushDemandedTask()); + + void pushDemanded() { + if (cancelled.get()) + return; + pushDemandedScheduler.runOrSchedule(); + } + + class PushDemandedTask extends SequentialScheduler.CompleteRestartableTask { + @Override + public void run() { + try { + Throwable t = throwable; + if (t != null) { + pushDemandedScheduler.stop(); // stop the demand scheduler + downstreamSubscriber.onError(t); + return; + } + + while (true) { + List item; + synchronized (buffersLock) { + if (cancelled.get()) + return; + if (!hasEnoughAccumulatedBytes()) + break; + if (!demand.tryDecrement()) + break; + item = fromInternalBuffers(); + } + assert item != null; + + downstreamSubscriber.onNext(item); + } + if (cancelled.get()) + return; + + // complete only if all data consumed + boolean complete; + synchronized (buffersLock) { + complete = state == COMPLETE && internalBuffers.isEmpty(); + } + if (complete) { + assert internalBuffers.isEmpty(); + pushDemandedScheduler.stop(); // stop the demand scheduler + downstreamSubscriber.onComplete(); + return; + } + } catch (Throwable t) { + cancel(); // cancel if there is any error + throw t; + } + + boolean requestMore = false; + synchronized (buffersLock) { + if (!hasEnoughAccumulatedBytes() && !demand.isFulfilled()) { + // request more upstream data + requestMore = true; + } + } + if (requestMore) + subscription.request(1); + } + } + + @Override + public void cancel() { + if (cancelled.compareAndExchange(false, true)) + return; // already cancelled + + state = CANCELLED; // set CANCELLED state of upstream subscriber + subscription.cancel(); // cancel upstream subscription + pushDemandedScheduler.stop(); // stop the demand scheduler + } + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + Objects.requireNonNull(subscription); + if (this.subscription != null) { + subscription.cancel(); + return; + } + + int s = this.state; + assert s == UNSUBSCRIBED; + state = ACTIVE; + this.subscription = subscription; + downstreamSubscription = new DownstreamSubscription(); + downstreamSubscriber.onSubscribe(downstreamSubscription); + } + + @Override + public void onNext(List item) { + Objects.requireNonNull(item); + + int s = state; + if (s == CANCELLED) + return; + + if (s != ACTIVE) + throw new InternalError("onNext on inactive subscriber"); + + synchronized (buffersLock) { + internalBuffers.addAll(item); + accumulatedBytes += remaining(item); + } + + downstreamSubscription.pushDemanded(); + } + + @Override + public void onError(Throwable incomingThrowable) { + Objects.requireNonNull(incomingThrowable); + int s = state; + assert s == ACTIVE : "Expected ACTIVE, got:" + s; + state = ERROR; + Throwable t = this.throwable; + assert t == null : "Expected null, got:" + t; + this.throwable = incomingThrowable; + downstreamSubscription.pushDemanded(); + } + + @Override + public void onComplete() { + int s = state; + assert s == ACTIVE : "Expected ACTIVE, got:" + s; + state = COMPLETE; + downstreamSubscription.pushDemanded(); + } + + @Override + public CompletionStage getBody() { + return downstreamSubscriber.getBody(); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/ConnectionPool.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ConnectionPool.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.lang.System.Logger.Level; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.Flow; +import java.util.stream.Collectors; +import jdk.internal.net.http.common.FlowTube; +import jdk.internal.net.http.common.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> plainPool; + private final HashMap> 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> pool) { + LinkedList 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> pool) { + //System.out.println("cacheCleaner removing: " + c); + assert Thread.holdsLock(this); + CacheKey k = c.cacheKey(); + List l = pool.get(k); + if (l == null || l.isEmpty()) { + pool.remove(k); + return false; + } + return l.remove(c); + } + + private void + putConnection(HttpConnection c, + HashMap> pool) { + CacheKey key = c.cacheKey(); + LinkedList 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 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 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 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 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 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 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 purgeUntil(Instant now) { + if (list.isEmpty()) return Collections.emptyList(); + + List 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 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 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 item) { + triggerCleanup(new IOException("Data received while in pool")); + } + + @Override + public void subscribe(Flow.Subscriber> subscriber) { + subscriber.onSubscribe(this); + } + + @Override + public String toString() { + return "CleanupTrigger(" + connection.getConnectionFlow() + ")"; + } + + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/CookieFilter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/CookieFilter.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.net.CookieHandler; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.net.http.HttpHeaders; +import jdk.internal.net.http.common.HttpHeadersImpl; +import jdk.internal.net.http.common.Log; + +class CookieFilter implements HeaderFilter { + + public CookieFilter() { + } + + @Override + public void request(HttpRequestImpl r, MultiExchange e) throws IOException { + HttpClientImpl client = e.client(); + Optional cookieHandlerOpt = client.cookieHandler(); + if (cookieHandlerOpt.isPresent()) { + CookieHandler cookieHandler = cookieHandlerOpt.get(); + Map> userheaders = r.getUserHeaders().map(); + Map> 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 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 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; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.lang.System.Logger.Level; +import java.net.InetSocketAddress; +import java.net.ProxySelector; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLPermission; +import java.security.AccessControlContext; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Function; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import java.net.http.HttpTimeoutException; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.common.Log; + +import static jdk.internal.net.http.common.Utils.permissionForProxy; + +/** + * One request/response exchange (handles 100/101 intermediate response also). + * depth field used to track number of times a new request is being sent + * for a given API request. If limit exceeded exception is thrown. + * + * Security check is performed here: + * - uses AccessControlContext captured at API level + * - checks for appropriate URLPermission for request + * - if permission allowed, grants equivalent SocketPermission to call + * - in case of direct HTTP proxy, checks additionally for access to proxy + * (CONNECT proxying uses its own Exchange, so check done there) + * + */ +final class Exchange { + + 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 exchImpl; + volatile CompletableFuture> exchangeCF; + volatile CompletableFuture bodyIgnored; + + // used to record possible cancellation raised before the exchImpl + // has been established. + private volatile IOException failed; + final AccessControlContext acc; + final MultiExchange multi; + final Executor parentExecutor; + boolean upgrading; // to HTTP/2 + final PushGroup pushGroup; + final String dbgTag; + + Exchange(HttpRequestImpl request, MultiExchange 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 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 getPushGroup() { + return pushGroup; + } + + Executor executor() { + return parentExecutor; + } + + public HttpRequestImpl request() { + return request; + } + + HttpClientImpl client() { + return client; + } + + + public CompletableFuture readBodyAsync(HttpResponse.BodyHandler 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 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> 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> + 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> 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 responseAsync() { + return responseAsyncImpl(null); + } + + CompletableFuture 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 checkFor407(ExchangeImpl ex, Throwable t, + Function,CompletableFuture> 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 expectContinue(ExchangeImpl 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 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 sendRequestBody(ExchangeImpl ex) { + assert !request.expectContinue(); + CompletableFuture cf = ex.sendBodyAsync() + .thenCompose(exIm -> exIm.getResponseAsync(parentExecutor)); + cf = wrapForUpgrade(cf); + cf = wrapForLog(cf); + return cf; + } + + CompletableFuture responseAsyncImpl0(HttpConnection connection) { + Function, CompletableFuture> 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, CompletableFuture> 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 wrapForUpgrade(CompletableFuture cf) { + if (upgrading) { + return cf.thenCompose(r -> checkForUpgradeAsync(r, exchImpl)); + } + return cf; + } + + private CompletableFuture wrapForLog(CompletableFuture cf) { + if (Log.requests()) { + return cf.thenApply(response -> { + Log.logResponse(response::toString); + return response; + }); + } + return cf; + } + + HttpResponse.BodySubscriber ignoreBody(int status, HttpHeaders hdrs) { + return HttpResponse.BodySubscriber.replace(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 + checkForUpgradeAsync(Response resp, + ExchangeImpl ex) { + + int rcode = resp.statusCode(); + if (upgrading && (rcode == 101)) { + Http1Exchange e = (Http1Exchange)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 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> 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; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java Wed Feb 07 21:45:37 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.internal.net.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 java.net.http.HttpResponse; +import jdk.internal.net.http.common.MinimalFuture; +import static java.net.http.HttpClient.Version.HTTP_1_1; +import jdk.internal.net.http.common.Utils; + +/** + * Splits request so that headers and body can be sent separately with optional + * (multiple) responses in between (e.g. 100 Continue). Also request and + * response always sent/received in different calls. + * + * Synchronous and asynchronous versions of each method are provided. + * + * Separate implementations of this class exist for HTTP/1.1 and HTTP/2 + * Http1Exchange (HTTP/1.1) + * Stream (HTTP/2) + * + * These implementation classes are where work is allocated to threads. + */ +abstract class ExchangeImpl { + + static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag. + private static final System.Logger DEBUG_LOGGER = + Utils.getDebugLogger("ExchangeImpl"::toString, DEBUG); + + final Exchange exchange; + + ExchangeImpl(Exchange e) { + // e == null means a http/2 pushed stream + this.exchange = e; + } + + final Exchange 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 CompletableFuture> + get(Exchange 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 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 CompletableFuture> + createExchangeImpl(Http2Connection c, + Throwable t, + Exchange 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> 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> 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 s = c.createStream(exchange); + CompletableFuture> ex = MinimalFuture.completedFuture(s); + return ex; + } + } + + private static CompletableFuture> + createHttp1Exchange(Exchange 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> sendHeadersAsync(); + + /** Sends a request body, after request headers have been sent. */ + abstract CompletableFuture> sendBodyAsync(); + + abstract CompletableFuture readBodyAsync(HttpResponse.BodyHandler handler, + boolean returnConnectionToPool, + Executor executor); + + /** + * Ignore/consume the body. + */ + abstract CompletableFuture ignoreBody(); + + /** Gets the response headers. Completes before body is read. */ + abstract CompletableFuture 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(); +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/FilterFactory.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/FilterFactory.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.util.LinkedList; +import java.util.List; + +class FilterFactory { + + final LinkedList> filterClasses = new LinkedList<>(); + + public void addFilter(Class type) { + filterClasses.add(type); + } + + List getFilterChain() { + List l = new LinkedList<>(); + for (Class 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; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/HeaderFilter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HeaderFilter.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; + +/** + * A header filter that can examine or modify, typically system headers for + * requests before they are sent, and responses before they are returned to the + * user. Some ability to resend requests is provided. + */ +interface HeaderFilter { + + void request(HttpRequestImpl r, MultiExchange e) throws IOException; + + /** + * Returns null if response ok to be given to user. Non null is a request + * that must be resent and its response given to user. If impl throws an + * exception that is returned to user instead. + */ + HttpRequestImpl response(Response r) throws IOException; +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/HeaderParser.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HeaderParser.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.util.Iterator; +import java.util.Locale; +import java.util.NoSuchElementException; + +/* This is useful for the nightmare of parsing multi-part HTTP/RFC822 headers + * sensibly: + * From a String like: 'timeout=15, max=5' + * create an array of Strings: + * { {"timeout", "15"}, + * {"max", "5"} + * } + * From one like: 'Basic Realm="FuzzFace" Foo="Biz Bar Baz"' + * create one like (no quotes in literal): + * { {"basic", null}, + * {"realm", "FuzzFace"} + * {"foo", "Biz Bar Baz"} + * } + * keys are converted to lower case, vals are left as is.... + */ +class HeaderParser { + + /* table of key/val pairs */ + String raw; + String[][] tab; + int nkeys; + int asize = 10; // initial size of array is 10 + + public HeaderParser(String raw) { + this.raw = raw; + tab = new String[asize][2]; + parse(); + } + +// private HeaderParser () { } + +// /** +// * Creates a new HeaderParser from this, whose keys (and corresponding +// * values) range from "start" to "end-1" +// */ +// public HeaderParser subsequence(int start, int end) { +// if (start == 0 && end == nkeys) { +// return this; +// } +// if (start < 0 || start >= end || end > nkeys) { +// throw new IllegalArgumentException("invalid start or end"); +// } +// HeaderParser n = new HeaderParser(); +// n.tab = new String [asize][2]; +// n.asize = asize; +// System.arraycopy (tab, start, n.tab, 0, (end-start)); +// n.nkeys= (end-start); +// return n; +// } + + private void parse() { + + if (raw != null) { + raw = raw.trim(); + char[] ca = raw.toCharArray(); + int beg = 0, end = 0, i = 0; + boolean inKey = true; + boolean inQuote = false; + int len = ca.length; + while (end < len) { + char c = ca[end]; + if ((c == '=') && !inQuote) { // end of a key + tab[i][0] = new String(ca, beg, end-beg).toLowerCase(Locale.US); + inKey = false; + end++; + beg = end; + } else if (c == '\"') { + if (inQuote) { + tab[i++][1]= new String(ca, beg, end-beg); + inQuote=false; + do { + end++; + } while (end < len && (ca[end] == ' ' || ca[end] == ',')); + inKey=true; + beg=end; + } else { + inQuote=true; + end++; + beg=end; + } + } else if (c == ' ' || c == ',') { // end key/val, of whatever we're in + if (inQuote) { + end++; + continue; + } else if (inKey) { + tab[i++][0] = (new String(ca, beg, end-beg)).toLowerCase(Locale.US); + } else { + tab[i++][1] = (new String(ca, beg, end-beg)); + } + while (end < len && (ca[end] == ' ' || ca[end] == ',')) { + end++; + } + inKey = true; + beg = end; + } else { + end++; + } + if (i == asize) { + asize = asize * 2; + String[][] ntab = new String[asize][2]; + System.arraycopy (tab, 0, ntab, 0, tab.length); + tab = ntab; + } + } + // get last key/val, if any + if (--end > beg) { + if (!inKey) { + if (ca[end] == '\"') { + tab[i++][1] = (new String(ca, beg, end-beg)); + } else { + tab[i++][1] = (new String(ca, beg, end-beg+1)); + } + } else { + tab[i++][0] = (new String(ca, beg, end-beg+1)).toLowerCase(); + } + } 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 { + int index; + boolean returnsValue; // or key + + ParserIterator (boolean returnValue) { + returnsValue = returnValue; + } + @Override + public boolean hasNext () { + return index= nkeys) { + throw new NoSuchElementException(); + } + return tab[index++][returnsValue?1:0]; + } + } + + public Iterator keys () { + return new ParserIterator (false); + } + +// public Iterator values () { +// return new ParserIterator (true); +// } + + @Override + public String toString () { + Iterator 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; +// } +// } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/Http1AsyncReceiver.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1AsyncReceiver.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.EOFException; +import java.io.IOException; +import java.lang.System.Logger.Level; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Executor; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import jdk.internal.net.http.common.Demand; +import jdk.internal.net.http.common.FlowTube.TubeSubscriber; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.ConnectionExpiredException; +import jdk.internal.net.http.common.Utils; + + +/** + * A helper class that will queue up incoming data until the receiving + * side is ready to handle it. + */ +class Http1AsyncReceiver { + + 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 queue + = new ConcurrentLinkedDeque<>(); + private final SequentialScheduler scheduler = + SequentialScheduler.synchronizedScheduler(this::flush); + private final Executor executor; + private final Http1TubeSubscriber subscriber = new Http1TubeSubscriber(); + private final AtomicReference 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 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 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= 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 lbb = Arrays.asList(qbb); + Set 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; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.lang.System.Logger.Level; +import java.net.InetSocketAddress; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodySubscriber; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Executor; +import java.util.concurrent.Flow; +import jdk.internal.net.http.common.Demand; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.FlowTube; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import static java.net.http.HttpClient.Version.HTTP_1_1; + +/** + * Encapsulates one HTTP/1.1 request/response exchange. + */ +class Http1Exchange extends ExchangeImpl { + + 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 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> 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 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> headersSentCF = new MinimalFuture<>(); + /** Completed when the body has been published, or there is an error */ + private final CompletableFuture> 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 data; + DataPair(List 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 { + protected volatile Flow.Subscription subscription; + protected volatile boolean complete; + + /** Final sentinel in the stream of request body. */ + static final List 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 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> 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 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 cf = new MinimalFuture<>(); + try { + connectFlows(connection); + + debug.log(Level.DEBUG, "requestAction.headers"); + List 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> 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 getResponseAsync(Executor executor) { + CompletableFuture 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 readBodyAsync(BodyHandler handler, + boolean returnConnectionToPool, + Executor executor) + { + BodySubscriber bs = handler.apply(response.responseCode(), + response.responseHeaders()); + CompletableFuture bodyCF = response.readBody(bs, + returnConnectionToPool, + executor); + return bodyCF; + } + + @Override + CompletableFuture ignoreBody() { + return response.ignoreBody(executor); + } + + ByteBuffer drainLeftOverBytes() { + synchronized (lock) { + asyncReceiver.stop(); + return asyncReceiver.drain(Utils.EMPTY_BYTEBUFFER); + } + } + + void released() { + Http1Response resp = this.response; + if (resp != null) resp.completed(); + asyncReceiver.clear(); + } + + void completed() { + Http1Response 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> 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 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> 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> 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 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"; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.net.http.HttpHeaders; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +class Http1HeaderParser { + + private static final char CR = '\r'; + private static final char LF = '\n'; + private static final char HT = '\t'; + private static final char SP = ' '; + + private StringBuilder sb = new StringBuilder(); + private String statusLine; + private int responseCode; + private HttpHeaders headers; + private Map> 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)); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.lang.System.Logger.Level; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.net.InetSocketAddress; +import java.util.Objects; +import java.util.concurrent.Flow; +import java.util.function.BiPredicate; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import jdk.internal.net.http.Http1Exchange.Http1BodySubscriber; +import jdk.internal.net.http.common.HttpHeadersImpl; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.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> 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> filter) { + for (Map.Entry> entry : headers.map().entrySet()) { + String key = entry.getKey(); + List 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 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 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 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); + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/Http1Response.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Response.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,525 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import java.io.EOFException; +import java.lang.System.Logger.Level; +import java.nio.ByteBuffer; +import java.util.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 java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import jdk.internal.net.http.ResponseContent.BodyParser; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import static java.net.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 { + + private volatile ResponseContent content; + private final HttpRequestImpl request; + private Response response; + private final HttpConnection connection; + private HttpHeaders headers; + private int responseCode; + private final Http1Exchange 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 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 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 cf = headersReader.completion(); + assert cf != null : "parsing not started"; + + Function 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 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(), true, executor); + } + } + + public CompletableFuture readBody(HttpResponse.BodySubscriber p, + boolean return2Cache, + Executor executor) { + this.return2Cache = return2Cache; + final HttpResponse.BodySubscriber pusher = p; + + final CompletableFuture cf = new MinimalFuture<>(); + + int clen0 = (int)headers.firstValueAsLong("Content-Length").orElse(-1); + + final int clen = fixupContentLen(clen0); + + // expect-continue reads headers and body twice. + // if we reach here, we must reset the headersReader state. + asyncReceiver.unsubscribe(headersReader); + headersReader.reset(); + + 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 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); + } + } + }); + p.getBody().whenComplete((U u, Throwable t) -> { + if (t == null) + cf.complete(u); + else + cf.completeExceptionally(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 + implements Http1AsyncReceiver.Http1AsyncDelegate { + abstract void start(T parser); + abstract CompletableFuture 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 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 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 { + final Consumer onComplete; + volatile Http1HeaderParser parser; + volatile CompletableFuture cf; + volatile long count; // bytes parsed (for debug) + volatile AbstractSubscription subscription; + + HeadersReader(Consumer 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 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 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 { + final Consumer onComplete; + volatile BodyParser parser; + volatile CompletableFuture cf; + volatile AbstractSubscription subscription; + BodyReader(Consumer 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 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 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); + } + + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java Wed Feb 07 21:45:37 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.internal.net.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.internal.net.http.common.Log; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.frame.SettingsFrame; +import static jdk.internal.net.http.frame.SettingsFrame.INITIAL_WINDOW_SIZE; +import static jdk.internal.net.http.frame.SettingsFrame.ENABLE_PUSH; +import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE; +import static jdk.internal.net.http.frame.SettingsFrame.MAX_CONCURRENT_STREAMS; +import static jdk.internal.net.http.frame.SettingsFrame.MAX_FRAME_SIZE; + +/** + * Http2 specific aspects of HttpClientImpl + */ +class Http2ClientImpl { + + 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 connections = new ConcurrentHashMap<>(); + + private final Set 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 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; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java Wed Feb 07 21:45:37 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.internal.net.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 java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import jdk.internal.net.http.HttpConnection.HttpPublisher; +import jdk.internal.net.http.common.FlowTube; +import jdk.internal.net.http.common.FlowTube.TubeSubscriber; +import jdk.internal.net.http.common.HttpHeadersImpl; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.frame.ContinuationFrame; +import jdk.internal.net.http.frame.DataFrame; +import jdk.internal.net.http.frame.ErrorFrame; +import jdk.internal.net.http.frame.FramesDecoder; +import jdk.internal.net.http.frame.FramesEncoder; +import jdk.internal.net.http.frame.GoAwayFrame; +import jdk.internal.net.http.frame.HeaderFrame; +import jdk.internal.net.http.frame.HeadersFrame; +import jdk.internal.net.http.frame.Http2Frame; +import jdk.internal.net.http.frame.MalformedFrame; +import jdk.internal.net.http.frame.OutgoingHeaders; +import jdk.internal.net.http.frame.PingFrame; +import jdk.internal.net.http.frame.PushPromiseFrame; +import jdk.internal.net.http.frame.ResetFrame; +import jdk.internal.net.http.frame.SettingsFrame; +import jdk.internal.net.http.frame.WindowUpdateFrame; +import jdk.internal.net.http.hpack.Encoder; +import jdk.internal.net.http.hpack.Decoder; +import jdk.internal.net.http.hpack.DecodingCallback; +import static java.nio.charset.StandardCharsets.UTF_8; +import static jdk.internal.net.http.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 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 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> 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 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 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 createAsync(HttpConnection connection, + Http2ClientImpl client2, + Exchange exchange, + Supplier initial) + { + return MinimalFuture.supply(() -> new Http2Connection(connection, client2, exchange, initial)); + } + + // Requires TLS handshake. So, is really async + static CompletableFuture 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 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> checkAlpnCF = (alpn) -> { + CompletableFuture 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 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 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> 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 void handlePushPromise(Stream 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 pushExch = new Exchange<>(pushReq, parent.exchange.multi); + Stream.PushedStream 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") + Stream getStream(int streamid) { + return (Stream)streams.get(streamid); + } + + /** + * Creates Stream with given id. + */ + final Stream createStream(Exchange exchange) { + Stream stream = new Stream<>(this, exchange, windowController); + return stream; + } + + Stream.PushedStream createPushStream(Stream parent, Exchange pushEx) { + PushGroup pg = parent.exchange.getPushGroup(); + return new Stream.PushedStream<>(pg, this, pushEx); + } + + void putStream(Stream 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 and then create HEADERS + * and CONTINUATION frames from the list and return the List. + */ + private List encodeHeaders(OutgoingHeaders> frame) { + List buffers = encodeHeadersImpl( + getMaxSendFrameSize(), + frame.getAttachment().getRequestPseudoHeaders(), + frame.getUserHeaders(), + frame.getSystemHeaders()); + + List frames = new ArrayList<>(buffers.size()); + Iterator 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 encodeHeadersImpl(int maxFrameSize, HttpHeaders... headers) { + ByteBuffer buffer = getHeaderBuffer(maxFrameSize); + List buffers = new ArrayList<>(); + for(HttpHeaders header : headers) { + for (Map.Entry> e : header.map().entrySet()) { + String lKey = e.getKey().toLowerCase(); + List 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 encodeHeaders(OutgoingHeaders> 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 frames = encodeHeaders(oh); + return encodeFrames(frames); + } + + private List encodeFrames(List frames) { + if (Log.frames()) { + frames.forEach(f -> Log.logFrames(f, "OUT")); + } + return framesEncoder.encodeFrames(frames); + } + + private Stream registerNewStream(OutgoingHeaders> 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> oh = (OutgoingHeaders>) 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 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 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 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; + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/HttpClientBuilderImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientBuilderImpl.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.net.Authenticator; +import java.net.CookieHandler; +import java.net.ProxySelector; +import java.util.concurrent.Executor; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.net.http.HttpClient; +import jdk.internal.net.http.common.Utils; +import static java.util.Objects.requireNonNull; + +public class HttpClientBuilderImpl 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); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/HttpClientFacade.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientFacade.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.lang.ref.Reference; +import java.net.Authenticator; +import java.net.CookieHandler; +import java.net.ProxySelector; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.net.http.WebSocket; + +/** + * 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() { + return impl.cookieHandler(); + } + + @Override + public Redirect followRedirects() { + return impl.followRedirects(); + } + + @Override + public Optional proxy() { + return impl.proxy(); + } + + @Override + public SSLContext sslContext() { + return impl.sslContext(); + } + + @Override + public SSLParameters sslParameters() { + return impl.sslParameters(); + } + + @Override + public Optional authenticator() { + return impl.authenticator(); + } + + @Override + public HttpClient.Version version() { + return impl.version(); + } + + @Override + public Optional executor() { + return impl.executor(); + } + + @Override + public HttpResponse + send(HttpRequest req, HttpResponse.BodyHandler responseBodyHandler) + throws IOException, InterruptedException + { + try { + return impl.send(req, responseBodyHandler); + } finally { + Reference.reachabilityFence(this); + } + } + + @Override + public CompletableFuture> + sendAsync(HttpRequest req, HttpResponse.BodyHandler responseBodyHandler) { + try { + return impl.sendAsync(req, responseBodyHandler); + } finally { + Reference.reachabilityFence(this); + } + } + + @Override + public CompletableFuture> + sendAsync(HttpRequest req, + BodyHandler responseBodyHandler, + PushPromiseHandler 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(); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,1023 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import 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 java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.net.http.WebSocket; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Pair; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.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 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 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 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 timeouts; + + /** + * This is a bit tricky: + * 1. an HttpClientFacade has a final HttpClientImpl field. + * 2. an HttpClientImpl has a final WeakReference 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 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 HttpResponse + send(HttpRequest req, BodyHandler 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 CompletableFuture> + sendAsync(HttpRequest userRequest, BodyHandler responseHandler) + { + return sendAsync(userRequest, responseHandler, null); + } + + + @Override + public CompletableFuture> + sendAsync(HttpRequest userRequest, + BodyHandler responseHandler, + PushPromiseHandler 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 mex = new MultiExchange<>(userRequest, + requestImpl, + this, + responseHandler, + pushPromiseHandler, + acc); + CompletableFuture> 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= + 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 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); + if (key != null) { + 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> errorList = new ArrayList<>(); + List 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 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 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 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. + * + *

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 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 + try { + chan.register(selector, interestOps, this); + } catch (CancelledKeyException x) { + abortPending(x); + } + } + } + + /** + * Returns a Stream containing only events that are + * registered with the given {@code interestOps}. + */ + Stream 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 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() { + return Optional.ofNullable(authenticator); + } + + /*package-private*/ final Executor theExecutor() { + return executor; + } + + @Override + public final Optional executor() { + return isDefaultExecutor ? Optional.empty() : Optional.of(executor); + } + + ConnectionPool connectionPool() { + return connections; + } + + @Override + public Redirect followRedirects() { + return followRedirects; + } + + + @Override + public Optional cookieHandler() { + return Optional.ofNullable(cookieHandler); + } + + @Override + public Optional 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 f) { + filters.addFilter(f); + } + + final List 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 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 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 + ); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.Closeable; +import java.io.IOException; +import java.lang.System.Logger.Level; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Flow; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import jdk.internal.net.http.common.Demand; +import jdk.internal.net.http.common.FlowTube; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.SequentialScheduler.DeferredCompleter; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Utils; +import static java.net.http.HttpClient.Version.HTTP_2; + +/** + * Wraps socket channel layer and takes care of SSL also. + * + * Subtypes are: + * PlainHttpConnection: regular direct TCP connection to server + * PlainProxyConnection: plain text proxy connection + * PlainTunnelingConnection: opens plain text (CONNECT) tunnel to server + * AsyncSSLConnection: TLS channel direct to server + * AsyncSSLTunnelConnection: TLS channel via (CONNECT) proxy tunnel + */ +abstract class HttpConnection implements Closeable { + + 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, 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 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 buffers) throws IOException; + void enqueueUnordered(List 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 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: + *

+     *    - 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).
+     * 
+ * @param request + * @return + */ + BiPredicate> 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> 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> queue = new ConcurrentLinkedDeque<>(); + volatile Flow.Subscriber> subscriber; + volatile HttpWriteSubscription subscription; + final SequentialScheduler writeScheduler = + new SequentialScheduler(this::flushTask); + @Override + public void subscribe(Flow.Subscriber> 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 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 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 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(); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.net.URI; +import java.time.Duration; +import java.util.Optional; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublisher; +import jdk.internal.net.http.common.HttpHeadersImpl; +import jdk.internal.net.http.common.Utils; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.common.Utils.isValidName; +import static jdk.internal.net.http.common.Utils.isValidValue; + +public class HttpRequestBuilderImpl extends HttpRequest.Builder { + + private HttpHeadersImpl userHeaders; + private URI uri; + private String method; + private boolean expectContinue; + private BodyPublisher bodyPublisher; + private volatile Optional 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 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 "); + 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; } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.URI; +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.time.Duration; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import jdk.internal.net.http.common.HttpHeadersImpl; +import jdk.internal.net.http.websocket.WebSocketRequest; + +import static jdk.internal.net.http.common.Utils.ALLOWED_HEADERS; + +class HttpRequestImpl extends HttpRequest implements WebSocketRequest { + + private final HttpHeaders userHeaders; + private final HttpHeadersImpl systemHeaders; + private final URI uri; + private volatile Proxy proxy; // ensure safe publishing + private final InetSocketAddress authority; // only used when URI not specified + private final String method; + final BodyPublisher requestPublisher; + final boolean secure; + final boolean expectContinue; + private volatile boolean isWebSocket; + private volatile AccessControlContext acc; + private final Duration timeout; // may be null + private final Optional version; + + private static String userAgent() { + PrivilegedAction 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 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() { + 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 timeout() { + return timeout == null ? Optional.empty() : Optional.of(timeout); + } + + HttpHeaders getUserHeaders() { return userHeaders; } + + HttpHeadersImpl getSystemHeaders() { return systemHeaders; } + + @Override + public Optional 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 pa = () -> new InetSocketAddress(host, port); + return AccessController.doPrivileged(pa); + } else { + return InetSocketAddress.createUnresolved(host, port); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import javax.net.ssl.SSLParameters; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import jdk.internal.net.http.websocket.RawChannel; + +/** + * The implementation class for HttpResponse + */ +class HttpResponseImpl extends HttpResponse implements RawChannel.Provider { + + final int responseCode; + final Exchange exchange; + final HttpRequest initialRequest; + final Optional> previousResponse; + final HttpHeaders headers; + final SSLParameters sslParameters; + final URI uri; + final HttpClient.Version version; + RawChannel rawchan; + final HttpConnection connection; + final Stream stream; + final T body; + + public HttpResponseImpl(HttpRequest initialRequest, + Response response, + HttpResponse previousResponse, + T body, + Exchange 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> 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 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(); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHeaders.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHeaders.java Wed Feb 07 21:45:37 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.internal.net.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 java.net.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> map; + + public static ImmutableHeaders empty() { + return of(emptyMap()); + } + + public static ImmutableHeaders of(Map> 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> src, + Predicate keyAllowed) { + requireNonNull(src, "src"); + requireNonNull(keyAllowed, "keyAllowed"); + return new ImmutableHeaders(src, headerAllowed(keyAllowed)); + } + + public static ImmutableHeaders of(Map> src, + BiPredicate> headerAllowed) { + requireNonNull(src, "src"); + requireNonNull(headerAllowed, "headerAllowed"); + return new ImmutableHeaders(src, headerAllowed); + } + + private ImmutableHeaders(Map> src, + BiPredicate> headerAllowed) { + Map> m = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + src.entrySet().stream() + .filter(e -> headerAllowed.test(e.getKey(), e.getValue())) + .forEach(e -> + { + List values = new ArrayList<>(e.getValue()); + m.put(e.getKey(), unmodifiableList(values)); + } + ); + this.map = unmodifiableMap(m); + } + + private static BiPredicate> headerAllowed(Predicate keyAllowed) { + return (n,v) -> keyAllowed.test(n); + } + + @Override + public Map> map() { + return map; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/LineSubscriberAdapter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/LineSubscriberAdapter.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,467 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Flow; +import java.util.concurrent.Flow.Subscriber; +import java.util.concurrent.Flow.Subscription; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import jdk.internal.net.http.common.Demand; +import java.net.http.HttpResponse.BodySubscriber; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; + +/** An adapter between {@code BodySubscriber} and {@code Flow.Subscriber}. */ +public final class LineSubscriberAdapter,R> + implements BodySubscriber { + private final CompletableFuture cf = new MinimalFuture<>(); + private final S subscriber; + private final Function finisher; + private final Charset charset; + private final String eol; + private volatile LineSubscription downstream; + + private LineSubscriberAdapter(S subscriber, + Function finisher, + Charset charset, + String eol) { + if (eol != null && eol.isEmpty()) + throw new IllegalArgumentException("empty line separator"); + this.subscriber = Objects.requireNonNull(subscriber); + this.finisher = Objects.requireNonNull(finisher); + this.charset = Objects.requireNonNull(charset); + this.eol = eol; + } + + @Override + public void onSubscribe(Subscription subscription) { + downstream = LineSubscription.create(subscription, + charset, + eol, + subscriber, + cf); + subscriber.onSubscribe(downstream); + } + + @Override + public void onNext(List item) { + try { + downstream.submit(item); + } catch (Throwable t) { + onError(t); + } + } + + @Override + public void onError(Throwable throwable) { + try { + downstream.signalError(throwable); + } finally { + cf.completeExceptionally(throwable); + } + } + + @Override + public void onComplete() { + try { + downstream.signalComplete(); + } finally { + cf.complete(finisher.apply(subscriber)); + } + } + + @Override + public CompletionStage getBody() { + return cf; + } + + public static , R> LineSubscriberAdapter + create(S subscriber, Function finisher, Charset charset, String eol) + { + if (eol != null && eol.isEmpty()) + throw new IllegalArgumentException("empty line separator"); + return new LineSubscriberAdapter<>(Objects.requireNonNull(subscriber), + Objects.requireNonNull(finisher), + Objects.requireNonNull(charset), + eol); + } + + static final class LineSubscription implements Flow.Subscription { + final Flow.Subscription upstreamSubscription; + final CharsetDecoder decoder; + final String newline; + final Demand downstreamDemand; + final ConcurrentLinkedDeque queue; + final SequentialScheduler scheduler; + final Flow.Subscriber upstream; + final CompletableFuture cf; + private final AtomicReference errorRef = new AtomicReference<>(); + private final AtomicLong demanded = new AtomicLong(); + private volatile boolean completed; + private volatile boolean cancelled; + + private final char[] chars = new char[1024]; + private final ByteBuffer leftover = ByteBuffer.wrap(new byte[64]); + private final CharBuffer buffer = CharBuffer.wrap(chars); + private final StringBuilder builder = new StringBuilder(); + private int lineCount; + private String nextLine; + + private LineSubscription(Flow.Subscription s, + CharsetDecoder dec, + String separator, + Flow.Subscriber subscriber, + CompletableFuture completion) { + downstreamDemand = new Demand(); + queue = new ConcurrentLinkedDeque<>(); + upstreamSubscription = Objects.requireNonNull(s); + decoder = Objects.requireNonNull(dec); + newline = separator; + upstream = Objects.requireNonNull(subscriber); + cf = Objects.requireNonNull(completion); + scheduler = SequentialScheduler.synchronizedScheduler(this::loop); + } + + @Override + public void request(long n) { + if (cancelled) return; + if (downstreamDemand.increase(n)) { + scheduler.runOrSchedule(); + } + } + + @Override + public void cancel() { + cancelled = true; + upstreamSubscription.cancel(); + } + + public void submit(List list) { + queue.addAll(list); + demanded.decrementAndGet(); + scheduler.runOrSchedule(); + } + + public void signalComplete() { + completed = true; + scheduler.runOrSchedule(); + } + + public void signalError(Throwable error) { + if (errorRef.compareAndSet(null, + Objects.requireNonNull(error))) { + scheduler.runOrSchedule(); + } + } + + // This method looks at whether some bytes where left over (in leftover) + // from decoding the previous buffer when the previous buffer was in + // underflow. If so, it takes bytes one by one from the new buffer 'in' + // and combines them with the leftover bytes until 'in' is exhausted or a + // character was produced in 'out', resolving the previous underflow. + // Returns true if the buffer is still in underflow, false otherwise. + // However, in both situation some chars might have been produced in 'out'. + private boolean isUnderFlow(ByteBuffer in, CharBuffer out, boolean endOfInput) + throws CharacterCodingException { + int limit = leftover.position(); + if (limit == 0) { + // no leftover + return false; + } else { + CoderResult res = null; + while (in.hasRemaining()) { + leftover.position(limit); + leftover.limit(++limit); + leftover.put(in.get()); + leftover.position(0); + res = decoder.decode(leftover, out, + endOfInput && !in.hasRemaining()); + int remaining = leftover.remaining(); + if (remaining > 0) { + assert leftover.position() == 0; + leftover.position(remaining); + } else { + leftover.position(0); + } + leftover.limit(leftover.capacity()); + if (res.isUnderflow() && remaining > 0 && in.hasRemaining()) { + continue; + } + if (res.isError()) { + res.throwException(); + } + assert !res.isOverflow(); + return false; + } + return !endOfInput; + } + } + + // extract characters from start to end and remove them from + // the StringBuilder + private static String take(StringBuilder b, int start, int end) { + assert start == 0; + String line; + if (end == start) return ""; + line = b.substring(start, end); + b.delete(start, end); + return line; + } + + // finds end of line, returns -1 if not found, or the position after + // the line delimiter if found, removing the delimiter in the process. + private static int endOfLine(StringBuilder b, String eol, boolean endOfInput) { + int len = b.length(); + if (eol != null) { // delimiter explicitly specified + int i = b.indexOf(eol); + if (i >= 0) { + // remove the delimiter and returns the position + // of the char after it. + b.delete(i, i + eol.length()); + return i; + } + } else { // no delimiter specified, behaves as BufferedReader::readLine + boolean crfound = false; + for (int i = 0; i < len; i++) { + char c = b.charAt(i); + if (c == '\n') { + // '\n' or '\r\n' found. + // remove the delimiter and returns the position + // of the char after it. + b.delete(crfound ? i - 1 : i, i + 1); + return crfound ? i - 1 : i; + } else if (crfound) { + // previous char was '\r', c != '\n' + assert i != 0; + // remove the delimiter and returns the position + // of the char after it. + b.delete(i - 1, i); + return i - 1; + } + crfound = c == '\r'; + } + if (crfound && endOfInput) { + // remove the delimiter and returns the position + // of the char after it. + b.delete(len - 1, len); + return len - 1; + } + } + return endOfInput && len > 0 ? len : -1; + } + + // Looks at whether the StringBuilder contains a line. + // Returns null if more character are needed. + private static String nextLine(StringBuilder b, String eol, boolean endOfInput) { + int next = endOfLine(b, eol, endOfInput); + return (next > -1) ? take(b, 0, next) : null; + } + + // Attempts to read the next line. Returns the next line if + // the delimiter was found, null otherwise. The delimiters are + // consumed. + private String nextLine() + throws CharacterCodingException { + assert nextLine == null; + LINES: + while (nextLine == null) { + boolean endOfInput = completed && queue.isEmpty(); + nextLine = nextLine(builder, newline, + endOfInput && leftover.position() == 0); + if (nextLine != null) return nextLine; + ByteBuffer b; + BUFFERS: + while ((b = queue.peek()) != null) { + if (!b.hasRemaining()) { + queue.poll(); + continue BUFFERS; + } + BYTES: + while (b.hasRemaining()) { + buffer.position(0); + buffer.limit(buffer.capacity()); + boolean endofInput = completed && queue.size() <= 1; + if (isUnderFlow(b, buffer, endofInput)) { + assert !b.hasRemaining(); + if (buffer.position() > 0) { + buffer.flip(); + builder.append(buffer); + } + continue BUFFERS; + } + CoderResult res = decoder.decode(b, buffer, endofInput); + if (res.isError()) res.throwException(); + if (buffer.position() > 0) { + buffer.flip(); + builder.append(buffer); + continue LINES; + } + if (res.isUnderflow() && b.hasRemaining()) { + //System.out.println("underflow: adding " + b.remaining() + " bytes"); + leftover.put(b); + assert !b.hasRemaining(); + continue BUFFERS; + } + } + } + + assert queue.isEmpty(); + if (endOfInput) { + // Time to cleanup: there may be some undecoded leftover bytes + // We need to flush them out. + // The decoder has been configured to replace malformed/unmappable + // chars with some replacement, in order to behave like + // InputStreamReader. + leftover.flip(); + buffer.position(0); + buffer.limit(buffer.capacity()); + + // decode() must be called just before flush, even if there + // is nothing to decode. We must do this even if leftover + // has no remaining bytes. + CoderResult res = decoder.decode(leftover, buffer, endOfInput); + if (buffer.position() > 0) { + buffer.flip(); + builder.append(buffer); + } + if (res.isError()) res.throwException(); + + // Now call decoder.flush() + buffer.position(0); + buffer.limit(buffer.capacity()); + res = decoder.flush(buffer); + if (buffer.position() > 0) { + buffer.flip(); + builder.append(buffer); + } + if (res.isError()) res.throwException(); + + // It's possible that we reach here twice - just for the + // purpose of checking that no bytes were left over, so + // we reset leftover/decoder to make the function reentrant. + leftover.position(0); + leftover.limit(leftover.capacity()); + decoder.reset(); + + // if some chars were produced then this call will + // return them. + return nextLine = nextLine(builder, newline, endOfInput); + } + return null; + } + return null; + } + + // The main sequential scheduler loop. + private void loop() { + try { + while (!cancelled) { + Throwable error = errorRef.get(); + if (error != null) { + cancelled = true; + scheduler.stop(); + upstream.onError(error); + cf.completeExceptionally(error); + return; + } + if (nextLine == null) nextLine = nextLine(); + if (nextLine == null) { + if (completed) { + scheduler.stop(); + if (leftover.position() != 0) { + // Underflow: not all bytes could be + // decoded, but no more bytes will be coming. + // This should not happen as we should already + // have got a MalformedInputException, or + // replaced the unmappable chars. + errorRef.compareAndSet(null, + new IllegalStateException( + "premature end of input (" + + leftover.position() + + " undecoded bytes)")); + continue; + } else { + upstream.onComplete(); + } + return; + } else if (demanded.get() == 0 + && !downstreamDemand.isFulfilled()) { + long incr = Math.max(1, downstreamDemand.get()); + demanded.addAndGet(incr); + upstreamSubscription.request(incr); + continue; + } else return; + } + assert nextLine != null; + assert newline != null && !nextLine.endsWith(newline) + || !nextLine.endsWith("\n") || !nextLine.endsWith("\r"); + if (downstreamDemand.tryDecrement()) { + String forward = nextLine; + nextLine = null; + upstream.onNext(forward); + } else return; // no demand: come back later + } + } catch (Throwable t) { + try { + upstreamSubscription.cancel(); + } finally { + signalError(t); + } + } + } + + static LineSubscription create(Flow.Subscription s, + Charset charset, + String lineSeparator, + Flow.Subscriber upstream, + CompletableFuture cf) { + return new LineSubscription(Objects.requireNonNull(s), + Objects.requireNonNull(charset).newDecoder() + // use the same decoder configuration than + // java.io.InputStreamReader + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE), + lineSeparator, + Objects.requireNonNull(upstream), + Objects.requireNonNull(cf)); + } + } +} + diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java Wed Feb 07 21:45:37 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.internal.net.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 java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.net.http.HttpTimeoutException; +import jdk.internal.net.http.UntrustedBodyHandler; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.ConnectionExpiredException; +import jdk.internal.net.http.common.Utils; +import static jdk.internal.net.http.common.MinimalFuture.completedFuture; +import static jdk.internal.net.http.common.MinimalFuture.failedFuture; + +/** + * Encapsulates multiple Exchanges belonging to one HttpRequestImpl. + * - manages filters + * - retries due to filters. + * - I/O errors and most other exceptions get returned directly to user + * + * Creates a new Exchange for each request/response interaction + */ +class MultiExchange { + + 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 responseHandler; + final Executor executor; + final AtomicInteger attempts = new AtomicInteger(); + HttpRequestImpl currentreq; // used for async only + Exchange exchange; // the current exchange + Exchange previous; + volatile Throwable retryCause; + volatile boolean expiredOnce; + volatile HttpResponse 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 filters; + TimedEvent timedEvent; + volatile boolean cancelled; + final PushGroup 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 responseHandler, + PushPromiseHandler 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 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 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> responseAsync() { + CompletableFuture start = new MinimalFuture<>(); + CompletableFuture> cf = responseAsync0(start); + start.completeAsync( () -> null, executor); // trigger execution + return cf; + } + + private CompletableFuture> + responseAsync0(CompletableFuture start) { + return start.thenCompose( v -> responseAsyncImpl()) + .thenCompose((Response r) -> { + Exchange exch = getExchange(); + return exch.readBodyAsync(responseHandler) + .thenApply((T body) -> { + this.response = + new HttpResponseImpl<>(userRequest, r, this.response, body, exch); + return this.response; + }); + }); + } + + private CompletableFuture responseAsyncImpl() { + CompletableFuture 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 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 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 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 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")); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.lang.System.Logger.Level; +import java.net.InetSocketAddress; +import java.net.StandardSocketOptions; +import java.nio.ByteBuffer; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.concurrent.CompletableFuture; +import jdk.internal.net.http.common.FlowTube; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; + +/** + * Plain raw TCP connection direct to destination. + * The connection operates in asynchronous non-blocking mode. + * All reads and writes are done non-blocking. + */ +class PlainHttpConnection extends HttpConnection { + + private final Object reading = new Object(); + protected final SocketChannel chan; + private final 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 cf; + + ConnectEvent(CompletableFuture 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 connectAsync() { + CompletableFuture cf = new MinimalFuture<>(); + try { + assert !connected : "Already connected"; + assert !chan.isBlocking() : "Unexpected blocking channel"; + boolean finished = false; + PrivilegedExceptionAction 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); + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/PlainProxyConnection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PlainProxyConnection.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.net.InetSocketAddress; + +class PlainProxyConnection extends PlainHttpConnection { + + PlainProxyConnection(InetSocketAddress proxy, HttpClientImpl client) { + super(proxy, client); + } + + @Override + ConnectionPool.CacheKey cacheKey() { + return new ConnectionPool.CacheKey(null, address); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/PlainTunnelingConnection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PlainTunnelingConnection.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.lang.System.Logger.Level; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.net.http.HttpHeaders; +import jdk.internal.net.http.common.FlowTube; +import jdk.internal.net.http.common.MinimalFuture; +import static java.net.http.HttpResponse.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 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 mulEx = new MultiExchange<>(null, req, + client, discard(), null, null); + Exchange connectExchange = new Exchange<>(req, mulEx); + + return connectExchange + .responseAsyncImpl(delegate) + .thenCompose((Response resp) -> { + CompletableFuture 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(); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/PrivilegedExecutor.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PrivilegedExecutor.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * Executes tasks within a given access control context, and by a given executor. + */ +class PrivilegedExecutor implements Executor { + + /** The underlying executor. May be provided by the user. */ + final Executor executor; + /** The ACC to execute the tasks within. */ + final AccessControlContext acc; + + public PrivilegedExecutor(Executor executor, AccessControlContext acc) { + Objects.requireNonNull(executor); + Objects.requireNonNull(acc); + this.executor = executor; + this.acc = acc; + } + + private static class PrivilegedRunnable implements Runnable { + private final Runnable r; + private final AccessControlContext acc; + PrivilegedRunnable(Runnable r, AccessControlContext acc) { + this.r = r; + this.acc = acc; + } + @Override + public void run() { + PrivilegedAction pa = () -> { r.run(); return null; }; + AccessController.doPrivileged(pa, acc); + } + } + + @Override + public void execute(Runnable r) { + executor.execute(new PrivilegedRunnable(r, acc)); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/ProxyAuthenticationRequired.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ProxyAuthenticationRequired.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; + +/** + * Signals that a proxy has refused a CONNECT request with a + * 407 error code. + */ +final class ProxyAuthenticationRequired extends IOException { + private static final long serialVersionUID = 0; + final transient Response proxyResponse; + + /** + * Constructs a {@code 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; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/PullPublisher.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PullPublisher.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.util.Iterator; +import java.util.concurrent.Flow; +import jdk.internal.net.http.common.Demand; +import jdk.internal.net.http.common.SequentialScheduler; + +/** + * A Publisher that publishes items obtained from the given Iterable. Each new + * subscription gets a new Iterator. + */ +class PullPublisher implements Flow.Publisher { + + // 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 iterable; + private final Throwable throwable; + + PullPublisher(Iterable iterable, Throwable throwable) { + this.iterable = iterable; + this.throwable = throwable; + } + + PullPublisher(Iterable iterable) { + this(iterable, null); + } + + @Override + public void subscribe(Flow.Subscriber 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 subscriber; + private final Iterator 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 subscriber, + Iterator 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; + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/PushGroup.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PushGroup.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.security.AccessControlContext; +import java.security.AccessController; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.PushPromiseHandler; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Log; + +/** + * One PushGroup object is associated with the parent Stream of the pushed + * Streams. This keeps track of all common state associated with the pushes. + */ +class PushGroup { + private final HttpRequest initiatingRequest; + + final CompletableFuture noMorePushesCF; + + volatile Throwable error; // any exception that occurred during pushes + + // user's subscriber object + final PushPromiseHandler pushPromiseHandler; + + private final AccessControlContext acc; + + int numberOfPushes; + int remainingPushes; + boolean noMorePushes = false; + + PushGroup(PushPromiseHandler pushPromiseHandler, + HttpRequestImpl initiatingRequest, + AccessControlContext acc) { + this(pushPromiseHandler, initiatingRequest, new MinimalFuture<>(), acc); + } + + // Check mainBodyHandler before calling nested constructor. + private PushGroup(HttpResponse.PushPromiseHandler pushPromiseHandler, + HttpRequestImpl initiatingRequest, + CompletableFuture> 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 { + BodyHandler bodyHandler(); + CompletableFuture> cf(); + boolean accepted(); + } + + private static class AcceptorImpl implements Acceptor { + private volatile HttpResponse.BodyHandler bodyHandler; + private volatile CompletableFuture> cf; + + CompletableFuture> accept(BodyHandler 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 bodyHandler() { return bodyHandler; } + + @Override public CompletableFuture> cf() { return cf; } + + @Override public boolean accepted() { return cf != null; } + } + + Acceptor acceptPushRequest(HttpRequest pushRequest) { + AcceptorImpl 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 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; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/RawChannelImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/RawChannelImpl.java Wed Feb 07 21:45:37 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.internal.net.http; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.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 initial; + + RawChannelImpl(HttpClientImpl client, + HttpConnection connection, + Supplier 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() + ")"; + } + + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/RedirectFilter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/RedirectFilter.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import jdk.internal.net.http.common.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(); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import java.io.File; +import java.io.FileInputStream; +import java.io.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 java.net.http.HttpRequest.BodyPublisher; +import jdk.internal.net.http.common.Utils; + +public final class RequestPublishers { + + private RequestPublishers() { } + + public static class ByteArrayPublisher implements BodyPublisher { + private volatile Flow.Publisher 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 copy(byte[] content, int offset, int length) { + List 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 subscriber) { + List 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 delegate; + private final Iterable content; + private volatile long contentLength; + + public IterablePublisher(Iterable 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 { + final ConcurrentLinkedQueue buffers = new ConcurrentLinkedQueue<>(); + final Iterator 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 iterator() { + return new ByteBufferIterator(); + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + Iterable iterable = this::iterator; + this.delegate = new PullPublisher<>(iterable); + delegate.subscribe(subscriber); + } + + static long computeLength(Iterable 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 delegate = + new PullPublisher(Collections.emptyList(), null); + + @Override + public long contentLength() { + return 0; + } + + @Override + public void subscribe(Flow.Subscriber 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 subscriber) { + if (System.getSecurityManager() != null && acc == null) + throw new InternalError( + "Unexpected null acc when security manager has been installed"); + + InputStream is; + try { + PrivilegedExceptionAction pa = + () -> new FileInputStream(file); + is = AccessController.doPrivileged(pa, acc); + } catch (PrivilegedActionException pae) { + throw new UncheckedIOException((IOException)pae.getCause()); + } + PullPublisher publisher = + new PullPublisher<>(() -> new StreamIterator(is)); + publisher.subscribe(subscriber); + } + + @Override + public long contentLength() { + assert System.getSecurityManager() != null ? acc != null: true; + PrivilegedAction pa = () -> file.length(); + return AccessController.doPrivileged(pa, acc); + } + } + + /** + * Reads one buffer ahead all the time, blocking in hasNext() + */ + public static class StreamIterator implements Iterator { + final InputStream is; + final Supplier bufSupplier; + volatile ByteBuffer nextBuffer; + volatile boolean need2Read = true; + volatile boolean haveNext; + + StreamIterator(InputStream is) { + this(is, Utils::getBuffer); + } + + StreamIterator(InputStream is, Supplier 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 streamSupplier; + + public InputStreamPublisher(Supplier streamSupplier) { + this.streamSupplier = Objects.requireNonNull(streamSupplier); + } + + @Override + public void subscribe(Flow.Subscriber subscriber) { + PullPublisher 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 iterableOf(InputStream is) { + return () -> new StreamIterator(is); + } + + @Override + public long contentLength() { + return -1; + } + } + + public static final class PublisherAdapter implements BodyPublisher { + + private final Publisher publisher; + private final long contentLength; + + public PublisherAdapter(Publisher 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 subscriber) { + publisher.subscribe(subscriber); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/Response.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Response.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.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(); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import java.io.IOException; +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 java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodySubscriber; +import jdk.internal.net.http.ResponseSubscribers.PathSubscriber; +import static jdk.internal.net.http.common.Utils.unchecked; + +public final class ResponseBodyHandlers { + + private 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 { + 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 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 + implements HttpResponse.PushPromiseHandler + { + private final ConcurrentMap>> pushPromisesMap; + private final Function> pushPromiseHandler; + + public PushPromisesHandlerWithMap(Function> pushPromiseHandler, + ConcurrentMap>> pushPromisesMap) { + this.pushPromiseHandler = pushPromiseHandler; + this.pushPromisesMap = pushPromisesMap; + } + + @Override + public void applyPushPromise( + HttpRequest initiatingRequest, HttpRequest pushRequest, + Function,CompletableFuture>> 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> 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 { + 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 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 asFileImpl(Path file, OpenOption... openOptions) { + return new ResponseSubscribers.PathSubscriber(file, openOptions); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/ResponseContent.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseContent.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.io.IOException; +import java.lang.System.Logger.Level; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import jdk.internal.net.http.common.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 { + 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 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 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 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 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 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 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 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); + } + } + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/ResponseSubscribers.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseSubscribers.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,650 @@ +/* + * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.System.Logger.Level; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Flow; +import java.util.concurrent.Flow.Subscriber; +import java.util.concurrent.Flow.Subscription; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; +import java.net.http.HttpResponse.BodySubscriber; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class ResponseSubscribers { + + public static class ConsumerSubscriber implements BodySubscriber { + private final Consumer> consumer; + private Flow.Subscription subscription; + private final CompletableFuture result = new MinimalFuture<>(); + private final AtomicBoolean subscribed = new AtomicBoolean(); + + public ConsumerSubscriber(Consumer> consumer) { + this.consumer = Objects.requireNonNull(consumer); + } + + @Override + public CompletionStage getBody() { + return result; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + if (!subscribed.compareAndSet(false, true)) { + subscription.cancel(); + } else { + this.subscription = subscription; + subscription.request(1); + } + } + + @Override + public void onNext(List items) { + for (ByteBuffer item : items) { + byte[] buf = new byte[item.remaining()]; + item.get(buf); + consumer.accept(Optional.of(buf)); + } + subscription.request(1); + } + + @Override + public void onError(Throwable throwable) { + result.completeExceptionally(throwable); + } + + @Override + public void onComplete() { + consumer.accept(Optional.empty()); + result.complete(null); + } + + } + + public static class PathSubscriber implements BodySubscriber { + + private final Path file; + private final CompletableFuture result = new MinimalFuture<>(); + + private volatile Flow.Subscription subscription; + private volatile FileChannel out; + private volatile AccessControlContext acc; + private final OpenOption[] options; + + public PathSubscriber(Path file, OpenOption... options) { + this.file = file; + this.options = options; + } + + public void setAccessControlContext(AccessControlContext acc) { + this.acc = acc; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + if (System.getSecurityManager() != null && acc == null) + throw new InternalError( + "Unexpected null acc when security manager has been installed"); + + this.subscription = subscription; + try { + PrivilegedExceptionAction pa = + () -> FileChannel.open(file, options); + out = AccessController.doPrivileged(pa, acc); + } catch (PrivilegedActionException pae) { + Throwable t = pae.getCause() != null ? pae.getCause() : pae; + result.completeExceptionally(t); + subscription.cancel(); + return; + } + subscription.request(1); + } + + @Override + public void onNext(List items) { + try { + out.write(items.toArray(Utils.EMPTY_BB_ARRAY)); + } catch (IOException ex) { + Utils.close(out); + subscription.cancel(); + result.completeExceptionally(ex); + } + subscription.request(1); + } + + @Override + public void onError(Throwable e) { + result.completeExceptionally(e); + Utils.close(out); + } + + @Override + public void onComplete() { + Utils.close(out); + result.complete(file); + } + + @Override + public CompletionStage getBody() { + return result; + } + } + + public static class ByteArraySubscriber implements BodySubscriber { + private final Function finisher; + private final CompletableFuture result = new MinimalFuture<>(); + private final List received = new ArrayList<>(); + + private volatile Flow.Subscription subscription; + + public ByteArraySubscriber(Function finisher) { + this.finisher = finisher; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + if (this.subscription != null) { + subscription.cancel(); + return; + } + this.subscription = subscription; + // We can handle whatever you've got + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(List items) { + // incoming buffers are allocated by http client internally, + // and won't be used anywhere except this place. + // So it's free simply to store them for further processing. + assert Utils.hasRemaining(items); + received.addAll(items); + } + + @Override + public void onError(Throwable throwable) { + received.clear(); + result.completeExceptionally(throwable); + } + + static private byte[] join(List bytes) { + int size = Utils.remaining(bytes, Integer.MAX_VALUE); + byte[] res = new byte[size]; + int from = 0; + for (ByteBuffer b : bytes) { + int l = b.remaining(); + b.get(res, from, l); + from += l; + } + return res; + } + + @Override + public void onComplete() { + try { + result.complete(finisher.apply(join(received))); + received.clear(); + } catch (IllegalArgumentException e) { + result.completeExceptionally(e); + } + } + + @Override + public CompletionStage getBody() { + return result; + } + } + + /** + * An InputStream built on top of the Flow API. + */ + public static class HttpResponseInputStream extends InputStream + implements BodySubscriber + { + final static boolean DEBUG = Utils.DEBUG; + final static int MAX_BUFFERS_IN_QUEUE = 1; // lock-step with the producer + + // An immutable ByteBuffer sentinel to mark that the last byte was received. + private static final ByteBuffer LAST_BUFFER = ByteBuffer.wrap(new byte[0]); + private static final List LAST_LIST = List.of(LAST_BUFFER); + private static final System.Logger DEBUG_LOGGER = + Utils.getDebugLogger("HttpResponseInputStream"::toString, DEBUG); + + // A queue of yet unprocessed ByteBuffers received from the flow API. + private final BlockingQueue> buffers; + private volatile Flow.Subscription subscription; + private volatile boolean closed; + private volatile Throwable failed; + private volatile Iterator currentListItr; + private volatile ByteBuffer currentBuffer; + private final AtomicBoolean subscribed = new AtomicBoolean(); + + public HttpResponseInputStream() { + this(MAX_BUFFERS_IN_QUEUE); + } + + HttpResponseInputStream(int maxBuffers) { + int capacity = (maxBuffers <= 0 ? MAX_BUFFERS_IN_QUEUE : maxBuffers); + // 1 additional slot needed for LAST_LIST added by onComplete + this.buffers = new ArrayBlockingQueue<>(capacity + 1); + } + + @Override + public CompletionStage getBody() { + // Returns the stream immediately, before the + // response body is received. + // This makes it possible for sendAsync().get().body() + // to complete before the response body is received. + return CompletableFuture.completedStage(this); + } + + // Returns the current byte buffer to read from. + // If the current buffer has no remaining data, this method will take the + // next buffer from the buffers queue, possibly blocking until + // a new buffer is made available through the Flow API, or the + // end of the flow has been reached. + private ByteBuffer current() throws IOException { + while (currentBuffer == null || !currentBuffer.hasRemaining()) { + // Check whether the stream is closed or exhausted + if (closed || failed != null) { + throw new IOException("closed", failed); + } + if (currentBuffer == LAST_BUFFER) break; + + try { + if (currentListItr == null || !currentListItr.hasNext()) { + // Take a new list of buffers from the queue, blocking + // if none is available yet... + + DEBUG_LOGGER.log(Level.DEBUG, "Taking list of Buffers"); + List lb = buffers.take(); + currentListItr = lb.iterator(); + DEBUG_LOGGER.log(Level.DEBUG, "List of Buffers Taken"); + + // Check whether an exception was encountered upstream + if (closed || failed != null) + throw new IOException("closed", failed); + + // Check whether we're done. + if (lb == LAST_LIST) { + currentListItr = null; + currentBuffer = LAST_BUFFER; + break; + } + + // Request another upstream item ( list of buffers ) + Flow.Subscription s = subscription; + if (s != null) { + DEBUG_LOGGER.log(Level.DEBUG, "Increased demand by 1"); + s.request(1); + } + assert currentListItr != null; + if (lb.isEmpty()) continue; + } + assert currentListItr != null; + assert currentListItr.hasNext(); + DEBUG_LOGGER.log(Level.DEBUG, "Next Buffer"); + currentBuffer = currentListItr.next(); + } catch (InterruptedException ex) { + // continue + } + } + assert currentBuffer == LAST_BUFFER || currentBuffer.hasRemaining(); + return currentBuffer; + } + + @Override + public int read(byte[] bytes, int off, int len) throws IOException { + // get the buffer to read from, possibly blocking if + // none is available + ByteBuffer buffer; + if ((buffer = current()) == LAST_BUFFER) return -1; + + // don't attempt to read more than what is available + // in the current buffer. + int read = Math.min(buffer.remaining(), len); + assert read > 0 && read <= buffer.remaining(); + + // buffer.get() will do the boundary check for us. + buffer.get(bytes, off, read); + return read; + } + + @Override + public int read() throws IOException { + ByteBuffer buffer; + if ((buffer = current()) == LAST_BUFFER) return -1; + return buffer.get() & 0xFF; + } + + @Override + public void onSubscribe(Flow.Subscription s) { + try { + if (!subscribed.compareAndSet(false, true)) { + s.cancel(); + } else { + // check whether the stream is already closed. + // if so, we should cancel the subscription + // immediately. + boolean closed; + synchronized (this) { + closed = this.closed; + if (!closed) { + this.subscription = s; + } + } + if (closed) { + s.cancel(); + return; + } + assert buffers.remainingCapacity() > 1; // should contain at least 2 + DEBUG_LOGGER.log(Level.DEBUG, () -> "onSubscribe: requesting " + + Math.max(1, buffers.remainingCapacity() - 1)); + s.request(Math.max(1, buffers.remainingCapacity() - 1)); + } + } catch (Throwable t) { + failed = t; + try { + close(); + } catch (IOException x) { + // OK + } finally { + onError(t); + } + } + } + + @Override + public void onNext(List t) { + Objects.requireNonNull(t); + try { + DEBUG_LOGGER.log(Level.DEBUG, "next item received"); + if (!buffers.offer(t)) { + throw new IllegalStateException("queue is full"); + } + DEBUG_LOGGER.log(Level.DEBUG, "item offered"); + } catch (Throwable ex) { + failed = ex; + try { + close(); + } catch (IOException ex1) { + // OK + } finally { + onError(ex); + } + } + } + + @Override + public void onError(Throwable thrwbl) { + subscription = null; + failed = Objects.requireNonNull(thrwbl); + // The client process that reads the input stream might + // be blocked in queue.take(). + // Tries to offer LAST_LIST to the queue. If the queue is + // full we don't care if we can't insert this buffer, as + // the client can't be blocked in queue.take() in that case. + // Adding LAST_LIST to the queue is harmless, as the client + // should find failed != null before handling LAST_LIST. + buffers.offer(LAST_LIST); + } + + @Override + public void onComplete() { + subscription = null; + onNext(LAST_LIST); + } + + @Override + public void close() throws IOException { + Flow.Subscription s; + synchronized (this) { + if (closed) return; + closed = true; + s = subscription; + subscription = null; + } + // s will be null if already completed + try { + if (s != null) { + s.cancel(); + } + } finally { + buffers.offer(LAST_LIST); + super.close(); + } + } + + } + + public static BodySubscriber> createLineStream() { + return createLineStream(UTF_8); + } + + public static BodySubscriber> createLineStream(Charset charset) { + Objects.requireNonNull(charset); + BodySubscriber s = new HttpResponseInputStream(); + return new MappedSubscriber>(s, + (InputStream stream) -> { + return new BufferedReader(new InputStreamReader(stream, charset)) + .lines().onClose(() -> Utils.close(stream)); + }); + } + + /** + * Currently this consumes all of the data and ignores it + */ + public static class NullSubscriber implements BodySubscriber { + + private final CompletableFuture cf = new MinimalFuture<>(); + private final Optional result; + private final AtomicBoolean subscribed = new AtomicBoolean(); + + public NullSubscriber(Optional result) { + this.result = result; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + if (!subscribed.compareAndSet(false, true)) { + subscription.cancel(); + } else { + subscription.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(List items) { + Objects.requireNonNull(items); + } + + @Override + public void onError(Throwable throwable) { + cf.completeExceptionally(throwable); + } + + @Override + public void onComplete() { + if (result.isPresent()) { + cf.complete(result.get()); + } else { + cf.complete(null); + } + } + + @Override + public CompletionStage getBody() { + return cf; + } + } + + /** An adapter between {@code BodySubscriber} and {@code Flow.Subscriber}. */ + public static final class SubscriberAdapter>,R> + implements BodySubscriber + { + private final CompletableFuture cf = new MinimalFuture<>(); + private final S subscriber; + private final Function finisher; + private volatile Subscription subscription; + + public SubscriberAdapter(S subscriber, Function finisher) { + this.subscriber = Objects.requireNonNull(subscriber); + this.finisher = Objects.requireNonNull(finisher); + } + + @Override + public void onSubscribe(Subscription subscription) { + Objects.requireNonNull(subscription); + if (this.subscription != null) { + subscription.cancel(); + } else { + this.subscription = subscription; + subscriber.onSubscribe(subscription); + } + } + + @Override + public void onNext(List item) { + Objects.requireNonNull(item); + try { + subscriber.onNext(item); + } catch (Throwable throwable) { + subscription.cancel(); + onError(throwable); + } + } + + @Override + public void onError(Throwable throwable) { + Objects.requireNonNull(throwable); + try { + subscriber.onError(throwable); + } finally { + cf.completeExceptionally(throwable); + } + } + + @Override + public void onComplete() { + try { + subscriber.onComplete(); + } finally { + try { + cf.complete(finisher.apply(subscriber)); + } catch (Throwable throwable) { + cf.completeExceptionally(throwable); + } + } + } + + @Override + public CompletionStage getBody() { + return cf; + } + } + + /** + * A body subscriber which receives input from an upstream subscriber + * and maps that subscriber's body type to a new type. The upstream subscriber + * delegates all flow operations directly to this object. The + * {@link CompletionStage} returned by {@link #getBody()}} takes the output + * of the upstream {@code getBody()} and applies the mapper function to + * obtain the new {@code CompletionStage} type. + * + * Uses an Executor that must be set externally. + * + * @param the upstream body type + * @param this subscriber's body type + */ + public static class MappedSubscriber implements BodySubscriber { + final BodySubscriber upstream; + final Function mapper; + + /** + * + * @param upstream + * @param mapper + */ + public MappedSubscriber(BodySubscriber upstream, Function mapper) { + this.upstream = upstream; + this.mapper = mapper; + } + + @Override + public CompletionStage getBody() { + return upstream.getBody() + .thenApply(mapper); + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + upstream.onSubscribe(subscription); + } + + @Override + public void onNext(List item) { + upstream.onNext(item); + } + + @Override + public void onError(Throwable throwable) { + upstream.onError(throwable); + } + + @Override + public void onComplete() { + upstream.onComplete(); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/SSLDelegate.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/SSLDelegate.java Wed Feb 07 21:45:37 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.internal.net.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.internal.net.http.common.Log; +import jdk.internal.net.http.common.Utils; +import static javax.net.ssl.SSLEngineResult.HandshakeStatus.*; + +/** + * Implements the mechanics of SSL by managing an SSLEngine object. + *

+ * 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 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(); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/SocketTube.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/SocketTube.java Wed Feb 07 21:45:37 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.internal.net.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.internal.net.http.common.Demand; +import jdk.internal.net.http.common.FlowTube; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.SequentialScheduler.DeferredCompleter; +import jdk.internal.net.http.common.SequentialScheduler.RestartableTask; +import jdk.internal.net.http.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 buffersSource; + private final Object lock = new Object(); + private final AtomicReference errorRef = new AtomicReference<>(); + private final InternalReadPublisher readPublisher; + private final InternalWriteSubscriber writeSubscriber; + private final long id = IDS.incrementAndGet(); + + public SocketTube(HttpClientImpl client, SocketChannel channel, + Supplier 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> 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 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> { + + volatile Flow.Subscription subscription; + volatile List 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 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 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 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> { + private final InternalReadSubscription subscriptionImpl + = new InternalReadSubscription(); + AtomicReference pendingSubscription = new AtomicReference<>(); + private volatile ReadSubscription subscription; + + @Override + public void subscribe(Flow.Subscriber> 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 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 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 EOF = List.of(); + + private List readAvailable() throws IOException { + ByteBuffer buf = buffersSource.get(); + assert buf.hasRemaining(); + + int read; + int pos = buf.position(); + List 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 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 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 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+")"; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/Stream.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,1180 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import java.io.IOException; +import java.lang.System.Logger.Level; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.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 java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodySubscriber; +import jdk.internal.net.http.common.*; +import jdk.internal.net.http.frame.*; +import jdk.internal.net.http.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 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 extends ExchangeImpl { + + final static boolean DEBUG = Utils.DEBUG; // Revisit: temporary developer's flag + final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG); + + final ConcurrentLinkedQueue 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 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 requestBodyCF = new MinimalFuture<>(); + volatile CompletableFuture 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 buffers = df.getData(); + List 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 readBodyAsync(HttpResponse.BodyHandler handler, + boolean returnConnectionToPool, + Executor executor) + { + Log.logTrace("Reading body on stream {0}", streamid); + BodySubscriber bodySubscriber = handler.apply(responseCode, responseHeaders); + CompletableFuture cf = receiveData(bodySubscriber, executor); + + 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 receiveData(BodySubscriber bodySubscriber, Executor executor) { + responseBodyCF = new MinimalFuture<>(); + // We want to allow the subscriber's getBody() method to block so it + // can work with InputStreams. So, we offload execution. + executor.execute(() -> { + bodySubscriber.getBody().whenComplete((T body, Throwable t) -> { + if (t == null) + responseBodyCF.complete(body); + else + responseBodyCF.completeExceptionally(t); + }); + }); + + 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> sendBodyAsync() { + return sendBodyImpl().thenApply( v -> this); + } + + @SuppressWarnings("unchecked") + Stream(Http2Connection connection, + Exchange 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 pushStream) + throws IOException + { + if (Log.requests()) { + Log.logRequest("PUSH_PROMISE: " + pushRequest.toString()); + } + PushGroup 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 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> pushResponseCF = acceptor.cf(); + HttpResponse.BodyHandler 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> cf = pushStream.responseCF(); + cf.whenComplete((HttpResponse 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> 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> 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> 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> 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> 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> f = headerFrame(requestContentLen); + connection.sendFrame(f); + CompletableFuture> 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 { + // 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 outgoing = new ConcurrentLinkedDeque<>(); + + private final AtomicReference 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 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> response_cfs = new ArrayList<>(5); + + @Override + CompletableFuture getResponseAsync(Executor executor) { + CompletableFuture 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 cf; + int cfs_len = response_cfs.size(); + for (int i=0; i cf = response_cfs.get(i); + if (!cf.isDone()) { + cf.completeExceptionally(t); + response_cfs.remove(i); + return; + } + } + response_cfs.add(MinimalFuture.failedFuture(t)); + } + } + + CompletableFuture 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 extends Stream { + final PushGroup 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 pushCF; + CompletableFuture> responseCF; + final HttpRequestImpl pushReq; + HttpResponse.BodyHandler pushHandler; + + PushedStream(PushGroup pushGroup, + Http2Connection connection, + Exchange 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> responseCF() { + return responseCF; + } + + synchronized void setPushHandler(HttpResponse.BodyHandler pushHandler) { + this.pushHandler = pushHandler; + } + + synchronized HttpResponse.BodyHandler 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> sendBodyAsync() { + return super.sendBodyAsync() + .whenComplete((ExchangeImpl v, Throwable t) + -> pushGroup.pushError(Utils.getCompletionCause(t))); + } + + @Override + CompletableFuture> sendHeadersAsync() { + return super.sendHeadersAsync() + .whenComplete((ExchangeImpl ex, Throwable t) + -> pushGroup.pushError(Utils.getCompletionCause(t))); + } + + @Override + CompletableFuture getResponseAsync(Executor executor) { + CompletableFuture 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 readBodyAsync( + HttpResponse.BodyHandler 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 start = new MinimalFuture<>(); + start.thenCompose( v -> readBodyAsync(getPushHandler(), false, getExchange().executor())) + .whenComplete((T body, Throwable t) -> { + if (t != null) { + responseCF.completeExceptionally(t); + } else { + HttpResponseImpl 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+")"; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/TimeoutEvent.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/TimeoutEvent.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Timeout event notified by selector thread. Executes the given handler if + * the timer not canceled first. + * + * Register with {@link HttpClientImpl#registerTimer(TimeoutEvent)}. + * + * Cancel with {@link HttpClientImpl#cancelTimer(TimeoutEvent)}. + */ +abstract class TimeoutEvent implements Comparable { + + 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 + "]"; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/UntrustedBodyHandler.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/UntrustedBodyHandler.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.security.AccessControlContext; +import java.net.http.HttpResponse; + +/** A body handler that is further restricted by a given ACC. */ +public interface UntrustedBodyHandler extends HttpResponse.BodyHandler { + void setAccessControlContext(AccessControlContext acc); +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/WindowController.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/WindowController.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.lang.System.Logger.Level; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; +import jdk.internal.net.http.common.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 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>> 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> 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,Integer>>> iter = + pending.entrySet().iterator(); + + while (iter.hasNext() && size > 0) { + Map.Entry,Integer>> item = iter.next(); + Integer streamSize = streams.get(item.getKey()); + if (streamSize == null) { + iter.remove(); + } else { + Map.Entry,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,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 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(); +// } +// } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/WindowUpdateSender.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/WindowUpdateSender.java Wed Feb 07 21:45:37 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.internal.net.http; + +import java.lang.System.Logger.Level; +import jdk.internal.net.http.frame.SettingsFrame; +import jdk.internal.net.http.frame.WindowUpdateFrame; +import jdk.internal.net.http.common.Utils; + +import java.util.concurrent.atomic.AtomicInteger; + +abstract class WindowUpdateSender { + + final 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() + ")"; + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/ByteBufferPool.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/ByteBufferPool.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.common; + +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * The class provides reuse of ByteBuffers. + * It is supposed that all requested buffers have the same size for a long period of time. + * That is why there is no any logic splitting buffers into different buckets (by size). It's unnecessary. + * + * At the same moment it is allowed to change requested buffers size (all smaller buffers will be discarded). + * It may be needed for example, if after rehandshaking netPacketBufferSize was changed. + */ +public class ByteBufferPool { + + private final java.util.Queue pool = new ConcurrentLinkedQueue<>(); + + public ByteBufferPool() { + } + + public ByteBufferReference get(int size) { + ByteBuffer buffer; + while ((buffer = pool.poll()) != null) { + if (buffer.capacity() >= size) { + return ByteBufferReference.of(buffer, this); + } + } + return ByteBufferReference.of(ByteBuffer.allocate(size), this); + } + + public void release(ByteBuffer buffer) { + buffer.clear(); + pool.offer(buffer); + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/ByteBufferReference.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/ByteBufferReference.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.common; + +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.function.Supplier; + +public class ByteBufferReference implements Supplier { + + private ByteBuffer buffer; + private final ByteBufferPool pool; + + public static ByteBufferReference of(ByteBuffer buffer) { + return of(buffer, null); + } + + public static ByteBufferReference of(ByteBuffer buffer, ByteBufferPool pool) { + Objects.requireNonNull(buffer); + return new ByteBufferReference(buffer, pool); + } + + public static ByteBuffer[] toBuffers(ByteBufferReference... refs) { + ByteBuffer[] bufs = new ByteBuffer[refs.length]; + for (int i = 0; i < refs.length; i++) { + bufs[i] = refs[i].get(); + } + return bufs; + } + + public static ByteBufferReference[] toReferences(ByteBuffer... buffers) { + ByteBufferReference[] refs = new ByteBufferReference[buffers.length]; + for (int i = 0; i < buffers.length; i++) { + refs[i] = of(buffers[i]); + } + return refs; + } + + + public static void clear(ByteBufferReference[] refs) { + for(ByteBufferReference ref : refs) { + ref.clear(); + } + } + + private ByteBufferReference(ByteBuffer buffer, ByteBufferPool pool) { + this.buffer = buffer; + this.pool = pool; + } + + @Override + public ByteBuffer get() { + ByteBuffer buf = this.buffer; + assert buf!=null : "getting ByteBuffer after clearance"; + return buf; + } + + public void clear() { + ByteBuffer buf = this.buffer; + assert buf!=null : "double ByteBuffer clearance"; + this.buffer = null; + if (pool != null) { + pool.release(buf); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.common; + +import java.io.IOException; + +/** + * Signals that an end of file or end of stream has been reached + * unexpectedly before any protocol specific data has been received. + */ +public final class ConnectionExpiredException extends IOException { + private static final long serialVersionUID = 0; + + /** + * Constructs a {@code ConnectionExpiredException} with the specified detail + * message and cause. + * + * @param s the detail message + * @param cause the throwable cause + */ + public ConnectionExpiredException(String s, Throwable cause) { + super(s, cause); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/DebugLogger.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/DebugLogger.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.common; + +import java.io.PrintStream; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.function.Supplier; +import java.lang.System.Logger; + +/** + * A {@code System.Logger} that forwards all messages to an underlying + * {@code System.Logger}, after adding some decoration. + * The logger also has the ability to additionally send the logged messages + * to System.err or System.out, whether the underlying logger is activated or not. + * In addition instance of {@code DebugLogger} support both + * {@link String#format(String, Object...)} and + * {@link java.text.MessageFormat#format(String, Object...)} formatting. + * String-like formatting is enabled by the presence of "%s" or "%d" in the format + * string. MessageFormat-like formatting is enabled by the presence of "{0" or "{1". + *

+ * See {@link Utils#getDebugLogger(Supplier, boolean)} and + * {@link Utils#getHpackLogger(Supplier, boolean)}. + */ +class DebugLogger implements Logger { + // deliberately not in the same subtree than standard loggers. + final static String HTTP_NAME = "jdk.internal.httpclient.debug"; + final static String HPACK_NAME = "jdk.internal.httpclient.hpack.debug"; + final static Logger HTTP = System.getLogger(HTTP_NAME); + final static Logger HPACK = System.getLogger(HPACK_NAME); + final static long START_NANOS = System.nanoTime(); + + private final Supplier dbgTag; + private final Level errLevel; + private final Level outLevel; + private final Logger logger; + private final boolean debugOn; + private final boolean traceOn; + + /** + * Create a logger for debug traces.The logger should only be used + * with levels whose severity is {@code <= DEBUG}. + * + * By default, this logger will forward all messages logged to the supplied + * {@code logger}. + * But in addition, if the message severity level is {@code >=} to + * the provided {@code errLevel} it will print the messages on System.err, + * and if the message severity level is {@code >=} to + * the provided {@code outLevel} it will also print the messages on System.out. + *

+ * The logger will add some decoration to the printed message, in the form of + * {@code :[] [] : } + * + * @apiNote To obtain a logger that will always print things on stderr in + * addition to forwarding to the internal logger, use + * {@code new DebugLogger(logger, this::dbgTag, Level.OFF, Level.ALL);}. + * To obtain a logger that will only forward to the internal logger, + * use {@code new DebugLogger(logger, this::dbgTag, Level.OFF, Level.OFF);}. + * + * @param logger The internal logger to which messages will be forwarded. + * This should be either {@link #HPACK} or {@link #HTTP}; + * + * @param dbgTag A lambda that returns a string that identifies the caller + * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))") + * @param outLevel The level above which messages will be also printed on + * System.out (in addition to being forwarded to the internal logger). + * @param errLevel The level above which messages will be also printed on + * System.err (in addition to being forwarded to the internal logger). + * + * @return A logger for HTTP internal debug traces + */ + private DebugLogger(Logger logger, + Supplier dbgTag, + Level outLevel, + Level errLevel) { + this.dbgTag = dbgTag; + this.errLevel = errLevel; + this.outLevel = outLevel; + this.logger = Objects.requireNonNull(logger); + // support only static configuration. + this.debugOn = isEnabled(Level.DEBUG); + this.traceOn = isEnabled(Level.TRACE); + } + + @Override + public String getName() { + return logger.getName(); + } + + private boolean isEnabled(Level level) { + if (level == Level.OFF) return false; + int severity = level.getSeverity(); + return severity >= errLevel.getSeverity() + || severity >= outLevel.getSeverity() + || logger.isLoggable(level); + } + + @Override + public boolean isLoggable(Level level) { + // fast path, we assume these guys never change. + // support only static configuration. + if (level == Level.DEBUG) return debugOn; + if (level == Level.TRACE) return traceOn; + return isEnabled(level); + } + + @Override + public void log(Level level, ResourceBundle unused, + String format, Object... params) { + // fast path, we assume these guys never change. + // support only static configuration. + if (level == Level.DEBUG && !debugOn) return; + if (level == Level.TRACE && !traceOn) return; + + int severity = level.getSeverity(); + if (errLevel != Level.OFF + && errLevel.getSeverity() <= severity) { + print(System.err, level, format, params, null); + } + if (outLevel != Level.OFF + && outLevel.getSeverity() <= severity) { + print(System.out, level, format, params, null); + } + if (logger.isLoggable(level)) { + logger.log(level, unused, + getFormat(new StringBuilder(), format, params).toString(), + params); + } + } + + @Override + public void log(Level level, ResourceBundle unused, String msg, + Throwable thrown) { + // fast path, we assume these guys never change. + if (level == Level.DEBUG && !debugOn) return; + if (level == Level.TRACE && !traceOn) return; + + if (errLevel != Level.OFF + && errLevel.getSeverity() <= level.getSeverity()) { + print(System.err, level, msg, null, thrown); + } + if (outLevel != Level.OFF + && outLevel.getSeverity() <= level.getSeverity()) { + print(System.out, level, msg, null, thrown); + } + if (logger.isLoggable(level)) { + logger.log(level, unused, + getFormat(new StringBuilder(), msg, null).toString(), + thrown); + } + } + + private void print(PrintStream out, Level level, String msg, + Object[] params, Throwable t) { + StringBuilder sb = new StringBuilder(); + sb.append(level.name()).append(':').append(' '); + sb = format(sb, msg, params); + if (t != null) sb.append(' ').append(t.toString()); + out.println(sb.toString()); + if (t != null) { + t.printStackTrace(out); + } + } + + private StringBuilder decorate(StringBuilder sb, String msg) { + String tag = dbgTag == null ? null : dbgTag.get(); + String res = msg == null ? "" : msg; + long elapsed = System.nanoTime() - START_NANOS; + long millis = elapsed / 1000_000; + long secs = millis / 1000; + sb.append('[').append(Thread.currentThread().getName()).append(']') + .append(' ').append('['); + if (secs > 0) { + sb.append(secs).append('s'); + } + millis = millis % 1000; + if (millis > 0) { + if (secs > 0) sb.append(' '); + sb.append(millis).append("ms"); + } + sb.append(']').append(' '); + if (tag != null) { + sb.append(tag).append(' '); + } + sb.append(res); + return sb; + } + + + private StringBuilder getFormat(StringBuilder sb, String format, Object[] params) { + if (format == null || params == null || params.length == 0) { + return decorate(sb, format); + } else if (format.contains("{0}") || format.contains("{1}")) { + return decorate(sb, format); + } else if (format.contains("%s") || format.contains("%d")) { + try { + return decorate(sb, String.format(format, params)); + } catch (Throwable t) { + return decorate(sb, format); + } + } else { + return decorate(sb, format); + } + } + + private StringBuilder format(StringBuilder sb, String format, Object[] params) { + if (format == null || params == null || params.length == 0) { + return decorate(sb, format); + } else if (format.contains("{0}") || format.contains("{1}")) { + return decorate(sb, java.text.MessageFormat.format(format, params)); + } else if (format.contains("%s") || format.contains("%d")) { + try { + return decorate(sb, String.format(format, params)); + } catch (Throwable t) { + return decorate(sb, format); + } + } else { + return decorate(sb, format); + } + } + + public static DebugLogger createHttpLogger(Supplier dbgTag, Level outLevel, Level errLevel) { + return new DebugLogger(HTTP, dbgTag, outLevel, errLevel); + } + + public static DebugLogger createHpackLogger(Supplier dbgTag, Level outLevel, Level errLevel) { + return new DebugLogger(HPACK, dbgTag, outLevel, errLevel); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/Demand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Demand.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.common; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Encapsulates operations with demand (Reactive Streams). + * + *

Demand is the aggregated number of elements requested by a Subscriber + * which is yet to be delivered (fulfilled) by the Publisher. + */ +public final class Demand { + + private final AtomicLong val = new AtomicLong(); + + /** + * Increases this demand by the specified positive value. + * + * @param n + * increment {@code > 0} + * + * @return {@code true} iff prior to this operation this demand was fulfilled + */ + public boolean increase(long n) { + if (n <= 0) { + throw new IllegalArgumentException(String.valueOf(n)); + } + long prev = val.getAndAccumulate(n, (p, i) -> p + i < 0 ? Long.MAX_VALUE : p + i); + return prev == 0; + } + + /** + * Tries to decrease this demand by the specified positive value. + * + *

The actual value this demand has been decreased by might be less than + * {@code n}, including {@code 0} (no decrease at all). + * + * @param n + * decrement {@code > 0} + * + * @return a value {@code m} ({@code 0 <= m <= n}) this demand has been + * actually decreased by + */ + public long decreaseAndGet(long n) { + if (n <= 0) { + throw new IllegalArgumentException(String.valueOf(n)); + } + long p, d; + do { + d = val.get(); + p = Math.min(d, n); + } while (!val.compareAndSet(d, d - p)); + return p; + } + + /** + * Tries to decrease this demand by {@code 1}. + * + * @return {@code true} iff this demand has been decreased by {@code 1} + */ + public boolean tryDecrement() { + return decreaseAndGet(1) == 1; + } + + /** + * @return {@code true} iff there is no unfulfilled demand + */ + public boolean isFulfilled() { + return val.get() == 0; + } + + /** + * Resets this object to its initial state. + */ + public void reset() { + val.set(0); + } + + /** + * Returns the current value of this demand. + * + * @return the current value of this demand + */ + public long get() { + return val.get(); + } + + @Override + public String toString() { + return String.valueOf(val.get()); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/FlowTube.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/FlowTube.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.common; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.Flow; + +/** + * FlowTube is an I/O abstraction that allows reading from and writing to a + * destination asynchronously. + * This is not a {@link Flow.Processor + * Flow.Processor<List<ByteBuffer>, List<ByteBuffer>>}, + * but rather models a publisher source and a subscriber sink in a bidirectional flow. + *

+ * The {@code connectFlows} method should be called to connect the bidirectional + * flow. A FlowTube supports handing over the same read subscription to different + * sequential read subscribers over time. When {@code connectFlows(writePublisher, + * readSubscriber} is called, the FlowTube will call {@code dropSubscription} on + * its former readSubscriber, and {@code onSubscribe} on its new readSubscriber. + */ +public interface FlowTube extends + Flow.Publisher>, + Flow.Subscriber> { + + /** + * A subscriber for reading from the bidirectional flow. + * A TubeSubscriber is a {@code Flow.Subscriber} that can be canceled + * by calling {@code dropSubscription()}. + * Once {@code dropSubscription()} is called, the {@code TubeSubscriber} + * should stop calling any method on its subscription. + */ + static interface TubeSubscriber extends Flow.Subscriber> { + + /** + * Called when the flow is connected again, and the subscription + * is handed over to a new subscriber. + * Once {@code dropSubscription()} is called, the {@code TubeSubscriber} + * should stop calling any method on its subscription. + */ + default void dropSubscription() { } + + } + + /** + * A publisher for writing to the bidirectional flow. + */ + static interface TubePublisher extends Flow.Publisher> { + + } + + /** + * Connects the bidirectional flows to a write {@code Publisher} and a + * read {@code Subscriber}. This method can be called sequentially + * several times to switch existing publishers and subscribers to a new + * pair of write subscriber and read publisher. + * @param writePublisher A new publisher for writing to the bidirectional flow. + * @param readSubscriber A new subscriber for reading from the bidirectional + * flow. + */ + default void connectFlows(TubePublisher writePublisher, + TubeSubscriber readSubscriber) { + + this.subscribe(readSubscriber); + writePublisher.subscribe(this); + } + + /** + * Returns true if this flow was completed, either exceptionally + * or normally (EOF reached). + * @return true if the flow is finished + */ + boolean isFinished(); + + + /** + * Returns {@code s} if {@code s} is a {@code TubeSubscriber}, otherwise + * wraps it in a {@code TubeSubscriber}. + * Using the wrapper is only appropriate in the case where + * {@code dropSubscription} doesn't need to be implemented, and the + * {@code TubeSubscriber} is subscribed only once. + * + * @param s a subscriber for reading. + * @return a {@code TubeSubscriber}: either {@code s} if {@code s} is a + * {@code TubeSubscriber}, otherwise a {@code TubeSubscriber} + * wrapper that delegates to {@code s} + */ + static TubeSubscriber asTubeSubscriber(Flow.Subscriber> s) { + if (s instanceof TubeSubscriber) { + return (TubeSubscriber) s; + } + return new AbstractTubeSubscriber.TubeSubscriberWrapper(s); + } + + /** + * Returns {@code s} if {@code s} is a {@code TubePublisher}, otherwise + * wraps it in a {@code TubePublisher}. + * + * @param p a publisher for writing. + * @return a {@code TubePublisher}: either {@code s} if {@code s} is a + * {@code TubePublisher}, otherwise a {@code TubePublisher} + * wrapper that delegates to {@code s} + */ + static TubePublisher asTubePublisher(Flow.Publisher> p) { + if (p instanceof TubePublisher) { + return (TubePublisher) p; + } + return new AbstractTubePublisher.TubePublisherWrapper(p); + } + + /** + * Convenience abstract class for {@code TubePublisher} implementations. + * It is not required that a {@code TubePublisher} implementation extends + * this class. + */ + static abstract class AbstractTubePublisher implements TubePublisher { + static final class TubePublisherWrapper extends AbstractTubePublisher { + final Flow.Publisher> delegate; + public TubePublisherWrapper(Flow.Publisher> delegate) { + this.delegate = delegate; + } + @Override + public void subscribe(Flow.Subscriber> subscriber) { + delegate.subscribe(subscriber); + } + } + } + + /** + * Convenience abstract class for {@code TubeSubscriber} implementations. + * It is not required that a {@code TubeSubscriber} implementation extends + * this class. + */ + static abstract class AbstractTubeSubscriber implements TubeSubscriber { + static final class TubeSubscriberWrapper extends AbstractTubeSubscriber { + final Flow.Subscriber> delegate; + TubeSubscriberWrapper(Flow.Subscriber> delegate) { + this.delegate = delegate; + } + @Override + public void dropSubscription() {} + @Override + public void onSubscribe(Flow.Subscription subscription) { + delegate.onSubscribe(subscription); + } + @Override + public void onNext(List item) { + delegate.onNext(item); + } + @Override + public void onError(Throwable throwable) { + delegate.onError(throwable); + } + @Override + public void onComplete() { + delegate.onComplete(); + } + } + + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersImpl.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.common; + +import java.net.http.HttpHeaders; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Implementation of HttpHeaders. + */ +public class HttpHeadersImpl extends HttpHeaders { + + private final TreeMap> headers; + + public HttpHeadersImpl() { + headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + + @Override + public Map> map() { + return Collections.unmodifiableMap(headers); + } + + // package private mutators + + public HttpHeadersImpl deepCopy() { + HttpHeadersImpl h1 = new HttpHeadersImpl(); + for (Map.Entry> entry : headers.entrySet()) { + List valuesCopy = new ArrayList<>(entry.getValue()); + h1.headers.put(entry.getKey(), valuesCopy); + } + return h1; + } + + public void addHeader(String name, String value) { + headers.computeIfAbsent(name, k -> new ArrayList<>(1)) + .add(value); + } + + public void setHeader(String name, String value) { + List values = new ArrayList<>(1); // most headers has one value + values.add(value); + headers.put(name, values); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.common; + +import java.net.http.HttpHeaders; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import jdk.internal.net.http.frame.DataFrame; +import jdk.internal.net.http.frame.Http2Frame; +import jdk.internal.net.http.frame.WindowUpdateFrame; + +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLParameters; + +/** + * -Djava.net.HttpClient.log= + * errors,requests,headers, + * frames[:control:data:window:all..],content,ssl,trace + * + * Any of errors, requests, headers or content are optional. + * + * Other handlers may be added. All logging is at level INFO + * + * Logger name is "jdk.httpclient.HttpClient" + */ +// implements System.Logger in order to be skipped when printing the caller's +// information +public abstract class Log implements System.Logger { + + static final String logProp = "jdk.httpclient.HttpClient.log"; + + public static final int OFF = 0; + public static final int ERRORS = 0x1; + public static final int REQUESTS = 0x2; + public static final int HEADERS = 0x4; + public static final int CONTENT = 0x8; + public static final int FRAMES = 0x10; + public static final int SSL = 0x20; + public static final int TRACE = 0x40; + static int logging; + + // Frame types: "control", "data", "window", "all" + public static final int CONTROL = 1; // all except DATA and WINDOW_UPDATES + public static final int DATA = 2; + public static final int WINDOW_UPDATES = 4; + public static final int ALL = CONTROL| DATA | WINDOW_UPDATES; + static int frametypes; + + static final System.Logger logger; + + static { + String s = Utils.getNetProperty(logProp); + if (s == null) { + logging = OFF; + } else { + String[] vals = s.split(","); + for (String val : vals) { + switch (val.toLowerCase(Locale.US)) { + case "errors": + logging |= ERRORS; + break; + case "requests": + logging |= REQUESTS; + break; + case "headers": + logging |= HEADERS; + break; + case "content": + logging |= CONTENT; + break; + case "ssl": + logging |= SSL; + break; + case "trace": + logging |= TRACE; + break; + case "all": + logging |= CONTENT|HEADERS|REQUESTS|FRAMES|ERRORS|TRACE|SSL; + break; + } + if (val.startsWith("frames")) { + logging |= FRAMES; + String[] types = val.split(":"); + if (types.length == 1) { + frametypes = CONTROL | DATA | WINDOW_UPDATES; + } else { + for (String type : types) { + switch (type.toLowerCase()) { + case "control": + frametypes |= CONTROL; + break; + case "data": + frametypes |= DATA; + break; + case "window": + frametypes |= WINDOW_UPDATES; + break; + case "all": + frametypes = ALL; + break; + } + } + } + } + } + } + if (logging != OFF) { + logger = System.getLogger("jdk.httpclient.HttpClient"); + } else { + logger = null; + } + } + public static boolean errors() { + return (logging & ERRORS) != 0; + } + + public static boolean requests() { + return (logging & REQUESTS) != 0; + } + + public static boolean headers() { + return (logging & HEADERS) != 0; + } + + public static boolean trace() { + return (logging & TRACE) != 0; + } + + public static boolean ssl() { + return (logging & SSL) != 0; + } + + public static boolean frames() { + return (logging & FRAMES) != 0; + } + + public static void logError(String s, Object... s1) { + if (errors()) { + logger.log(Level.INFO, "ERROR: " + s, s1); + } + } + + public static void logError(Throwable t) { + if (errors()) { + String s = Utils.stackTrace(t); + logger.log(Level.INFO, "ERROR: " + s); + } + } + + public static void logSSL(String s, Object... s1) { + if (ssl()) { + logger.log(Level.INFO, "SSL: " + s, s1); + } + } + + public static void logSSL(Supplier msgSupplier) { + if (ssl()) { + logger.log(Level.INFO, "SSL: " + msgSupplier.get()); + } + } + + public static void logTrace(String s, Object... s1) { + if (trace()) { + String format = "TRACE: " + s; + logger.log(Level.INFO, format, s1); + } + } + + public static void logRequest(String s, Object... s1) { + if (requests()) { + logger.log(Level.INFO, "REQUEST: " + s, s1); + } + } + + public static void logResponse(Supplier supplier) { + if (requests()) { + logger.log(Level.INFO, "RESPONSE: " + supplier.get()); + } + } + + public static void logHeaders(String s, Object... s1) { + if (headers()) { + logger.log(Level.INFO, "HEADERS: " + s, s1); + } + } + + public static boolean loggingFrame(Class clazz) { + if (frametypes == ALL) { + return true; + } + if (clazz == DataFrame.class) { + return (frametypes & DATA) != 0; + } else if (clazz == WindowUpdateFrame.class) { + return (frametypes & WINDOW_UPDATES) != 0; + } else { + return (frametypes & CONTROL) != 0; + } + } + + public static void logFrames(Http2Frame f, String direction) { + if (frames() && loggingFrame(f.getClass())) { + logger.log(Level.INFO, "FRAME: " + direction + ": " + f.toString()); + } + } + + public static void logParams(SSLParameters p) { + if (!Log.ssl()) { + return; + } + + if (p == null) { + Log.logSSL("SSLParameters: Null params"); + return; + } + + final StringBuilder sb = new StringBuilder("SSLParameters:"); + final List params = new ArrayList<>(); + if (p.getCipherSuites() != null) { + for (String cipher : p.getCipherSuites()) { + sb.append("\n cipher: {") + .append(params.size()).append("}"); + params.add(cipher); + } + } + + // SSLParameters.getApplicationProtocols() can't return null + // JDK 8 EXCL START + for (String approto : p.getApplicationProtocols()) { + sb.append("\n application protocol: {") + .append(params.size()).append("}"); + params.add(approto); + } + // JDK 8 EXCL END + + if (p.getProtocols() != null) { + for (String protocol : p.getProtocols()) { + sb.append("\n protocol: {") + .append(params.size()).append("}"); + params.add(protocol); + } + } + + if (p.getServerNames() != null) { + for (SNIServerName sname : p.getServerNames()) { + sb.append("\n server name: {") + .append(params.size()).append("}"); + params.add(sname.toString()); + } + } + sb.append('\n'); + + Log.logSSL(sb.toString(), params.toArray()); + } + + public static void dumpHeaders(StringBuilder sb, String prefix, HttpHeaders headers) { + if (headers != null) { + Map> h = headers.map(); + Set>> entries = h.entrySet(); + for (Map.Entry> entry : entries) { + String key = entry.getKey(); + List values = entry.getValue(); + sb.append(prefix).append(key).append(":"); + for (String value : values) { + sb.append(' ').append(value); + } + sb.append('\n'); + } + } + } + + + // not instantiable + private Log() {} +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/MinimalFuture.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/MinimalFuture.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.common; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicLong; + +import static java.util.Objects.requireNonNull; + +/* + * A CompletableFuture which does not allow any obtrusion logic. + * All methods of CompletionStage return instances of this class. + */ +public final class MinimalFuture extends CompletableFuture { + + @FunctionalInterface + public interface ExceptionalSupplier { + U get() throws Throwable; + } + + private final static AtomicLong TOKENS = new AtomicLong(); + private final long id; + + public static MinimalFuture completedFuture(U value) { + MinimalFuture f = new MinimalFuture<>(); + f.complete(value); + return f; + } + + public static CompletableFuture failedFuture(Throwable ex) { + requireNonNull(ex); + MinimalFuture f = new MinimalFuture<>(); + f.completeExceptionally(ex); + return f; + } + + public static CompletableFuture supply(ExceptionalSupplier supplier) { + CompletableFuture cf = new MinimalFuture<>(); + try { + U value = supplier.get(); + cf.complete(value); + } catch (Throwable t) { + cf.completeExceptionally(t); + } + return cf; + } + + public MinimalFuture() { + super(); + this.id = TOKENS.incrementAndGet(); + } + + @Override + public MinimalFuture newIncompleteFuture() { + return new MinimalFuture<>(); + } + + @Override + public void obtrudeValue(T value) { + throw new UnsupportedOperationException(); + } + + @Override + public void obtrudeException(Throwable ex) { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + return super.toString() + " (id=" + id +")"; + } + + public static MinimalFuture of(CompletionStage stage) { + MinimalFuture cf = new MinimalFuture<>(); + stage.whenComplete((r,t) -> complete(cf, r, t)); + return cf; + } + + private static void complete(CompletableFuture cf, U result, Throwable t) { + if (t == null) { + cf.complete(result); + } else { + cf.completeExceptionally(t); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/Pair.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Pair.java Wed Feb 07 21:45:37 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.internal.net.http.common; + +/** + * A simple paired value class + */ +public final class Pair { + + public Pair(T first, U second) { + this.second = second; + this.first = first; + } + + public final T first; + public final U second; + + // Because 'pair()' is shorter than 'new Pair<>()'. + // Sometimes this difference might be very significant (especially in a + // 80-ish characters boundary). Sorry diamond operator. + public static Pair pair(T first, U second) { + return new Pair<>(first, second); + } + + @Override + public String toString() { + return "(" + first + ", " + second + ")"; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/SSLFlowDelegate.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/SSLFlowDelegate.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,906 @@ +/* + * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.common; + +import jdk.internal.net.http.common.SubscriberWrapper.SchedulingAction; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLEngineResult.Status; +import javax.net.ssl.SSLException; +import java.io.IOException; +import java.lang.System.Logger.Level; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.Flow; +import java.util.concurrent.Flow.Subscriber; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Implements SSL using two SubscriberWrappers. + * + *

Constructor takes two Flow.Subscribers: one that receives the network + * data (after it has been encrypted by SSLFlowDelegate) data, and one that + * receives the application data (before it has been encrypted by SSLFlowDelegate). + * + *

Methods upstreamReader() and upstreamWriter() return the corresponding + * Flow.Subscribers containing Flows for the encrypted/decrypted upstream data. + * See diagram below. + * + *

How Flow.Subscribers are used in this class, and where they come from: + *

+ * {@code
+ *
+ *
+ *
+ * --------->  data flow direction
+ *
+ *
+ *                         +------------------+
+ *        upstreamWriter   |                  | downWriter
+ *        ---------------> |                  | ------------>
+ *  obtained from this     |                  | supplied to constructor
+ *                         | SSLFlowDelegate  |
+ *        downReader       |                  | upstreamReader
+ *        <--------------- |                  | <--------------
+ * supplied to constructor |                  | obtained from this
+ *                         +------------------+
+ * }
+ * 
+ */ +public class SSLFlowDelegate { + + static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag. + final System.Logger debug = + Utils.getDebugLogger(this::dbgString, DEBUG); + + final Executor exec; + final Reader reader; + final Writer writer; + final SSLEngine engine; + final String tubeName; // hack + private final CompletableFuture cf; + final CompletableFuture alpnCF; // completes on initial handshake + final static ByteBuffer SENTINEL = Utils.EMPTY_BYTEBUFFER; + volatile boolean close_notify_received; + + /** + * Creates an SSLFlowDelegate fed from two Flow.Subscribers. Each + * Flow.Subscriber requires an associated {@link CompletableFuture} + * for errors that need to be signaled from downstream to upstream. + */ + public SSLFlowDelegate(SSLEngine engine, + Executor exec, + Subscriber> downReader, + Subscriber> downWriter) + { + this.tubeName = String.valueOf(downWriter); + this.reader = new Reader(); + this.writer = new Writer(); + this.engine = engine; + this.exec = exec; + this.handshakeState = new AtomicInteger(NOT_HANDSHAKING); + CompletableFuture cs = CompletableFuture.allOf( + reader.completion(), writer.completion()).thenRun(this::normalStop); + this.cf = MinimalFuture.of(cs); + this.alpnCF = new MinimalFuture<>(); + + // connect the Reader to the downReader and the + // Writer to the downWriter. + connect(downReader, downWriter); + + //Monitor.add(this::monitor); + } + + /** + * Returns true if the SSLFlowDelegate has detected a TLS + * close_notify from the server. + * @return true, if a close_notify was detected. + */ + public boolean closeNotifyReceived() { + return close_notify_received; + } + + /** + * Connects the read sink (downReader) to the SSLFlowDelegate Reader, + * and the write sink (downWriter) to the SSLFlowDelegate Writer. + * Called from within the constructor. Overwritten by SSLTube. + * + * @param downReader The left hand side read sink (typically, the + * HttpConnection read subscriber). + * @param downWriter The right hand side write sink (typically + * the SocketTube write subscriber). + */ + void connect(Subscriber> downReader, + Subscriber> downWriter) { + this.reader.subscribe(downReader); + this.writer.subscribe(downWriter); + } + + /** + * Returns a CompletableFuture which completes after + * the initial handshake completes, and which contains the negotiated + * alpn. + */ + public CompletableFuture alpn() { + return alpnCF; + } + + private void setALPN() { + // Handshake is finished. So, can retrieve the ALPN now + if (alpnCF.isDone()) + return; + String alpn = engine.getApplicationProtocol(); + debug.log(Level.DEBUG, "setALPN = %s", alpn); + alpnCF.complete(alpn); + } + + public String monitor() { + StringBuilder sb = new StringBuilder(); + sb.append("SSL: HS state: " + states(handshakeState)); + sb.append(" Engine state: " + engine.getHandshakeStatus().toString()); + sb.append(" LL : "); + synchronized(stateList) { + for (String s: stateList) { + sb.append(s).append(" "); + } + } + sb.append("\r\n"); + sb.append("Reader:: ").append(reader.toString()); + sb.append("\r\n"); + sb.append("Writer:: ").append(writer.toString()); + sb.append("\r\n==================================="); + return sb.toString(); + } + + protected SchedulingAction enterReadScheduling() { + return SchedulingAction.CONTINUE; + } + + + /** + * Processing function for incoming data. Pass it thru SSLEngine.unwrap(). + * Any decrypted buffers returned to be passed downstream. + * Status codes: + * NEED_UNWRAP: do nothing. Following incoming data will contain + * any required handshake data + * NEED_WRAP: call writer.addData() with empty buffer + * NEED_TASK: delegate task to executor + * BUFFER_OVERFLOW: allocate larger output buffer. Repeat unwrap + * BUFFER_UNDERFLOW: keep buffer and wait for more data + * OK: return generated buffers. + * + * Upstream subscription strategy is to try and keep no more than + * TARGET_BUFSIZE bytes in readBuf + */ + class Reader extends SubscriberWrapper { + final SequentialScheduler scheduler; + static final int TARGET_BUFSIZE = 16 * 1024; + volatile ByteBuffer readBuf; + volatile boolean completing = false; + final Object readBufferLock = new Object(); + final System.Logger debugr = + Utils.getDebugLogger(this::dbgString, DEBUG); + + class ReaderDownstreamPusher implements Runnable { + @Override public void run() { processData(); } + } + + Reader() { + super(); + scheduler = SequentialScheduler.synchronizedScheduler( + new ReaderDownstreamPusher()); + this.readBuf = ByteBuffer.allocate(1024); + readBuf.limit(0); // keep in read mode + } + + protected SchedulingAction enterScheduling() { + return enterReadScheduling(); + } + + public final String dbgString() { + return "SSL Reader(" + tubeName + ")"; + } + + /** + * entry point for buffers delivered from upstream Subscriber + */ + @Override + public void incoming(List buffers, boolean complete) { + debugr.log(Level.DEBUG, () -> "Adding " + Utils.remaining(buffers) + + " bytes to read buffer"); + addToReadBuf(buffers, complete); + scheduler.runOrSchedule(); + } + + @Override + public String toString() { + return "READER: " + super.toString() + " readBuf: " + readBuf.toString() + + " count: " + count.toString(); + } + + private void reallocReadBuf() { + int sz = readBuf.capacity(); + ByteBuffer newb = ByteBuffer.allocate(sz*2); + readBuf.flip(); + Utils.copy(readBuf, newb); + readBuf = newb; + } + + @Override + protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) { + if (readBuf.remaining() > TARGET_BUFSIZE) { + return 0; + } else { + return super.upstreamWindowUpdate(currentWindow, downstreamQsize); + } + } + + // readBuf is kept ready for reading outside of this method + private void addToReadBuf(List buffers, boolean complete) { + synchronized (readBufferLock) { + for (ByteBuffer buf : buffers) { + readBuf.compact(); + while (readBuf.remaining() < buf.remaining()) + reallocReadBuf(); + readBuf.put(buf); + readBuf.flip(); + } + if (complete) { + this.completing = complete; + } + } + } + + void schedule() { + scheduler.runOrSchedule(); + } + + void stop() { + debugr.log(Level.DEBUG, "stop"); + scheduler.stop(); + } + + AtomicInteger count = new AtomicInteger(0); + + // work function where it all happens + void processData() { + try { + debugr.log(Level.DEBUG, () -> "processData: " + readBuf.remaining() + + " bytes to unwrap " + + states(handshakeState) + + ", " + engine.getHandshakeStatus()); + int len; + boolean complete = false; + while ((len = readBuf.remaining()) > 0) { + boolean handshaking = false; + try { + EngineResult result; + synchronized (readBufferLock) { + complete = this.completing; + result = unwrapBuffer(readBuf); + debugr.log(Level.DEBUG, "Unwrapped: %s", result.result); + } + if (result.bytesProduced() > 0) { + debugr.log(Level.DEBUG, "sending %d", result.bytesProduced()); + count.addAndGet(result.bytesProduced()); + outgoing(result.destBuffer, false); + } + if (result.status() == Status.BUFFER_UNDERFLOW) { + debugr.log(Level.DEBUG, "BUFFER_UNDERFLOW"); + // not enough data in the read buffer... + requestMore(); + synchronized (readBufferLock) { + // check if we have received some data + if (readBuf.remaining() > len) continue; + return; + } + } + if (complete && result.status() == Status.CLOSED) { + debugr.log(Level.DEBUG, "Closed: completing"); + outgoing(Utils.EMPTY_BB_LIST, true); + return; + } + if (result.handshaking() && !complete) { + debugr.log(Level.DEBUG, "handshaking"); + doHandshake(result, READER); + resumeActivity(); + handshaking = true; + } else { + if ((handshakeState.getAndSet(NOT_HANDSHAKING) & ~DOING_TASKS) == HANDSHAKING) { + setALPN(); + handshaking = false; + resumeActivity(); + } + } + } catch (IOException ex) { + errorCommon(ex); + handleError(ex); + } + if (handshaking && !complete) + return; + } + if (!complete) { + synchronized (readBufferLock) { + complete = this.completing && !readBuf.hasRemaining(); + } + } + if (complete) { + debugr.log(Level.DEBUG, "completing"); + // Complete the alpnCF, if not already complete, regardless of + // whether or not the ALPN is available, there will be no more + // activity. + setALPN(); + outgoing(Utils.EMPTY_BB_LIST, true); + } + } catch (Throwable ex) { + errorCommon(ex); + handleError(ex); + } + } + } + + /** + * Returns a CompletableFuture which completes after all activity + * in the delegate is terminated (whether normally or exceptionally). + * + * @return + */ + public CompletableFuture completion() { + return cf; + } + + public interface Monitorable { + public String getInfo(); + } + + public static class Monitor extends Thread { + final List list; + static Monitor themon; + + static { + themon = new Monitor(); + themon.start(); // uncomment to enable Monitor + } + + Monitor() { + super("Monitor"); + setDaemon(true); + list = Collections.synchronizedList(new LinkedList<>()); + } + + void addTarget(Monitorable o) { + list.add(o); + } + + public static void add(Monitorable o) { + themon.addTarget(o); + } + + @Override + public void run() { + System.out.println("Monitor starting"); + while (true) { + try {Thread.sleep(20*1000); } catch (Exception e) {} + synchronized (list) { + for (Monitorable o : list) { + System.out.println(o.getInfo()); + System.out.println("-------------------------"); + } + } + System.out.println("--o-o-o-o-o-o-o-o-o-o-o-o-o-o-"); + + } + } + } + + /** + * Processing function for outgoing data. Pass it thru SSLEngine.wrap() + * Any encrypted buffers generated are passed downstream to be written. + * Status codes: + * NEED_UNWRAP: call reader.addData() with empty buffer + * NEED_WRAP: call addData() with empty buffer + * NEED_TASK: delegate task to executor + * BUFFER_OVERFLOW: allocate larger output buffer. Repeat wrap + * BUFFER_UNDERFLOW: shouldn't happen on writing side + * OK: return generated buffers + */ + class Writer extends SubscriberWrapper { + final SequentialScheduler scheduler; + // queues of buffers received from upstream waiting + // to be processed by the SSLEngine + final List writeList; + final System.Logger debugw = + Utils.getDebugLogger(this::dbgString, DEBUG); + volatile boolean completing; + boolean completed; // only accessed in processData + + class WriterDownstreamPusher extends SequentialScheduler.CompleteRestartableTask { + @Override public void run() { processData(); } + } + + Writer() { + super(); + writeList = Collections.synchronizedList(new LinkedList<>()); + scheduler = new SequentialScheduler(new WriterDownstreamPusher()); + } + + @Override + protected void incoming(List buffers, boolean complete) { + assert complete ? buffers == Utils.EMPTY_BB_LIST : true; + assert buffers != Utils.EMPTY_BB_LIST ? complete == false : true; + if (complete) { + debugw.log(Level.DEBUG, "adding SENTINEL"); + completing = true; + writeList.add(SENTINEL); + } else { + writeList.addAll(buffers); + } + debugw.log(Level.DEBUG, () -> "added " + buffers.size() + + " (" + Utils.remaining(buffers) + + " bytes) to the writeList"); + scheduler.runOrSchedule(); + } + + public final String dbgString() { + return "SSL Writer(" + tubeName + ")"; + } + + protected void onSubscribe() { + doHandshake(EngineResult.INIT, INIT); + resumeActivity(); + } + + void schedule() { + scheduler.runOrSchedule(); + } + + void stop() { + debugw.log(Level.DEBUG, "stop"); + scheduler.stop(); + } + + @Override + public boolean closing() { + return closeNotifyReceived(); + } + + private boolean isCompleting() { + return completing; + } + + @Override + protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) { + if (writeList.size() > 10) + return 0; + else + return super.upstreamWindowUpdate(currentWindow, downstreamQsize); + } + + private boolean hsTriggered() { + synchronized(writeList) { + for (ByteBuffer b : writeList) + if (b == HS_TRIGGER) + return true; + return false; + } + } + + private void processData() { + boolean completing = isCompleting(); + + try { + debugw.log(Level.DEBUG, () -> "processData(" + Utils.remaining(writeList) + ")"); + while (Utils.remaining(writeList) > 0 || hsTriggered() + || needWrap()) { + ByteBuffer[] outbufs = writeList.toArray(Utils.EMPTY_BB_ARRAY); + EngineResult result = wrapBuffers(outbufs); + debugw.log(Level.DEBUG, "wrapBuffer returned %s", result.result); + + if (result.status() == Status.CLOSED) { + if (result.bytesProduced() <= 0) + return; + + if (!completing && !completed) { + completing = this.completing = true; + // There could still be some outgoing data in outbufs. + writeList.add(SENTINEL); + } + } + + boolean handshaking = false; + if (result.handshaking()) { + debugw.log(Level.DEBUG, "handshaking"); + doHandshake(result, WRITER); + handshaking = true; + } else { + if ((handshakeState.getAndSet(NOT_HANDSHAKING) & ~DOING_TASKS) == HANDSHAKING) { + setALPN(); + resumeActivity(); + } + } + cleanList(writeList); // tidy up the source list + sendResultBytes(result); + if (handshaking && !completing) { + if (writeList.isEmpty() && !result.needUnwrap()) { + writer.addData(HS_TRIGGER); + } + if (needWrap()) continue; + return; + } + } + if (completing && Utils.remaining(writeList) == 0) { + /* + System.out.println("WRITER DOO 3"); + engine.closeOutbound(); + EngineResult result = wrapBuffers(Utils.EMPTY_BB_ARRAY); + sendResultBytes(result); + */ + if (!completed) { + completed = true; + writeList.clear(); + outgoing(Utils.EMPTY_BB_LIST, true); + } + return; + } + if (writeList.isEmpty() && needWrap()) { + writer.addData(HS_TRIGGER); + } + } catch (Throwable ex) { + errorCommon(ex); + handleError(ex); + } + } + + private boolean needWrap() { + return engine.getHandshakeStatus() == HandshakeStatus.NEED_WRAP; + } + + private void sendResultBytes(EngineResult result) { + if (result.bytesProduced() > 0) { + debugw.log(Level.DEBUG, "Sending %d bytes downstream", + result.bytesProduced()); + outgoing(result.destBuffer, false); + } + } + + @Override + public String toString() { + return "WRITER: " + super.toString() + + " writeList size " + Integer.toString(writeList.size()); + //" writeList: " + writeList.toString(); + } + } + + private void handleError(Throwable t) { + debug.log(Level.DEBUG, "handleError", t); + cf.completeExceptionally(t); + // no-op if already completed + alpnCF.completeExceptionally(t); + reader.stop(); + writer.stop(); + } + + private void normalStop() { + reader.stop(); + writer.stop(); + } + + private void cleanList(List l) { + synchronized (l) { + Iterator iter = l.iterator(); + while (iter.hasNext()) { + ByteBuffer b = iter.next(); + if (!b.hasRemaining() && b != SENTINEL) { + iter.remove(); + } + } + } + } + + /** + * States for handshake. We avoid races when accessing/updating the AtomicInt + * because updates always schedule an additional call to both the read() + * and write() functions. + */ + private static final int NOT_HANDSHAKING = 0; + private static final int HANDSHAKING = 1; + private static final int INIT = 2; + private static final int DOING_TASKS = 4; // bit added to above state + private static final ByteBuffer HS_TRIGGER = ByteBuffer.allocate(0); + + private static final int READER = 1; + private static final int WRITER = 2; + + private static String states(AtomicInteger state) { + int s = state.get(); + StringBuilder sb = new StringBuilder(); + int x = s & ~DOING_TASKS; + switch (x) { + case NOT_HANDSHAKING: + sb.append(" NOT_HANDSHAKING "); + break; + case HANDSHAKING: + sb.append(" HANDSHAKING "); + break; + case INIT: + sb.append(" INIT "); + break; + default: + throw new InternalError(); + } + if ((s & DOING_TASKS) > 0) + sb.append("|DOING_TASKS"); + return sb.toString(); + } + + private void resumeActivity() { + reader.schedule(); + writer.schedule(); + } + + final AtomicInteger handshakeState; + final ConcurrentLinkedQueue stateList = new ConcurrentLinkedQueue<>(); + + private void doHandshake(EngineResult r, int caller) { + int s = handshakeState.getAndAccumulate(HANDSHAKING, (current, update) -> update | (current & DOING_TASKS)); + stateList.add(r.handshakeStatus().toString()); + stateList.add(Integer.toString(caller)); + switch (r.handshakeStatus()) { + case NEED_TASK: + if ((s & DOING_TASKS) > 0) // someone else was doing tasks + return; + List tasks = obtainTasks(); + executeTasks(tasks); + break; + case NEED_WRAP: + writer.addData(HS_TRIGGER); + break; + case NEED_UNWRAP: + case NEED_UNWRAP_AGAIN: + // do nothing else + break; + default: + throw new InternalError("Unexpected handshake status:" + + r.handshakeStatus()); + } + } + + private List obtainTasks() { + List l = new ArrayList<>(); + Runnable r; + while ((r = engine.getDelegatedTask()) != null) { + l.add(r); + } + return l; + } + + private void executeTasks(List tasks) { + exec.execute(() -> { + try { + handshakeState.getAndUpdate((current) -> current | DOING_TASKS); + List nextTasks = tasks; + do { + nextTasks.forEach(Runnable::run); + if (engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + nextTasks = obtainTasks(); + } else { + break; + } + } while (true); + handshakeState.getAndUpdate((current) -> current & ~DOING_TASKS); + writer.addData(HS_TRIGGER); + resumeActivity(); + } catch (Throwable t) { + handleError(t); + } + }); + } + + + EngineResult unwrapBuffer(ByteBuffer src) throws IOException { + ByteBuffer dst = getAppBuffer(); + while (true) { + SSLEngineResult sslResult = engine.unwrap(src, dst); + switch (sslResult.getStatus()) { + case BUFFER_OVERFLOW: + // may happen only if app size buffer was changed. + // get it again if app buffer size changed + int appSize = engine.getSession().getApplicationBufferSize(); + ByteBuffer b = ByteBuffer.allocate(appSize + dst.position()); + dst.flip(); + b.put(dst); + dst = b; + break; + case CLOSED: + return doClosure(new EngineResult(sslResult)); + case BUFFER_UNDERFLOW: + // handled implicitly by compaction/reallocation of readBuf + return new EngineResult(sslResult); + case OK: + dst.flip(); + return new EngineResult(sslResult, dst); + } + } + } + + // FIXME: acknowledge a received CLOSE request from peer + EngineResult doClosure(EngineResult r) throws IOException { + debug.log(Level.DEBUG, + "doClosure(%s): %s [isOutboundDone: %s, isInboundDone: %s]", + r.result, engine.getHandshakeStatus(), + engine.isOutboundDone(), engine.isInboundDone()); + if (engine.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) { + // we have received TLS close_notify and need to send + // an acknowledgement back. We're calling doHandshake + // to finish the close handshake. + if (engine.isInboundDone() && !engine.isOutboundDone()) { + debug.log(Level.DEBUG, "doClosure: close_notify received"); + close_notify_received = true; + doHandshake(r, READER); + } + } + return r; + } + + /** + * Returns the upstream Flow.Subscriber of the reading (incoming) side. + * This flow must be given the encrypted data read from upstream (eg socket) + * before it is decrypted. + */ + public Flow.Subscriber> upstreamReader() { + return reader; + } + + /** + * Returns the upstream Flow.Subscriber of the writing (outgoing) side. + * This flow contains the plaintext data before it is encrypted. + */ + public Flow.Subscriber> upstreamWriter() { + return writer; + } + + public boolean resumeReader() { + return reader.signalScheduling(); + } + + public void resetReaderDemand() { + reader.resetDownstreamDemand(); + } + + static class EngineResult { + final SSLEngineResult result; + final ByteBuffer destBuffer; + + // normal result + EngineResult(SSLEngineResult result) { + this(result, null); + } + + EngineResult(SSLEngineResult result, ByteBuffer destBuffer) { + this.result = result; + this.destBuffer = destBuffer; + } + + // Special result used to trigger handshaking in constructor + static EngineResult INIT = + new EngineResult( + new SSLEngineResult(SSLEngineResult.Status.OK, HandshakeStatus.NEED_WRAP, 0, 0)); + + boolean handshaking() { + HandshakeStatus s = result.getHandshakeStatus(); + return s != HandshakeStatus.FINISHED + && s != HandshakeStatus.NOT_HANDSHAKING + && result.getStatus() != Status.CLOSED; + } + + boolean needUnwrap() { + HandshakeStatus s = result.getHandshakeStatus(); + return s == HandshakeStatus.NEED_UNWRAP; + } + + + int bytesConsumed() { + return result.bytesConsumed(); + } + + int bytesProduced() { + return result.bytesProduced(); + } + + SSLEngineResult.HandshakeStatus handshakeStatus() { + return result.getHandshakeStatus(); + } + + SSLEngineResult.Status status() { + return result.getStatus(); + } + } + + public ByteBuffer getNetBuffer() { + return ByteBuffer.allocate(engine.getSession().getPacketBufferSize()); + } + + private ByteBuffer getAppBuffer() { + return ByteBuffer.allocate(engine.getSession().getApplicationBufferSize()); + } + + final String dbgString() { + return "SSLFlowDelegate(" + tubeName + ")"; + } + + @SuppressWarnings("fallthrough") + EngineResult wrapBuffers(ByteBuffer[] src) throws SSLException { + debug.log(Level.DEBUG, () -> "wrapping " + + Utils.remaining(src) + " bytes"); + ByteBuffer dst = getNetBuffer(); + while (true) { + SSLEngineResult sslResult = engine.wrap(src, dst); + debug.log(Level.DEBUG, () -> "SSLResult: " + sslResult); + switch (sslResult.getStatus()) { + case BUFFER_OVERFLOW: + // Shouldn't happen. We allocated buffer with packet size + // get it again if net buffer size was changed + debug.log(Level.DEBUG, "BUFFER_OVERFLOW"); + int appSize = engine.getSession().getApplicationBufferSize(); + ByteBuffer b = ByteBuffer.allocate(appSize + dst.position()); + dst.flip(); + b.put(dst); + dst = b; + break; // try again + case CLOSED: + debug.log(Level.DEBUG, "CLOSED"); + // fallthrough. There could be some remaining data in dst. + // CLOSED will be handled by the caller. + case OK: + dst.flip(); + final ByteBuffer dest = dst; + debug.log(Level.DEBUG, () -> "OK => produced: " + + dest.remaining() + + " not wrapped: " + + Utils.remaining(src)); + return new EngineResult(sslResult, dest); + case BUFFER_UNDERFLOW: + // Shouldn't happen. Doesn't returns when wrap() + // underflow handled externally + // assert false : "Buffer Underflow"; + debug.log(Level.DEBUG, "BUFFER_UNDERFLOW"); + return new EngineResult(sslResult); + default: + debug.log(Level.DEBUG, "ASSERT"); + assert false; + } + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/SSLTube.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/SSLTube.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,586 @@ +/* + * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.common; + +import java.lang.System.Logger.Level; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import jdk.internal.net.http.common.SubscriberWrapper.SchedulingAction; +import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; +import static javax.net.ssl.SSLEngineResult.HandshakeStatus.FINISHED; + +/** + * An implementation of FlowTube that wraps another FlowTube in an + * SSL flow. + *

+ * The following diagram shows a typical usage of the SSLTube, where + * the SSLTube wraps a SocketTube on the right hand side, and is connected + * to an HttpConnection on the left hand side. + * + * {@code + * +---------- SSLTube -------------------------+ + * | | + * | +---SSLFlowDelegate---+ | + * HttpConnection | | | | SocketTube + * read sink <- SSLSubscriberW. <- Reader <- upstreamR.() <---- read source + * (a subscriber) | | \ / | | (a publisher) + * | | SSLEngine | | + * HttpConnection | | / \ | | SocketTube + * write source -> SSLSubscriptionW. -> upstreamW.() -> Writer ----> write sink + * (a publisher) | | | | (a subscriber) + * | +---------------------+ | + * | | + * +---------------------------------------------+ + * } + */ +public class SSLTube implements FlowTube { + + static final boolean DEBUG = Utils.DEBUG; // revisit: temporary developer's flag. + final System.Logger debug = + Utils.getDebugLogger(this::dbgString, DEBUG); + + private final FlowTube tube; + private final SSLSubscriberWrapper readSubscriber; + private final SSLSubscriptionWrapper writeSubscription; + private final SSLFlowDelegate sslDelegate; + private final SSLEngine engine; + private volatile boolean finished; + + public SSLTube(SSLEngine engine, Executor executor, FlowTube tube) { + Objects.requireNonNull(engine); + Objects.requireNonNull(executor); + this.tube = Objects.requireNonNull(tube); + writeSubscription = new SSLSubscriptionWrapper(); + readSubscriber = new SSLSubscriberWrapper(); + this.engine = engine; + sslDelegate = new SSLTubeFlowDelegate(engine, + executor, + readSubscriber, + tube); + } + + final class SSLTubeFlowDelegate extends SSLFlowDelegate { + SSLTubeFlowDelegate(SSLEngine engine, Executor executor, + SSLSubscriberWrapper readSubscriber, + FlowTube tube) { + super(engine, executor, readSubscriber, tube); + } + protected SchedulingAction enterReadScheduling() { + readSubscriber.processPendingSubscriber(); + return SchedulingAction.CONTINUE; + } + void connect(Flow.Subscriber> downReader, + Flow.Subscriber> downWriter) { + assert downWriter == tube; + assert downReader == readSubscriber; + + // Connect the read sink first. That's the left-hand side + // downstream subscriber from the HttpConnection (or more + // accurately, the SSLSubscriberWrapper that will wrap it + // when SSLTube::connectFlows is called. + reader.subscribe(downReader); + + // Connect the right hand side tube (the socket tube). + // + // The SSLFlowDelegate.writer publishes ByteBuffer to + // the SocketTube for writing on the socket, and the + // SSLFlowDelegate::upstreamReader subscribes to the + // SocketTube to receive ByteBuffers read from the socket. + // + // Basically this method is equivalent to: + // // connect the read source: + // // subscribe the SSLFlowDelegate upstream reader + // // to the socket tube publisher. + // tube.subscribe(upstreamReader()); + // // connect the write sink: + // // subscribe the socket tube write subscriber + // // with the SSLFlowDelegate downstream writer. + // writer.subscribe(tube); + tube.connectFlows(FlowTube.asTubePublisher(writer), + FlowTube.asTubeSubscriber(upstreamReader())); + + // Finally connect the write source. That's the left + // hand side publisher which will push ByteBuffer for + // writing and encryption to the SSLFlowDelegate. + // The writeSubscription is in fact the SSLSubscriptionWrapper + // that will wrap the subscription provided by the + // HttpConnection publisher when SSLTube::connectFlows + // is called. + upstreamWriter().onSubscribe(writeSubscription); + } + } + + public CompletableFuture getALPN() { + return sslDelegate.alpn(); + } + + @Override + public void subscribe(Flow.Subscriber> s) { + readSubscriber.dropSubscription(); + readSubscriber.setDelegate(s); + s.onSubscribe(readSubscription); + } + + /** + * Tells whether, or not, this FlowTube has finished receiving data. + * + * @return true when one of this FlowTube Subscriber's OnError or onComplete + * methods have been invoked + */ + @Override + public boolean isFinished() { + return finished; + } + + private volatile Flow.Subscription readSubscription; + + // The DelegateWrapper wraps a subscribed {@code Flow.Subscriber} and + // tracks the subscriber's state. In particular it makes sure that + // onComplete/onError are not called before onSubscribed. + final static class DelegateWrapper implements FlowTube.TubeSubscriber { + private final FlowTube.TubeSubscriber delegate; + private final System.Logger debug; + volatile boolean subscribedCalled; + volatile boolean subscribedDone; + volatile boolean completed; + volatile Throwable error; + DelegateWrapper(Flow.Subscriber> delegate, + System.Logger debug) { + this.delegate = FlowTube.asTubeSubscriber(delegate); + this.debug = debug; + } + + @Override + public void dropSubscription() { + if (subscribedCalled && !completed) { + delegate.dropSubscription(); + } + } + + @Override + public void onNext(List item) { + assert subscribedCalled; + delegate.onNext(item); + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + onSubscribe(delegate::onSubscribe, subscription); + } + + private void onSubscribe(Consumer method, + Flow.Subscription subscription) { + subscribedCalled = true; + method.accept(subscription); + Throwable x; + boolean finished; + synchronized (this) { + subscribedDone = true; + x = error; + finished = completed; + } + if (x != null) { + debug.log(Level.DEBUG, + "Subscriber completed before subscribe: forwarding %s", + (Object)x); + delegate.onError(x); + } else if (finished) { + debug.log(Level.DEBUG, + "Subscriber completed before subscribe: calling onComplete()"); + delegate.onComplete(); + } + } + + @Override + public void onError(Throwable t) { + if (completed) { + debug.log(Level.DEBUG, + "Subscriber already completed: ignoring %s", + (Object)t); + return; + } + boolean subscribed; + synchronized (this) { + if (completed) return; + error = t; + completed = true; + subscribed = subscribedDone; + } + if (subscribed) { + delegate.onError(t); + } else { + debug.log(Level.DEBUG, + "Subscriber not yet subscribed: stored %s", + (Object)t); + } + } + + @Override + public void onComplete() { + if (completed) return; + boolean subscribed; + synchronized (this) { + if (completed) return; + completed = true; + subscribed = subscribedDone; + } + if (subscribed) { + debug.log(Level.DEBUG, "DelegateWrapper: completing subscriber"); + delegate.onComplete(); + } else { + debug.log(Level.DEBUG, + "Subscriber not yet subscribed: stored completed=true"); + } + } + + @Override + public String toString() { + return "DelegateWrapper:" + delegate.toString(); + } + + } + + // Used to read data from the SSLTube. + final class SSLSubscriberWrapper implements FlowTube.TubeSubscriber { + private AtomicReference pendingDelegate = + new AtomicReference<>(); + private volatile DelegateWrapper subscribed; + private volatile boolean onCompleteReceived; + private final AtomicReference errorRef + = new AtomicReference<>(); + + // setDelegate can be called asynchronously when the SSLTube flow + // is connected. At this time the permanent subscriber (this class) + // may already be subscribed (readSubscription != null) or not. + // 1. If it's already subscribed (readSubscription != null), we + // are going to signal the SSLFlowDelegate reader, and make sure + // onSubscribed is called within the reader flow + // 2. If it's not yet subscribed (readSubscription == null), then + // we're going to wait for onSubscribe to be called. + // + void setDelegate(Flow.Subscriber> delegate) { + debug.log(Level.DEBUG, "SSLSubscriberWrapper (reader) got delegate: %s", + delegate); + assert delegate != null; + DelegateWrapper delegateWrapper = new DelegateWrapper(delegate, debug); + DelegateWrapper previous; + Flow.Subscription subscription; + boolean handleNow; + synchronized (this) { + previous = pendingDelegate.getAndSet(delegateWrapper); + subscription = readSubscription; + handleNow = this.errorRef.get() != null || finished; + } + if (previous != null) { + previous.dropSubscription(); + } + if (subscription == null) { + debug.log(Level.DEBUG, "SSLSubscriberWrapper (reader) no subscription yet"); + return; + } + if (handleNow || !sslDelegate.resumeReader()) { + processPendingSubscriber(); + } + } + + // Can be called outside of the flow if an error has already been + // raise. Otherwise, must be called within the SSLFlowDelegate + // downstream reader flow. + // If there is a subscription, and if there is a pending delegate, + // calls dropSubscription() on the previous delegate (if any), + // then subscribe the pending delegate. + void processPendingSubscriber() { + Flow.Subscription subscription; + DelegateWrapper delegateWrapper, previous; + synchronized (this) { + delegateWrapper = pendingDelegate.get(); + if (delegateWrapper == null) return; + subscription = readSubscription; + previous = subscribed; + } + if (subscription == null) { + debug.log(Level.DEBUG, + "SSLSubscriberWrapper (reader) %s", + "processPendingSubscriber: no subscription yet"); + return; + } + delegateWrapper = pendingDelegate.getAndSet(null); + if (delegateWrapper == null) return; + if (previous != null) { + previous.dropSubscription(); + } + onNewSubscription(delegateWrapper, subscription); + } + + @Override + public void dropSubscription() { + DelegateWrapper subscriberImpl = subscribed; + if (subscriberImpl != null) { + subscriberImpl.dropSubscription(); + } + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + debug.log(Level.DEBUG, + "SSLSubscriberWrapper (reader) onSubscribe(%s)", + subscription); + onSubscribeImpl(subscription); + } + + // called in the reader flow, from onSubscribe. + private void onSubscribeImpl(Flow.Subscription subscription) { + assert subscription != null; + DelegateWrapper subscriberImpl, pending; + synchronized (this) { + readSubscription = subscription; + subscriberImpl = subscribed; + pending = pendingDelegate.get(); + } + + if (subscriberImpl == null && pending == null) { + debug.log(Level.DEBUG, + "SSLSubscriberWrapper (reader) onSubscribeImpl: %s", + "no delegate yet"); + return; + } + + if (pending == null) { + // There is no pending delegate, but we have a previously + // subscribed delegate. This is obviously a re-subscribe. + // We are in the downstream reader flow, so we should call + // onSubscribe directly. + debug.log(Level.DEBUG, + "SSLSubscriberWrapper (reader) onSubscribeImpl: %s", + "resubscribing"); + onNewSubscription(subscriberImpl, subscription); + } else { + // We have some pending subscriber: subscribe it now that we have + // a subscription. If we already had a previous delegate then + // it will get a dropSubscription(). + debug.log(Level.DEBUG, + "SSLSubscriberWrapper (reader) onSubscribeImpl: %s", + "subscribing pending"); + processPendingSubscriber(); + } + } + + private void onNewSubscription(DelegateWrapper subscriberImpl, + Flow.Subscription subscription) { + assert subscriberImpl != null; + assert subscription != null; + + Throwable failed; + boolean completed; + // reset any demand that may have been made by the previous + // subscriber + sslDelegate.resetReaderDemand(); + // send the subscription to the subscriber. + subscriberImpl.onSubscribe(subscription); + + // The following twisted logic is just here that we don't invoke + // onError before onSubscribe. It also prevents race conditions + // if onError is invoked concurrently with setDelegate. + synchronized (this) { + failed = this.errorRef.get(); + completed = finished; + subscribed = subscriberImpl; + } + if (failed != null) { + subscriberImpl.onError(failed); + } else if (completed) { + subscriberImpl.onComplete(); + } + } + + @Override + public void onNext(List item) { + subscribed.onNext(item); + } + + public void onErrorImpl(Throwable throwable) { + // The following twisted logic is just here that we don't invoke + // onError before onSubscribe. It also prevents race conditions + // if onError is invoked concurrently with setDelegate. + // See setDelegate. + + errorRef.compareAndSet(null, throwable); + Throwable failed = errorRef.get(); + finished = true; + debug.log(Level.DEBUG, "%s: onErrorImpl: %s", this, throwable); + DelegateWrapper subscriberImpl; + synchronized (this) { + subscriberImpl = subscribed; + } + if (subscriberImpl != null) { + subscriberImpl.onError(failed); + } else { + debug.log(Level.DEBUG, "%s: delegate null, stored %s", this, failed); + } + // now if we have any pending subscriber, we should forward + // the error to them immediately as the read scheduler will + // already be stopped. + processPendingSubscriber(); + } + + @Override + public void onError(Throwable throwable) { + assert !finished && !onCompleteReceived; + onErrorImpl(throwable); + } + + private boolean handshaking() { + HandshakeStatus hs = engine.getHandshakeStatus(); + return !(hs == NOT_HANDSHAKING || hs == FINISHED); + } + + private boolean handshakeFailed() { + // sslDelegate can be null if we reach here + // during the initial handshake, as that happens + // within the SSLFlowDelegate constructor. + // In that case we will want to raise an exception. + return handshaking() + && (sslDelegate == null + || !sslDelegate.closeNotifyReceived()); + } + + @Override + public void onComplete() { + assert !finished && !onCompleteReceived; + onCompleteReceived = true; + DelegateWrapper subscriberImpl; + synchronized(this) { + subscriberImpl = subscribed; + } + + if (handshakeFailed()) { + debug.log(Level.DEBUG, + "handshake: %s, inbound done: %s outbound done: %s", + engine.getHandshakeStatus(), + engine.isInboundDone(), + engine.isOutboundDone()); + onErrorImpl(new SSLHandshakeException( + "Remote host terminated the handshake")); + } else if (subscriberImpl != null) { + finished = true; + subscriberImpl.onComplete(); + } + // now if we have any pending subscriber, we should complete + // them immediately as the read scheduler will already be stopped. + processPendingSubscriber(); + } + } + + @Override + public void connectFlows(TubePublisher writePub, + TubeSubscriber readSub) { + debug.log(Level.DEBUG, "connecting flows"); + readSubscriber.setDelegate(readSub); + writePub.subscribe(this); + } + + /** Outstanding write demand from the SSL Flow Delegate. */ + private final Demand writeDemand = new Demand(); + + final class SSLSubscriptionWrapper implements Flow.Subscription { + + volatile Flow.Subscription delegate; + + void setSubscription(Flow.Subscription sub) { + long demand = writeDemand.get(); // FIXME: isn't it a racy way of passing the demand? + delegate = sub; + debug.log(Level.DEBUG, "setSubscription: demand=%d", demand); + if (demand > 0) + sub.request(demand); + } + + @Override + public void request(long n) { + writeDemand.increase(n); + debug.log(Level.DEBUG, "request: n=%d", n); + Flow.Subscription sub = delegate; + if (sub != null && n > 0) { + sub.request(n); + } + } + + @Override + public void cancel() { + // TODO: no-op or error? + } + } + + /* Subscriber - writing side */ + @Override + public void onSubscribe(Flow.Subscription subscription) { + Objects.requireNonNull(subscription); + Flow.Subscription x = writeSubscription.delegate; + if (x != null) + x.cancel(); + + writeSubscription.setSubscription(subscription); + } + + @Override + public void onNext(List item) { + Objects.requireNonNull(item); + boolean decremented = writeDemand.tryDecrement(); + assert decremented : "Unexpected writeDemand: "; + debug.log(Level.DEBUG, + "sending %d buffers to SSL flow delegate", item.size()); + sslDelegate.upstreamWriter().onNext(item); + } + + @Override + public void onError(Throwable throwable) { + Objects.requireNonNull(throwable); + sslDelegate.upstreamWriter().onError(throwable); + } + + @Override + public void onComplete() { + sslDelegate.upstreamWriter().onComplete(); + } + + @Override + public String toString() { + return dbgString(); + } + + final String dbgString() { + return "SSLTube(" + tube + ")"; + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/SequentialScheduler.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/SequentialScheduler.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.common; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +import static java.util.Objects.requireNonNull; + +/** + * A scheduler of ( repeatable ) tasks that MUST be run sequentially. + * + *

This class can be used as a synchronization aid that assists a number of + * parties in running a task in a mutually exclusive fashion. + * + *

To run the task, a party invokes {@code runOrSchedule}. To permanently + * prevent the task from subsequent runs, the party invokes {@code stop}. + * + *

The parties can, but do not have to, operate in different threads. + * + *

The task can be either synchronous ( completes when its {@code run} + * method returns ), or asynchronous ( completed when its + * {@code DeferredCompleter} is explicitly completed ). + * + *

The next run of the task will not begin until the previous run has + * finished. + * + *

The task may invoke {@code runOrSchedule} itself, which may be a normal + * situation. + */ +public final class SequentialScheduler { + + /* + Since the task is fixed and known beforehand, no blocking synchronization + (locks, queues, etc.) is required. The job can be done solely using + nonblocking primitives. + + The machinery below addresses two problems: + + 1. Running the task in a sequential order (no concurrent runs): + + begin, end, begin, end... + + 2. Avoiding indefinite recursion: + + begin + end + begin + end + ... + + Problem #1 is solved with a finite state machine with 4 states: + + BEGIN, AGAIN, END, and STOP. + + Problem #2 is solved with a "state modifier" OFFLOAD. + + Parties invoke `runOrSchedule()` to signal the task must run. A party + that has invoked `runOrSchedule()` either begins the task or exploits the + party that is either beginning the task or ending it. + + The party that is trying to end the task either ends it or begins it + again. + + To avoid indefinite recursion, before re-running the task the + TryEndDeferredCompleter sets the OFFLOAD bit, signalling to its "child" + TryEndDeferredCompleter that this ("parent") TryEndDeferredCompleter is + available and the "child" must offload the task on to the "parent". Then + a race begins. Whichever invocation of TryEndDeferredCompleter.complete + manages to unset OFFLOAD bit first does not do the work. + + There is at most 1 thread that is beginning the task and at most 2 + threads that are trying to end it: "parent" and "child". In case of a + synchronous task "parent" and "child" are the same thread. + */ + + /** + * An interface to signal the completion of a {@link RestartableTask}. + * + *

The invocation of {@code complete} completes the task. The invocation + * of {@code complete} may restart the task, if an attempt has previously + * been made to run the task while it was already running. + * + * @apiNote {@code DeferredCompleter} is useful when a task is not necessary + * complete when its {@code run} method returns, but will complete at a + * later time, and maybe in different thread. This type exists for + * readability purposes at use-sites only. + */ + public static abstract class DeferredCompleter { + + /** Extensible from this (outer) class ONLY. */ + private DeferredCompleter() { } + + /** Completes the task. Must be called once, and once only. */ + public abstract void complete(); + } + + /** + * A restartable task. + */ + @FunctionalInterface + public interface RestartableTask { + + /** + * The body of the task. + * + * @param taskCompleter + * A completer that must be invoked once, and only once, + * when this task is logically finished + */ + void run(DeferredCompleter taskCompleter); + } + + /** + * A simple and self-contained task that completes once its {@code run} + * method returns. + */ + public static abstract class CompleteRestartableTask + implements RestartableTask + { + @Override + public final void run(DeferredCompleter taskCompleter) { + try { + run(); + } finally { + taskCompleter.complete(); + } + } + + /** The body of the task. */ + protected abstract void run(); + } + + /** + * A task that runs its main loop within a synchronized block to provide + * memory visibility between runs. Since the main loop can't run concurrently, + * the lock shouldn't be contended and no deadlock should ever be possible. + */ + public static final class SynchronizedRestartableTask + extends CompleteRestartableTask { + + private final Runnable mainLoop; + private final Object lock = new Object(); + + public SynchronizedRestartableTask(Runnable mainLoop) { + this.mainLoop = mainLoop; + } + + @Override + protected void run() { + synchronized(lock) { + mainLoop.run(); + } + } + } + + private static final int OFFLOAD = 1; + private static final int AGAIN = 2; + private static final int BEGIN = 4; + private static final int STOP = 8; + private static final int END = 16; + + private final AtomicInteger state = new AtomicInteger(END); + private final RestartableTask restartableTask; + private final DeferredCompleter completer; + private final SchedulableTask schedulableTask; + + /** + * An auxiliary task that starts the restartable task: + * {@code restartableTask.run(completer)}. + */ + private final class SchedulableTask implements Runnable { + @Override + public void run() { + restartableTask.run(completer); + } + } + + public SequentialScheduler(RestartableTask restartableTask) { + this.restartableTask = requireNonNull(restartableTask); + this.completer = new TryEndDeferredCompleter(); + this.schedulableTask = new SchedulableTask(); + } + + /** + * Runs or schedules the task to be run. + * + * @implSpec The recursion which is possible here must be bounded: + * + *

{@code
+     *     this.runOrSchedule()
+     *         completer.complete()
+     *             this.runOrSchedule()
+     *                 ...
+     * }
+ * + * @implNote The recursion in this implementation has the maximum + * depth of 1. + */ + public void runOrSchedule() { + runOrSchedule(schedulableTask, null); + } + + /** + * Executes or schedules the task to be executed in the provided executor. + * + *

This method can be used when potential executing from a calling + * thread is not desirable. + * + * @param executor + * An executor in which to execute the task, if the task needs + * to be executed. + * + * @apiNote The given executor can be {@code null} in which case calling + * {@code runOrSchedule(null)} is strictly equivalent to calling + * {@code runOrSchedule()}. + */ + public void runOrSchedule(Executor executor) { + runOrSchedule(schedulableTask, executor); + } + + private void runOrSchedule(SchedulableTask task, Executor executor) { + while (true) { + int s = state.get(); + if (s == END) { + if (state.compareAndSet(END, BEGIN)) { + break; + } + } else if ((s & BEGIN) != 0) { + // Tries to change the state to AGAIN, preserving OFFLOAD bit + if (state.compareAndSet(s, AGAIN | (s & OFFLOAD))) { + return; + } + } else if ((s & AGAIN) != 0 || s == STOP) { + /* In the case of AGAIN the scheduler does not provide + happens-before relationship between actions prior to + runOrSchedule() and actions that happen in task.run(). + The reason is that no volatile write is done in this case, + and the call piggybacks on the call that has actually set + AGAIN state. */ + return; + } else { + // Non-existent state, or the one that cannot be offloaded + throw new InternalError(String.valueOf(s)); + } + } + if (executor == null) { + task.run(); + } else { + executor.execute(task); + } + } + + /** The only concrete {@code DeferredCompleter} implementation. */ + private class TryEndDeferredCompleter extends DeferredCompleter { + + @Override + public void complete() { + while (true) { + int s; + while (((s = state.get()) & OFFLOAD) != 0) { + // Tries to offload ending of the task to the parent + if (state.compareAndSet(s, s & ~OFFLOAD)) { + return; + } + } + while (true) { + if ((s & OFFLOAD) != 0) { + /* OFFLOAD bit can never be observed here. Otherwise + it would mean there is another invocation of + "complete" that can run the task. */ + throw new InternalError(String.valueOf(s)); + } + if (s == BEGIN) { + if (state.compareAndSet(BEGIN, END)) { + return; + } + } else if (s == AGAIN) { + if (state.compareAndSet(AGAIN, BEGIN | OFFLOAD)) { + break; + } + } else if (s == STOP) { + return; + } else if (s == END) { + throw new IllegalStateException("Duplicate completion"); + } else { + // Non-existent state + throw new InternalError(String.valueOf(s)); + } + s = state.get(); + } + restartableTask.run(completer); + } + } + } + + /** + * Tells whether, or not, this scheduler has been permanently stopped. + * + *

Should be used from inside the task to poll the status of the + * scheduler, pretty much the same way as it is done for threads: + *

{@code
+     *     if (!Thread.currentThread().isInterrupted()) {
+     *         ...
+     *     }
+     * }
+ */ + public boolean isStopped() { + return state.get() == STOP; + } + + /** + * Stops this scheduler. Subsequent invocations of {@code runOrSchedule} + * are effectively no-ops. + * + *

If the task has already begun, this invocation will not affect it, + * unless the task itself uses {@code isStopped()} method to check the state + * of the handler. + */ + public void stop() { + state.set(STOP); + } + + /** + * Returns a new {@code SequentialScheduler} that executes the provided + * {@code mainLoop} from within a {@link SynchronizedRestartableTask}. + * + * @apiNote This is equivalent to calling + * {@code new SequentialScheduler(new SynchronizedRestartableTask(mainLoop))} + * The main loop must not perform any blocking operation. + * + * @param mainLoop The main loop of the new sequential scheduler + * @return a new {@code SequentialScheduler} that executes the provided + * {@code mainLoop} from within a {@link SynchronizedRestartableTask}. + */ + public static SequentialScheduler synchronizedScheduler(Runnable mainLoop) { + return new SequentialScheduler(new SynchronizedRestartableTask(mainLoop)); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/SubscriberWrapper.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/SubscriberWrapper.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,461 @@ +/* + * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.common; + +import java.io.Closeable; +import java.lang.System.Logger.Level; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Flow; +import java.util.concurrent.Flow.Subscriber; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +/** + * A wrapper for a Flow.Subscriber. This wrapper delivers data to the wrapped + * Subscriber which is supplied to the constructor. This class takes care of + * downstream flow control automatically and upstream flow control automatically + * by default. + *

+ * Processing is done by implementing the {@link #incoming(List, boolean)} method + * which supplies buffers from upstream. This method (or any other method) + * can then call the outgoing() method to deliver processed buffers downstream. + *

+ * Upstream error signals are delivered downstream directly. Cancellation from + * downstream is also propagated upstream immediately. + *

+ * Each SubscriberWrapper has a {@link java.util.concurrent.CompletableFuture}{@code } + * which propagates completion/errors from downstream to upstream. Normal completion + * can only occur after onComplete() is called, but errors can be propagated upwards + * at any time. + */ +public abstract class SubscriberWrapper + implements FlowTube.TubeSubscriber, Closeable, Flow.Processor,List> + // TODO: SSLTube Subscriber will never change? Does this really need to be a TS? +{ + static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag. + final System.Logger logger = + Utils.getDebugLogger(this::dbgString, DEBUG); + + public enum SchedulingAction { CONTINUE, RETURN, RESCHEDULE } + + volatile Flow.Subscription upstreamSubscription; + final SubscriptionBase downstreamSubscription; + volatile boolean upstreamCompleted; + volatile boolean downstreamCompleted; + volatile boolean completionAcknowledged; + private volatile Subscriber> downstreamSubscriber; + // processed byte to send to the downstream subscriber. + private final ConcurrentLinkedQueue> outputQ; + private final CompletableFuture cf; + private final SequentialScheduler pushScheduler; + private final AtomicReference errorRef = new AtomicReference<>(); + final AtomicLong upstreamWindow = new AtomicLong(0); + + /** + * Wraps the given downstream subscriber. For each call to {@link + * #onNext(List) } the given filter function is invoked + * and the list (if not empty) returned is passed downstream. + * + * A {@code CompletableFuture} is supplied which can be used to signal an + * error from downstream and which terminates the wrapper or which signals + * completion of downstream activity which can be propagated upstream. Error + * completion can be signaled at any time, but normal completion must not be + * signaled before onComplete() is called. + */ + public SubscriberWrapper() + { + this.outputQ = new ConcurrentLinkedQueue<>(); + this.cf = new MinimalFuture<>(); + this.pushScheduler = + SequentialScheduler.synchronizedScheduler(new DownstreamPusher()); + this.downstreamSubscription = new SubscriptionBase(pushScheduler, + this::downstreamCompletion); + } + + @Override + public final void subscribe(Subscriber> downstreamSubscriber) { + Objects.requireNonNull(downstreamSubscriber); + this.downstreamSubscriber = downstreamSubscriber; + } + + /** + * Wraps the given downstream wrapper in this. For each call to + * {@link #onNext(List) } the incoming() method is called. + * + * The {@code downstreamCF} from the downstream wrapper is linked to this + * wrappers notifier. + * + * @param downstreamWrapper downstream destination + */ + public SubscriberWrapper(Subscriber> downstreamWrapper) + { + this(); + subscribe(downstreamWrapper); + } + + /** + * Delivers data to be processed by this wrapper. Generated data to be sent + * downstream, must be provided to the {@link #outgoing(List, boolean)}} + * method. + * + * @param buffers a List of ByteBuffers. + * @param complete if true then no more data will be added to the list + */ + protected abstract void incoming(List buffers, boolean complete); + + /** + * This method is called to determine the window size to use at any time. The + * current window is supplied together with the current downstream queue size. + * {@code 0} should be returned if no change is + * required or a positive integer which will be added to the current window. + * The default implementation maintains a downstream queue size of no greater + * than 5. The method can be overridden if required. + * + * @param currentWindow the current upstream subscription window + * @param downstreamQsize the current number of buffers waiting to be sent + * downstream + * + * @return value to add to currentWindow + */ + protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) { + if (downstreamQsize > 5) { + return 0; + } + + if (currentWindow == 0) { + return 1; + } else { + return 0; + } + } + + /** + * Override this if anything needs to be done after the upstream subscriber + * has subscribed + */ + protected void onSubscribe() { + } + + /** + * Override this if anything needs to be done before checking for error + * and processing the input queue. + * @return + */ + protected SchedulingAction enterScheduling() { + return SchedulingAction.CONTINUE; + } + + protected boolean signalScheduling() { + if (downstreamCompleted || pushScheduler.isStopped()) { + return false; + } + pushScheduler.runOrSchedule(); + return true; + } + + /** + * Delivers buffers of data downstream. After incoming() + * has been called complete == true signifying completion of the upstream + * subscription, data may continue to be delivered, up to when outgoing() is + * called complete == true, after which, the downstream subscription is + * completed. + * + * It's an error to call outgoing() with complete = true if incoming() has + * not previously been called with it. + */ + public void outgoing(ByteBuffer buffer, boolean complete) { + Objects.requireNonNull(buffer); + assert !complete || !buffer.hasRemaining(); + outgoing(List.of(buffer), complete); + } + + /** + * Sometime it might be necessary to complete the downstream subscriber + * before the upstream completes. For instance, when an SSL server + * sends a notify_close. In that case we should let the outgoing + * complete before upstream us completed. + * @return true, may be overridden by subclasses. + */ + public boolean closing() { + return false; + } + + public void outgoing(List buffers, boolean complete) { + Objects.requireNonNull(buffers); + if (complete) { + assert Utils.remaining(buffers) == 0; + boolean closing = closing(); + logger.log(Level.DEBUG, + "completionAcknowledged upstreamCompleted:%s, downstreamCompleted:%s, closing:%s", + upstreamCompleted, downstreamCompleted, closing); + if (!upstreamCompleted && !closing) + throw new IllegalStateException("upstream not completed"); + completionAcknowledged = true; + } else { + logger.log(Level.DEBUG, () -> "Adding " + + Utils.remaining(buffers) + + " to outputQ queue"); + outputQ.add(buffers); + } + logger.log(Level.DEBUG, () -> "pushScheduler " + + (pushScheduler.isStopped() ? " is stopped!" : " is alive")); + pushScheduler.runOrSchedule(); + } + + /** + * Returns a CompletableFuture which completes when this wrapper completes. + * Normal completion happens with the following steps (in order): + * 1. onComplete() is called + * 2. incoming() called with complete = true + * 3. outgoing() may continue to be called normally + * 4. outgoing called with complete = true + * 5. downstream subscriber is called onComplete() + * + * If the subscription is canceled or onComplete() is invoked the + * CompletableFuture completes exceptionally. Exceptional completion + * also occurs if downstreamCF completes exceptionally. + */ + public CompletableFuture completion() { + return cf; + } + + /** + * Invoked whenever it 'may' be possible to push buffers downstream. + */ + class DownstreamPusher implements Runnable { + @Override + public void run() { + try { + run1(); + } catch (Throwable t) { + errorCommon(t); + } + } + + private void run1() { + if (downstreamCompleted) { + logger.log(Level.DEBUG, "DownstreamPusher: downstream is already completed"); + return; + } + switch (enterScheduling()) { + case CONTINUE: break; + case RESCHEDULE: pushScheduler.runOrSchedule(); return; + case RETURN: return; + default: + errorRef.compareAndSet(null, + new InternalError("unknown scheduling command")); + break; + } + // If there was an error, send it downstream. + Throwable error = errorRef.get(); + if (error != null) { + synchronized(this) { + if (downstreamCompleted) return; + downstreamCompleted = true; + } + logger.log(Level.DEBUG, + () -> "DownstreamPusher: forwarding error downstream: " + error); + pushScheduler.stop(); + outputQ.clear(); + downstreamSubscriber.onError(error); + return; + } + + // OK - no error, let's proceed + if (!outputQ.isEmpty()) { + logger.log(Level.DEBUG, + "DownstreamPusher: queue not empty, downstreamSubscription: %s", + downstreamSubscription); + } else { + logger.log(Level.DEBUG, + "DownstreamPusher: queue empty, downstreamSubscription: %s", + downstreamSubscription); + } + + final boolean dbgOn = logger.isLoggable(Level.DEBUG); + while (!outputQ.isEmpty() && downstreamSubscription.tryDecrement()) { + List b = outputQ.poll(); + if (dbgOn) logger.log(Level.DEBUG, + "DownstreamPusher: Pushing " + + Utils.remaining(b) + + " bytes downstream"); + downstreamSubscriber.onNext(b); + } + upstreamWindowUpdate(); + checkCompletion(); + } + } + + void upstreamWindowUpdate() { + long downstreamQueueSize = outputQ.size(); + long n = upstreamWindowUpdate(upstreamWindow.get(), downstreamQueueSize); + if (n > 0) + upstreamRequest(n); + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + if (upstreamSubscription != null) { + throw new IllegalStateException("Single shot publisher"); + } + this.upstreamSubscription = subscription; + upstreamRequest(upstreamWindowUpdate(0, 0)); + logger.log(Level.DEBUG, + "calling downstreamSubscriber::onSubscribe on %s", + downstreamSubscriber); + downstreamSubscriber.onSubscribe(downstreamSubscription); + onSubscribe(); + } + + @Override + public void onNext(List item) { + logger.log(Level.DEBUG, "onNext"); + long prev = upstreamWindow.getAndDecrement(); + if (prev <= 0) + throw new IllegalStateException("invalid onNext call"); + incomingCaller(item, false); + upstreamWindowUpdate(); + } + + private void upstreamRequest(long n) { + logger.log(Level.DEBUG, "requesting %d", n); + upstreamWindow.getAndAdd(n); + upstreamSubscription.request(n); + } + + protected void requestMore() { + if (upstreamWindow.get() == 0) { + upstreamRequest(1); + } + } + + public long upstreamWindow() { + return upstreamWindow.get(); + } + + @Override + public void onError(Throwable throwable) { + logger.log(Level.DEBUG, () -> "onError: " + throwable); + errorCommon(Objects.requireNonNull(throwable)); + } + + protected boolean errorCommon(Throwable throwable) { + assert throwable != null || + (throwable = new AssertionError("null throwable")) != null; + if (errorRef.compareAndSet(null, throwable)) { + logger.log(Level.DEBUG, "error", throwable); + pushScheduler.runOrSchedule(); + upstreamCompleted = true; + cf.completeExceptionally(throwable); + return true; + } + return false; + } + + @Override + public void close() { + errorCommon(new RuntimeException("wrapper closed")); + } + + private void incomingCaller(List l, boolean complete) { + try { + incoming(l, complete); + } catch(Throwable t) { + errorCommon(t); + } + } + + @Override + public void onComplete() { + logger.log(Level.DEBUG, () -> "upstream completed: " + toString()); + upstreamCompleted = true; + incomingCaller(Utils.EMPTY_BB_LIST, true); + // pushScheduler will call checkCompletion() + pushScheduler.runOrSchedule(); + } + + /** Adds the given data to the input queue. */ + public void addData(ByteBuffer l) { + if (upstreamSubscription == null) { + throw new IllegalStateException("can't add data before upstream subscriber subscribes"); + } + incomingCaller(List.of(l), false); + } + + void checkCompletion() { + if (downstreamCompleted || !upstreamCompleted) { + return; + } + if (!outputQ.isEmpty()) { + return; + } + if (errorRef.get() != null) { + pushScheduler.runOrSchedule(); + return; + } + if (completionAcknowledged) { + logger.log(Level.DEBUG, "calling downstreamSubscriber.onComplete()"); + downstreamSubscriber.onComplete(); + // Fix me subscriber.onComplete.run(); + downstreamCompleted = true; + cf.complete(null); + } + } + + // called from the downstream Subscription.cancel() + void downstreamCompletion() { + upstreamSubscription.cancel(); + cf.complete(null); + } + + public void resetDownstreamDemand() { + downstreamSubscription.demand.reset(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("SubscriberWrapper:") + .append(" upstreamCompleted: ").append(Boolean.toString(upstreamCompleted)) + .append(" upstreamWindow: ").append(upstreamWindow.toString()) + .append(" downstreamCompleted: ").append(Boolean.toString(downstreamCompleted)) + .append(" completionAcknowledged: ").append(Boolean.toString(completionAcknowledged)) + .append(" outputQ size: ").append(Integer.toString(outputQ.size())) + //.append(" outputQ: ").append(outputQ.toString()) + .append(" cf: ").append(cf.toString()) + .append(" downstreamSubscription: ").append(downstreamSubscription.toString()); + + return sb.toString(); + } + + public String dbgString() { + return "SubscriberWrapper"; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/SubscriptionBase.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/SubscriptionBase.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.common; + +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Maintains subscription counter and provides primitives for + * - accessing window + * - reducing window when delivering items externally + * - resume delivery when window was zero previously + * + * @author mimcmah + */ +public class SubscriptionBase implements Flow.Subscription { + + final Demand demand = new Demand(); + + final SequentialScheduler scheduler; // when window was zero and is opened, run this + final Runnable cancelAction; // when subscription cancelled, run this + final AtomicBoolean cancelled; + + public SubscriptionBase(SequentialScheduler scheduler, Runnable cancelAction) { + this.scheduler = scheduler; + this.cancelAction = cancelAction; + this.cancelled = new AtomicBoolean(false); + } + + @Override + public void request(long n) { + if (demand.increase(n)) + scheduler.runOrSchedule(); + } + + + + @Override + public synchronized String toString() { + return "SubscriptionBase: window = " + demand.get() + + " cancelled = " + cancelled.toString(); + } + + /** + * Returns true if the window was reduced by 1. In that case + * items must be supplied to subscribers and the scheduler run + * externally. If the window could not be reduced by 1, then false + * is returned and the scheduler will run later when the window is updated. + */ + public boolean tryDecrement() { + return demand.tryDecrement(); + } + + public long window() { + return demand.get(); + } + + @Override + public void cancel() { + if (cancelled.getAndSet(true)) + return; + scheduler.stop(); + cancelAction.run(); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,755 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.common; + +import java.net.http.HttpHeaders; +import sun.net.NetProperties; +import sun.net.util.IPAddressUtil; +import sun.net.www.HeaderParser; + +import javax.net.ssl.SSLParameters; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.io.UnsupportedEncodingException; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URLPermission; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.joining; + +/** + * Miscellaneous utilities + */ +public final class Utils { + + public static final boolean ASSERTIONSENABLED; + static { + boolean enabled = false; + assert enabled = true; + ASSERTIONSENABLED = enabled; + } +// public static final boolean TESTING; +// static { +// if (ASSERTIONSENABLED) { +// PrivilegedAction action = () -> System.getProperty("test.src"); +// TESTING = AccessController.doPrivileged(action) != null; +// } else TESTING = false; +// } + public static final boolean DEBUG = // Revisit: temporary dev flag. + getBooleanProperty(DebugLogger.HTTP_NAME, false); + public static final boolean DEBUG_HPACK = // Revisit: temporary dev flag. + getBooleanProperty(DebugLogger.HPACK_NAME, false); + public static final boolean TESTING = DEBUG; + + /** + * Allocated buffer size. Must never be higher than 16K. But can be lower + * if smaller allocation units preferred. HTTP/2 mandates that all + * implementations support frame payloads of at least 16K. + */ + private static final int DEFAULT_BUFSIZE = 16 * 1024; + + public static final int BUFSIZE = getIntegerNetProperty( + "jdk.httpclient.bufsize", DEFAULT_BUFSIZE + ); + + private static final Set DISALLOWED_HEADERS_SET; + static { + // A case insensitive TreeSet of strings. + TreeSet treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + treeSet.addAll(Set.of("connection", "content-length", + "date", "expect", "from", "host", "origin", + "referer", "upgrade", + "via", "warning")); + DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet); + } + + public static final Predicate + ALLOWED_HEADERS = header -> !DISALLOWED_HEADERS_SET.contains(header); + + private static final Predicate IS_PROXY_HEADER = (k) -> + k != null && k.length() > 6 && "proxy-".equalsIgnoreCase(k.substring(0,6)); + private static final Predicate NO_PROXY_HEADER = + IS_PROXY_HEADER.negate(); + private static final Predicate ALL_HEADERS = (s) -> true; + + private static final Set PROXY_AUTH_DISABLED_SCHEMES; + private static final Set PROXY_AUTH_TUNNEL_DISABLED_SCHEMES; + static { + String proxyAuthDisabled = + getNetProperty("jdk.http.auth.proxying.disabledSchemes"); + String proxyAuthTunnelDisabled = + getNetProperty("jdk.http.auth.tunneling.disabledSchemes"); + PROXY_AUTH_DISABLED_SCHEMES = + proxyAuthDisabled == null ? Set.of() : + Stream.of(proxyAuthDisabled.split(",")) + .map(String::trim) + .filter((s) -> !s.isEmpty()) + .collect(Collectors.toUnmodifiableSet()); + PROXY_AUTH_TUNNEL_DISABLED_SCHEMES = + proxyAuthTunnelDisabled == null ? Set.of() : + Stream.of(proxyAuthTunnelDisabled.split(",")) + .map(String::trim) + .filter((s) -> !s.isEmpty()) + .collect(Collectors.toUnmodifiableSet()); + } + + private static final String WSPACES = " \t\r\n"; + private static final boolean isAllowedForProxy(String name, + List value, + Set disabledSchemes, + Predicate allowedKeys) { + if (!allowedKeys.test(name)) return false; + if (disabledSchemes.isEmpty()) return true; + if (name.equalsIgnoreCase("proxy-authorization")) { + if (value.isEmpty()) return false; + for (String scheme : disabledSchemes) { + int slen = scheme.length(); + for (String v : value) { + int vlen = v.length(); + if (vlen == slen) { + if (v.equalsIgnoreCase(scheme)) { + return false; + } + } else if (vlen > slen) { + if (v.substring(0,slen).equalsIgnoreCase(scheme)) { + int c = v.codePointAt(slen); + if (WSPACES.indexOf(c) > -1 + || Character.isSpaceChar(c) + || Character.isWhitespace(c)) { + return false; + } + } + } + } + } + } + return true; + } + + public static final BiPredicate> PROXY_TUNNEL_FILTER = + (s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_TUNNEL_DISABLED_SCHEMES, + IS_PROXY_HEADER); + public static final BiPredicate> PROXY_FILTER = + (s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_DISABLED_SCHEMES, + ALL_HEADERS); + public static final BiPredicate> NO_PROXY_HEADERS_FILTER = + (n,v) -> Utils.NO_PROXY_HEADER.test(n); + + + public static boolean proxyHasDisabledSchemes(boolean tunnel) { + return tunnel ? ! PROXY_AUTH_TUNNEL_DISABLED_SCHEMES.isEmpty() + : ! PROXY_AUTH_DISABLED_SCHEMES.isEmpty(); + } + + public static ByteBuffer getBuffer() { + return ByteBuffer.allocate(BUFSIZE); + } + + public static Throwable getCompletionCause(Throwable x) { + if (!(x instanceof CompletionException) + && !(x instanceof ExecutionException)) return x; + final Throwable cause = x.getCause(); + if (cause == null) { + throw new InternalError("Unexpected null cause", x); + } + return cause; + } + + public static IOException getIOException(Throwable t) { + if (t instanceof IOException) { + return (IOException) t; + } + Throwable cause = t.getCause(); + if (cause != null) { + return getIOException(cause); + } + return new IOException(t); + } + + private Utils() { } + + /** + * Returns the security permissions required to connect to the proxy, or + * {@code null} if none is required or applicable. + */ + public static URLPermission permissionForProxy(InetSocketAddress proxyAddress) { + if (proxyAddress == null) + return null; + + StringBuilder sb = new StringBuilder(); + sb.append("socket://") + .append(proxyAddress.getHostString()).append(":") + .append(proxyAddress.getPort()); + String urlString = sb.toString(); + return new URLPermission(urlString, "CONNECT"); + } + + /** + * Returns the security permission required for the given details. + */ + public static URLPermission permissionForServer(URI uri, + String method, + Stream headers) { + String urlString = new StringBuilder() + .append(uri.getScheme()).append("://") + .append(uri.getAuthority()) + .append(uri.getPath()).toString(); + + StringBuilder actionStringBuilder = new StringBuilder(method); + String collected = headers.collect(joining(",")); + if (!collected.isEmpty()) { + actionStringBuilder.append(":").append(collected); + } + return new URLPermission(urlString, actionStringBuilder.toString()); + } + + + // ABNF primitives defined in RFC 7230 + private static final boolean[] tchar = new boolean[256]; + private static final boolean[] fieldvchar = new boolean[256]; + + static { + char[] allowedTokenChars = + ("!#$%&'*+-.^_`|~0123456789" + + "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray(); + for (char c : allowedTokenChars) { + tchar[c] = true; + } + for (char c = 0x21; c < 0xFF; c++) { + fieldvchar[c] = true; + } + fieldvchar[0x7F] = false; // a little hole (DEL) in the range + } + + /* + * Validates a RFC 7230 field-name. + */ + public static boolean isValidName(String token) { + for (int i = 0; i < token.length(); i++) { + char c = token.charAt(i); + if (c > 255 || !tchar[c]) { + return false; + } + } + return !token.isEmpty(); + } + + /** + * If the address was created with a domain name, then return + * the domain name string. If created with a literal IP address + * then return null. We do this to avoid doing a reverse lookup + * Used to populate the TLS SNI parameter. So, SNI is only set + * when a domain name was supplied. + */ + public static String getServerName(InetSocketAddress addr) { + String host = addr.getHostString(); + if (IPAddressUtil.textToNumericFormatV4(host) != null) + return null; + if (IPAddressUtil.textToNumericFormatV6(host) != null) + return null; + return host; + } + + /* + * Validates a RFC 7230 field-value. + * + * "Obsolete line folding" rule + * + * obs-fold = CRLF 1*( SP / HTAB ) + * + * is not permitted! + */ + public static boolean isValidValue(String token) { + boolean accepted = true; + for (int i = 0; i < token.length(); i++) { + char c = token.charAt(i); + if (c > 255) { + return false; + } + if (accepted) { + if (c == ' ' || c == '\t') { + accepted = false; + } else if (!fieldvchar[c]) { + return false; // forbidden byte + } + } else { + if (c != ' ' && c != '\t') { + if (fieldvchar[c]) { + accepted = true; + } else { + return false; // forbidden byte + } + } + } + } + return accepted; + } + + public static int getIntegerNetProperty(String name, int defaultValue) { + return AccessController.doPrivileged((PrivilegedAction) () -> + NetProperties.getInteger(name, defaultValue)); + } + + static String getNetProperty(String name) { + return AccessController.doPrivileged((PrivilegedAction) () -> + NetProperties.get(name)); + } + + static boolean getBooleanProperty(String name, boolean def) { + return AccessController.doPrivileged((PrivilegedAction) () -> + Boolean.parseBoolean(System.getProperty(name, String.valueOf(def)))); + } + + public static SSLParameters copySSLParameters(SSLParameters p) { + SSLParameters p1 = new SSLParameters(); + p1.setAlgorithmConstraints(p.getAlgorithmConstraints()); + p1.setCipherSuites(p.getCipherSuites()); + // JDK 8 EXCL START + p1.setEnableRetransmissions(p.getEnableRetransmissions()); + p1.setMaximumPacketSize(p.getMaximumPacketSize()); + // JDK 8 EXCL END + p1.setEndpointIdentificationAlgorithm(p.getEndpointIdentificationAlgorithm()); + p1.setNeedClientAuth(p.getNeedClientAuth()); + String[] protocols = p.getProtocols(); + if (protocols != null) { + p1.setProtocols(protocols.clone()); + } + p1.setSNIMatchers(p.getSNIMatchers()); + p1.setServerNames(p.getServerNames()); + p1.setUseCipherSuitesOrder(p.getUseCipherSuitesOrder()); + p1.setWantClientAuth(p.getWantClientAuth()); + return p1; + } + + /** + * Set limit to position, and position to mark. + */ + public static void flipToMark(ByteBuffer buffer, int mark) { + buffer.limit(buffer.position()); + buffer.position(mark); + } + + public static String stackTrace(Throwable t) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + String s = null; + try { + PrintStream p = new PrintStream(bos, true, "US-ASCII"); + t.printStackTrace(p); + s = bos.toString("US-ASCII"); + } catch (UnsupportedEncodingException ex) { + throw new InternalError(ex); // Can't happen + } + return s; + } + + /** + * Copies as much of src to dst as possible. + * Return number of bytes copied + */ + public static int copy(ByteBuffer src, ByteBuffer dst) { + int srcLen = src.remaining(); + int dstLen = dst.remaining(); + if (srcLen > dstLen) { + int diff = srcLen - dstLen; + int limit = src.limit(); + src.limit(limit - diff); + dst.put(src); + src.limit(limit); + } else { + dst.put(src); + } + return srcLen - src.remaining(); + } + + /** Threshold beyond which data is no longer copied into the current + * buffer, if that buffer has enough unused space. */ + private static final int COPY_THRESHOLD = 8192; + + /** + * Adds the data from buffersToAdd to currentList. Either 1) appends the + * data from a particular buffer to the last buffer in the list ( if + * there is enough unused space ), or 2) adds it to the list. + * + * @return the number of bytes added + */ + public static long accumulateBuffers(List currentList, + List buffersToAdd) { + long accumulatedBytes = 0; + for (ByteBuffer bufferToAdd : buffersToAdd) { + int remaining = bufferToAdd.remaining(); + if (remaining <= 0) + continue; + int listSize = currentList.size(); + if (listSize == 0) { + currentList.add(bufferToAdd); + accumulatedBytes = remaining; + continue; + } + + ByteBuffer lastBuffer = currentList.get(listSize - 1); + int freeSpace = lastBuffer.capacity() - lastBuffer.limit(); + if (remaining <= COPY_THRESHOLD && freeSpace >= remaining) { + // append the new data to the unused space in the last buffer + int position = lastBuffer.position(); + int limit = lastBuffer.limit(); + lastBuffer.position(limit); + lastBuffer.limit(limit + remaining); + lastBuffer.put(bufferToAdd); + lastBuffer.position(position); + } else { + currentList.add(bufferToAdd); + } + accumulatedBytes += remaining; + } + return accumulatedBytes; + } + + public static ByteBuffer copy(ByteBuffer src) { + ByteBuffer dst = ByteBuffer.allocate(src.remaining()); + dst.put(src); + dst.flip(); + return dst; + } + + public static String dump(Object... objects) { + return Arrays.toString(objects); + } + + public static String stringOf(Collection source) { + // We don't know anything about toString implementation of this + // collection, so let's create an array + return Arrays.toString(source.toArray()); + } + + public static long remaining(ByteBuffer[] bufs) { + long remain = 0; + for (ByteBuffer buf : bufs) { + remain += buf.remaining(); + } + return remain; + } + + public static boolean hasRemaining(List bufs) { + synchronized (bufs) { + for (ByteBuffer buf : bufs) { + if (buf.hasRemaining()) + return true; + } + } + return false; + } + + public static long remaining(List bufs) { + long remain = 0; + synchronized (bufs) { + for (ByteBuffer buf : bufs) { + remain += buf.remaining(); + } + } + return remain; + } + + public static int remaining(List bufs, int max) { + long remain = 0; + synchronized (bufs) { + for (ByteBuffer buf : bufs) { + remain += buf.remaining(); + if (remain > max) { + throw new IllegalArgumentException("too many bytes"); + } + } + } + return (int) remain; + } + + public static long remaining(ByteBufferReference[] refs) { + long remain = 0; + for (ByteBufferReference ref : refs) { + remain += ref.get().remaining(); + } + return remain; + } + + public static int remaining(ByteBufferReference[] refs, int max) { + long remain = 0; + for (ByteBufferReference ref : refs) { + remain += ref.get().remaining(); + if (remain > max) { + throw new IllegalArgumentException("too many bytes"); + } + } + return (int) remain; + } + + public static int remaining(ByteBuffer[] refs, int max) { + long remain = 0; + for (ByteBuffer b : refs) { + remain += b.remaining(); + if (remain > max) { + throw new IllegalArgumentException("too many bytes"); + } + } + return (int) remain; + } + + public static void close(Closeable... closeables) { + for (Closeable c : closeables) { + try { + c.close(); + } catch (IOException ignored) { } + } + } + + // Put all these static 'empty' singletons here + public static final ByteBuffer EMPTY_BYTEBUFFER = ByteBuffer.allocate(0); + public static final ByteBuffer[] EMPTY_BB_ARRAY = new ByteBuffer[0]; + public static final List EMPTY_BB_LIST = List.of(); + public static final ByteBufferReference[] EMPTY_BBR_ARRAY = new ByteBufferReference[0]; + + /** + * Returns a slice of size {@code amount} from the given buffer. If the + * buffer contains more data than {@code amount}, then the slice's capacity + * ( and, but not just, its limit ) is set to {@code amount}. If the buffer + * does not contain more data than {@code amount}, then the slice's capacity + * will be the same as the given buffer's capacity. + */ + public static ByteBuffer sliceWithLimitedCapacity(ByteBuffer buffer, int amount) { + final int index = buffer.position() + amount; + final int limit = buffer.limit(); + if (index != limit) { + // additional data in the buffer + buffer.limit(index); // ensures that the slice does not go beyond + } else { + // no additional data in the buffer + buffer.limit(buffer.capacity()); // allows the slice full capacity + } + + ByteBuffer newb = buffer.slice(); + buffer.position(index); + buffer.limit(limit); // restore the original buffer's limit + newb.limit(amount); // slices limit to amount (capacity may be greater) + return newb; + } + + /** + * Get the Charset from the Content-encoding header. Defaults to + * UTF_8 + */ + public static Charset charsetFrom(HttpHeaders headers) { + String type = headers.firstValue("Content-type") + .orElse("text/html; charset=utf-8"); + int i = type.indexOf(";"); + if (i >= 0) type = type.substring(i+1); + try { + HeaderParser parser = new HeaderParser(type); + String value = parser.findValue("charset"); + if (value == null) return StandardCharsets.UTF_8; + return Charset.forName(value); + } catch (Throwable x) { + Log.logTrace("Can't find charset in \"{0}\" ({1})", type, x); + return StandardCharsets.UTF_8; + } + } + + public static UncheckedIOException unchecked(IOException e) { + return new UncheckedIOException(e); + } + + /** + * Get a logger for debug HTTP traces. + * + * The logger should only be used with levels whose severity is + * {@code <= DEBUG}. By default, this logger will forward all messages + * logged to an internal logger named "jdk.internal.httpclient.debug". + * In addition, if the property -Djdk.internal.httpclient.debug=true is set, + * it will print the messages on stderr. + * The logger will add some decoration to the printed message, in the form of + * {@code :[] [] : } + * + * @param dbgTag A lambda that returns a string that identifies the caller + * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))") + * + * @return A logger for HTTP internal debug traces + */ + public static Logger getDebugLogger(Supplier dbgTag) { + return getDebugLogger(dbgTag, DEBUG); + } + + /** + * Get a logger for debug HTTP traces.The logger should only be used + * with levels whose severity is {@code <= DEBUG}. + * + * By default, this logger will forward all messages logged to an internal + * logger named "jdk.internal.httpclient.debug". + * In addition, if the message severity level is >= to + * the provided {@code errLevel} it will print the messages on stderr. + * The logger will add some decoration to the printed message, in the form of + * {@code :[] [] : } + * + * @apiNote To obtain a logger that will always print things on stderr in + * addition to forwarding to the internal logger, use + * {@code getDebugLogger(this::dbgTag, Level.ALL);}. + * This is also equivalent to calling + * {@code getDebugLogger(this::dbgTag, true);}. + * To obtain a logger that will only forward to the internal logger, + * use {@code getDebugLogger(this::dbgTag, Level.OFF);}. + * This is also equivalent to calling + * {@code getDebugLogger(this::dbgTag, false);}. + * + * @param dbgTag A lambda that returns a string that identifies the caller + * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))") + * @param errLevel The level above which messages will be also printed on + * stderr (in addition to be forwarded to the internal logger). + * + * @return A logger for HTTP internal debug traces + */ + static Logger getDebugLogger(Supplier dbgTag, Level errLevel) { + return DebugLogger.createHttpLogger(dbgTag, Level.OFF, errLevel); + } + + /** + * Get a logger for debug HTTP traces.The logger should only be used + * with levels whose severity is {@code <= DEBUG}. + * + * By default, this logger will forward all messages logged to an internal + * logger named "jdk.internal.httpclient.debug". + * In addition, the provided boolean {@code on==true}, it will print the + * messages on stderr. + * The logger will add some decoration to the printed message, in the form of + * {@code :[] [] : } + * + * @apiNote To obtain a logger that will always print things on stderr in + * addition to forwarding to the internal logger, use + * {@code getDebugLogger(this::dbgTag, true);}. + * This is also equivalent to calling + * {@code getDebugLogger(this::dbgTag, Level.ALL);}. + * To obtain a logger that will only forward to the internal logger, + * use {@code getDebugLogger(this::dbgTag, false);}. + * This is also equivalent to calling + * {@code getDebugLogger(this::dbgTag, Level.OFF);}. + * + * @param dbgTag A lambda that returns a string that identifies the caller + * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))") + * @param on Whether messages should also be printed on + * stderr (in addition to be forwarded to the internal logger). + * + * @return A logger for HTTP internal debug traces + */ + public static Logger getDebugLogger(Supplier dbgTag, boolean on) { + Level errLevel = on ? Level.ALL : Level.OFF; + return getDebugLogger(dbgTag, errLevel); + } + + /** + * Get a logger for debug HPACK traces.The logger should only be used + * with levels whose severity is {@code <= DEBUG}. + * + * By default, this logger will forward all messages logged to an internal + * logger named "jdk.internal.httpclient.hpack.debug". + * In addition, if the message severity level is >= to + * the provided {@code outLevel} it will print the messages on stdout. + * The logger will add some decoration to the printed message, in the form of + * {@code :[] [] : } + * + * @apiNote To obtain a logger that will always print things on stdout in + * addition to forwarding to the internal logger, use + * {@code getHpackLogger(this::dbgTag, Level.ALL);}. + * This is also equivalent to calling + * {@code getHpackLogger(this::dbgTag, true);}. + * To obtain a logger that will only forward to the internal logger, + * use {@code getHpackLogger(this::dbgTag, Level.OFF);}. + * This is also equivalent to calling + * {@code getHpackLogger(this::dbgTag, false);}. + * + * @param dbgTag A lambda that returns a string that identifies the caller + * (e.g: "Http2Connection(SocketTube(3))/hpack.Decoder(3)") + * @param outLevel The level above which messages will be also printed on + * stdout (in addition to be forwarded to the internal logger). + * + * @return A logger for HPACK internal debug traces + */ + public static Logger getHpackLogger(Supplier dbgTag, Level outLevel) { + Level errLevel = Level.OFF; + return DebugLogger.createHpackLogger(dbgTag, outLevel, errLevel); + } + + /** + * Get a logger for debug HPACK traces.The logger should only be used + * with levels whose severity is {@code <= DEBUG}. + * + * By default, this logger will forward all messages logged to an internal + * logger named "jdk.internal.httpclient.hpack.debug". + * In addition, the provided boolean {@code on==true}, it will print the + * messages on stdout. + * The logger will add some decoration to the printed message, in the form of + * {@code :[] [] : } + * + * @apiNote To obtain a logger that will always print things on stdout in + * addition to forwarding to the internal logger, use + * {@code getHpackLogger(this::dbgTag, true);}. + * This is also equivalent to calling + * {@code getHpackLogger(this::dbgTag, Level.ALL);}. + * To obtain a logger that will only forward to the internal logger, + * use {@code getHpackLogger(this::dbgTag, false);}. + * This is also equivalent to calling + * {@code getHpackLogger(this::dbgTag, Level.OFF);}. + * + * @param dbgTag A lambda that returns a string that identifies the caller + * (e.g: "Http2Connection(SocketTube(3))/hpack.Decoder(3)") + * @param on Whether messages should also be printed on + * stdout (in addition to be forwarded to the internal logger). + * + * @return A logger for HPACK internal debug traces + */ + public static Logger getHpackLogger(Supplier dbgTag, boolean on) { + Level outLevel = on ? Level.ALL : Level.OFF; + return getHpackLogger(dbgTag, outLevel); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/ContinuationFrame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/ContinuationFrame.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +import java.nio.ByteBuffer; +import java.util.List; + +public class ContinuationFrame extends HeaderFrame { + + public static final int TYPE = 0x9; + + public ContinuationFrame(int streamid, int flags, List headerBlocks) { + super(streamid, flags, headerBlocks); + } + + public ContinuationFrame(int streamid, ByteBuffer headersBlock) { + this(streamid, 0, List.of(headersBlock)); + } + + @Override + public int type() { + return TYPE; + } + + @Override + int length() { + return headerLength; + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/DataFrame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/DataFrame.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +import jdk.internal.net.http.common.Utils; + +import java.nio.ByteBuffer; +import java.util.List; + +public class DataFrame extends Http2Frame { + + public static final int TYPE = 0x0; + + // Flags + public static final int END_STREAM = 0x1; + public static final int PADDED = 0x8; + + private int padLength; + private final List data; + private final int dataLength; + + public DataFrame(int streamid, int flags, ByteBuffer data) { + this(streamid, flags, List.of(data)); + } + + public DataFrame(int streamid, int flags, List data) { + super(streamid, flags); + this.data = data; + this.dataLength = Utils.remaining(data, Integer.MAX_VALUE); + } + + public DataFrame(int streamid, int flags, List data, int padLength) { + this(streamid, flags, data); + if (padLength > 0) { + setPadLength(padLength); + } + } + + @Override + public int type() { + return TYPE; + } + + @Override + int length() { + return dataLength + (((flags & PADDED) != 0) ? (padLength + 1) : 0); + } + + @Override + public String flagAsString(int flag) { + switch (flag) { + case END_STREAM: + return "END_STREAM"; + case PADDED: + return "PADDED"; + } + return super.flagAsString(flag); + } + + public List getData() { + return data; + } + + public int getDataLength() { + return dataLength; + } + + int getPadLength() { + return padLength; + } + + public void setPadLength(int padLength) { + this.padLength = padLength; + flags |= PADDED; + } + + public int payloadLength() { + // RFC 7540 6.1: + // The entire DATA frame payload is included in flow control, + // including the Pad Length and Padding fields if present + if ((flags & PADDED) != 0) { + return dataLength + (1 + padLength); + } else { + return dataLength; + } + } + + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/ErrorFrame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/ErrorFrame.java Wed Feb 07 21:45:37 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.internal.net.http.frame; + +public abstract class ErrorFrame extends Http2Frame { + + // error codes + public static final int NO_ERROR = 0x0; + public static final int PROTOCOL_ERROR = 0x1; + public static final int INTERNAL_ERROR = 0x2; + public static final int FLOW_CONTROL_ERROR = 0x3; + public static final int SETTINGS_TIMEOUT = 0x4; + public static final int STREAM_CLOSED = 0x5; + public static final int FRAME_SIZE_ERROR = 0x6; + public static final int REFUSED_STREAM = 0x7; + public static final int CANCEL = 0x8; + public static final int COMPRESSION_ERROR = 0x9; + public static final int CONNECT_ERROR = 0xa; + public static final int ENHANCE_YOUR_CALM = 0xb; + public static final int INADEQUATE_SECURITY = 0xc; + public static final int HTTP_1_1_REQUIRED = 0xd; + static final int LAST_ERROR = 0xd; + + static final String[] errorStrings = { + "Not an error", + "Protocol error", + "Internal error", + "Flow control error", + "Settings timeout", + "Stream is closed", + "Frame size error", + "Stream not processed", + "Stream cancelled", + "Compression state not updated", + "TCP Connection error on CONNECT", + "Processing capacity exceeded", + "Negotiated TLS parameters not acceptable", + "Use HTTP/1.1 for request" + }; + + public static String stringForCode(int code) { + if (code < 0) { + throw new IllegalArgumentException(); + } + + if (code > LAST_ERROR) { + return "Error: " + Integer.toString(code); + } else { + return errorStrings[code]; + } + } + + int errorCode; + + public ErrorFrame(int streamid, int flags, int errorCode) { + super(streamid, flags); + this.errorCode = errorCode; + } + + @Override + public String toString() { + return super.toString() + " Error: " + stringForCode(errorCode); + } + + public int getErrorCode() { + return this.errorCode; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesDecoder.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesDecoder.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,545 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +import java.io.IOException; +import java.lang.System.Logger.Level; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Utils; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Frames Decoder + *

+ * collect buffers until frame decoding is possible, + * all decoded frames are passed to the FrameProcessor callback in order of decoding. + * + * It's a stateful class due to the fact that FramesDecoder stores buffers inside. + * Should be allocated only the single instance per connection. + */ +public class FramesDecoder { + + static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag. + static final System.Logger DEBUG_LOGGER = + Utils.getDebugLogger("FramesDecoder"::toString, DEBUG); + + @FunctionalInterface + public interface FrameProcessor { + void processFrame(Http2Frame frame) throws IOException; + } + + private final FrameProcessor frameProcessor; + private final int maxFrameSize; + + private ByteBuffer currentBuffer; // current buffer either null or hasRemaining + + private final ArrayDeque tailBuffers = new ArrayDeque<>(); + private int tailSize = 0; + + private boolean slicedToDataFrame = false; + + private final List prepareToRelease = new ArrayList<>(); + + // if true - Frame Header was parsed (9 bytes consumed) and subsequent fields have meaning + // otherwise - stopped at frames boundary + private boolean frameHeaderParsed = false; + private int frameLength; + private int frameType; + private int frameFlags; + private int frameStreamid; + private boolean closed; + + /** + * Creates Frame Decoder + * + * @param frameProcessor - callback for decoded frames + */ + public FramesDecoder(FrameProcessor frameProcessor) { + this(frameProcessor, 16 * 1024); + } + + /** + * Creates Frame Decoder + * @param frameProcessor - callback for decoded frames + * @param maxFrameSize - maxFrameSize accepted by this decoder + */ + public FramesDecoder(FrameProcessor frameProcessor, int maxFrameSize) { + this.frameProcessor = frameProcessor; + this.maxFrameSize = Math.min(Math.max(16 * 1024, maxFrameSize), 16 * 1024 * 1024 - 1); + } + + /** Threshold beyond which data is no longer copied into the current buffer, + * if that buffer has enough unused space. */ + private static final int COPY_THRESHOLD = 8192; + + /** + * Adds the data from the given buffer, and performs frame decoding if + * possible. Either 1) appends the data from the given buffer to the + * current buffer ( if there is enough unused space ), or 2) adds it to the + * next buffer in the queue. + * + * If there is enough data to perform frame decoding then, all buffers are + * decoded and the FrameProcessor is invoked. + */ + public void decode(ByteBuffer inBoundBuffer) throws IOException { + if (closed) { + DEBUG_LOGGER.log(Level.DEBUG, "closed: ignoring buffer (%s bytes)", + inBoundBuffer.remaining()); + inBoundBuffer.position(inBoundBuffer.limit()); + return; + } + int remaining = inBoundBuffer.remaining(); + DEBUG_LOGGER.log(Level.DEBUG, "decodes: %d", remaining); + if (remaining > 0) { + if (currentBuffer == null) { + currentBuffer = inBoundBuffer; + } else { + ByteBuffer b = currentBuffer; + if (!tailBuffers.isEmpty()) { + b = tailBuffers.getLast(); + } + + int limit = b.limit(); + int freeSpace = b.capacity() - limit; + if (remaining <= COPY_THRESHOLD && freeSpace >= remaining) { + // append the new data to the unused space in the current buffer + int position = b.position(); + b.position(limit); + b.limit(limit + inBoundBuffer.remaining()); + b.put(inBoundBuffer); + b.position(position); + if (b != currentBuffer) + tailSize += remaining; + DEBUG_LOGGER.log(Level.DEBUG, "copied: %d", remaining); + } else { + DEBUG_LOGGER.log(Level.DEBUG, "added: %d", remaining); + tailBuffers.add(inBoundBuffer); + tailSize += remaining; + } + } + } + DEBUG_LOGGER.log(Level.DEBUG, "Tail size is now: %d, current=", + tailSize, + (currentBuffer == null ? 0 : + currentBuffer.remaining())); + Http2Frame frame; + while ((frame = nextFrame()) != null) { + DEBUG_LOGGER.log(Level.DEBUG, "Got frame: %s", frame); + frameProcessor.processFrame(frame); + frameProcessed(); + } + } + + private Http2Frame nextFrame() throws IOException { + while (true) { + if (currentBuffer == null) { + return null; // no data at all + } + long available = currentBuffer.remaining() + tailSize; + if (!frameHeaderParsed) { + if (available >= Http2Frame.FRAME_HEADER_SIZE) { + parseFrameHeader(); + if (frameLength > maxFrameSize) { + // connection error + return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, + "Frame type("+frameType+") " + +"length("+frameLength + +") exceeds MAX_FRAME_SIZE(" + + maxFrameSize+")"); + } + frameHeaderParsed = true; + } else { + DEBUG_LOGGER.log(Level.DEBUG, + "Not enough data to parse header, needs: %d, has: %d", + Http2Frame.FRAME_HEADER_SIZE, available); + return null; + } + } + available = currentBuffer == null ? 0 : currentBuffer.remaining() + tailSize; + if ((frameLength == 0) || + (currentBuffer != null && available >= frameLength)) { + Http2Frame frame = parseFrameBody(); + frameHeaderParsed = false; + // frame == null means we have to skip this frame and try parse next + if (frame != null) { + return frame; + } + } else { + DEBUG_LOGGER.log(Level.DEBUG, + "Not enough data to parse frame body, needs: %d, has: %d", + frameLength, available); + return null; // no data for the whole frame header + } + } + } + + private void frameProcessed() { + prepareToRelease.clear(); + } + + private void parseFrameHeader() throws IOException { + int x = getInt(); + this.frameLength = (x >>> 8) & 0x00ffffff; + this.frameType = x & 0xff; + this.frameFlags = getByte(); + this.frameStreamid = getInt() & 0x7fffffff; + // R: A reserved 1-bit field. The semantics of this bit are undefined, + // MUST be ignored when receiving. + } + + // move next buffer from tailBuffers to currentBuffer if required + private void nextBuffer() { + if (!currentBuffer.hasRemaining()) { + if (!slicedToDataFrame) { + prepareToRelease.add(currentBuffer); + } + slicedToDataFrame = false; + currentBuffer = tailBuffers.poll(); + if (currentBuffer != null) { + tailSize -= currentBuffer.remaining(); + } + } + } + + public int getByte() { + int res = currentBuffer.get() & 0xff; + nextBuffer(); + return res; + } + + public int getShort() { + if (currentBuffer.remaining() >= 2) { + int res = currentBuffer.getShort() & 0xffff; + nextBuffer(); + return res; + } + int val = getByte(); + val = (val << 8) + getByte(); + return val; + } + + public int getInt() { + if (currentBuffer.remaining() >= 4) { + int res = currentBuffer.getInt(); + nextBuffer(); + return res; + } + int val = getByte(); + val = (val << 8) + getByte(); + val = (val << 8) + getByte(); + val = (val << 8) + getByte(); + return val; + + } + + public byte[] getBytes(int n) { + byte[] bytes = new byte[n]; + int offset = 0; + while (n > 0) { + int length = Math.min(n, currentBuffer.remaining()); + currentBuffer.get(bytes, offset, length); + offset += length; + n -= length; + nextBuffer(); + } + return bytes; + + } + + private List getBuffers(boolean isDataFrame, int bytecount) { + List res = new ArrayList<>(); + while (bytecount > 0) { + int remaining = currentBuffer.remaining(); + int extract = Math.min(remaining, bytecount); + ByteBuffer extractedBuf; + if (isDataFrame) { + extractedBuf = Utils.sliceWithLimitedCapacity(currentBuffer, extract) + .asReadOnlyBuffer(); + slicedToDataFrame = true; + } else { + // Header frames here + // HPACK decoding should performed under lock and immediately after frame decoding. + // in that case it is safe to release original buffer, + // because of sliced buffer has a very short life + extractedBuf = Utils.sliceWithLimitedCapacity(currentBuffer, extract); + } + res.add(extractedBuf); + bytecount -= extract; + nextBuffer(); + } + return res; + } + + public void close(String msg) { + closed = true; + tailBuffers.clear(); + int bytes = tailSize; + ByteBuffer b = currentBuffer; + if (b != null) { + bytes += b.remaining(); + b.position(b.limit()); + } + tailSize = 0; + currentBuffer = null; + DEBUG_LOGGER.log(Level.DEBUG, "closed %s, ignoring %d bytes", msg, bytes); + } + + public void skipBytes(int bytecount) { + while (bytecount > 0) { + int remaining = currentBuffer.remaining(); + int extract = Math.min(remaining, bytecount); + currentBuffer.position(currentBuffer.position() + extract); + bytecount -= remaining; + nextBuffer(); + } + } + + private Http2Frame parseFrameBody() throws IOException { + assert frameHeaderParsed; + switch (frameType) { + case DataFrame.TYPE: + return parseDataFrame(frameLength, frameStreamid, frameFlags); + case HeadersFrame.TYPE: + return parseHeadersFrame(frameLength, frameStreamid, frameFlags); + case PriorityFrame.TYPE: + return parsePriorityFrame(frameLength, frameStreamid, frameFlags); + case ResetFrame.TYPE: + return parseResetFrame(frameLength, frameStreamid, frameFlags); + case SettingsFrame.TYPE: + return parseSettingsFrame(frameLength, frameStreamid, frameFlags); + case PushPromiseFrame.TYPE: + return parsePushPromiseFrame(frameLength, frameStreamid, frameFlags); + case PingFrame.TYPE: + return parsePingFrame(frameLength, frameStreamid, frameFlags); + case GoAwayFrame.TYPE: + return parseGoAwayFrame(frameLength, frameStreamid, frameFlags); + case WindowUpdateFrame.TYPE: + return parseWindowUpdateFrame(frameLength, frameStreamid, frameFlags); + case ContinuationFrame.TYPE: + return parseContinuationFrame(frameLength, frameStreamid, frameFlags); + default: + // RFC 7540 4.1 + // Implementations MUST ignore and discard any frame that has a type that is unknown. + Log.logTrace("Unknown incoming frame type: {0}", frameType); + skipBytes(frameLength); + return null; + } + } + + private Http2Frame parseDataFrame(int frameLength, int streamid, int flags) { + // non-zero stream + if (streamid == 0) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, + "zero streamId for DataFrame"); + } + int padLength = 0; + if ((flags & DataFrame.PADDED) != 0) { + padLength = getByte(); + if (padLength >= frameLength) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, + "the length of the padding is the length of the frame payload or greater"); + } + frameLength--; + } + DataFrame df = new DataFrame(streamid, flags, + getBuffers(true, frameLength - padLength), padLength); + skipBytes(padLength); + return df; + + } + + private Http2Frame parseHeadersFrame(int frameLength, int streamid, int flags) { + // non-zero stream + if (streamid == 0) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, + "zero streamId for HeadersFrame"); + } + int padLength = 0; + if ((flags & HeadersFrame.PADDED) != 0) { + padLength = getByte(); + frameLength--; + } + boolean hasPriority = (flags & HeadersFrame.PRIORITY) != 0; + boolean exclusive = false; + int streamDependency = 0; + int weight = 0; + if (hasPriority) { + int x = getInt(); + exclusive = (x & 0x80000000) != 0; + streamDependency = x & 0x7fffffff; + weight = getByte(); + frameLength -= 5; + } + if(frameLength < padLength) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, + "Padding exceeds the size remaining for the header block"); + } + HeadersFrame hf = new HeadersFrame(streamid, flags, + getBuffers(false, frameLength - padLength), padLength); + skipBytes(padLength); + if (hasPriority) { + hf.setPriority(streamDependency, exclusive, weight); + } + return hf; + } + + private Http2Frame parsePriorityFrame(int frameLength, int streamid, int flags) { + // non-zero stream; no flags + if (streamid == 0) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, + "zero streamId for PriorityFrame"); + } + if(frameLength != 5) { + skipBytes(frameLength); + return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, streamid, + "PriorityFrame length is "+ frameLength+", expected 5"); + } + int x = getInt(); + int weight = getByte(); + return new PriorityFrame(streamid, x & 0x7fffffff, (x & 0x80000000) != 0, weight); + } + + private Http2Frame parseResetFrame(int frameLength, int streamid, int flags) { + // non-zero stream; no flags + if (streamid == 0) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, + "zero streamId for ResetFrame"); + } + if(frameLength != 4) { + return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, + "ResetFrame length is "+ frameLength+", expected 4"); + } + return new ResetFrame(streamid, getInt()); + } + + private Http2Frame parseSettingsFrame(int frameLength, int streamid, int flags) { + // only zero stream + if (streamid != 0) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, + "non-zero streamId for SettingsFrame"); + } + if ((SettingsFrame.ACK & flags) != 0 && frameLength > 0) { + // RFC 7540 6.5 + // Receipt of a SETTINGS frame with the ACK flag set and a length + // field value other than 0 MUST be treated as a connection error + return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, + "ACK SettingsFrame is not empty"); + } + if (frameLength % 6 != 0) { + return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, + "invalid SettingsFrame size: "+frameLength); + } + SettingsFrame sf = new SettingsFrame(flags); + int n = frameLength / 6; + for (int i=0; i 0 && id <= SettingsFrame.MAX_PARAM) { + // a known parameter. Ignore otherwise + sf.setParameter(id, val); // TODO parameters validation + } + } + return sf; + } + + private Http2Frame parsePushPromiseFrame(int frameLength, int streamid, int flags) { + // non-zero stream + if (streamid == 0) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, + "zero streamId for PushPromiseFrame"); + } + int padLength = 0; + if ((flags & PushPromiseFrame.PADDED) != 0) { + padLength = getByte(); + frameLength--; + } + int promisedStream = getInt() & 0x7fffffff; + frameLength -= 4; + if(frameLength < padLength) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, + "Padding exceeds the size remaining for the PushPromiseFrame"); + } + PushPromiseFrame ppf = new PushPromiseFrame(streamid, flags, promisedStream, + getBuffers(false, frameLength - padLength), padLength); + skipBytes(padLength); + return ppf; + } + + private Http2Frame parsePingFrame(int frameLength, int streamid, int flags) { + // only zero stream + if (streamid != 0) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, + "non-zero streamId for PingFrame"); + } + if(frameLength != 8) { + return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, + "PingFrame length is "+ frameLength+", expected 8"); + } + return new PingFrame(flags, getBytes(8)); + } + + private Http2Frame parseGoAwayFrame(int frameLength, int streamid, int flags) { + // only zero stream; no flags + if (streamid != 0) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, + "non-zero streamId for GoAwayFrame"); + } + if (frameLength < 8) { + return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, + "Invalid GoAway frame size"); + } + int lastStream = getInt() & 0x7fffffff; + int errorCode = getInt(); + byte[] debugData = getBytes(frameLength - 8); + if (debugData.length > 0) { + Log.logError("GoAway debugData " + new String(debugData, UTF_8)); + } + return new GoAwayFrame(lastStream, errorCode, debugData); + } + + private Http2Frame parseWindowUpdateFrame(int frameLength, int streamid, int flags) { + // any stream; no flags + if(frameLength != 4) { + return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, + "WindowUpdateFrame length is "+ frameLength+", expected 4"); + } + return new WindowUpdateFrame(streamid, getInt() & 0x7fffffff); + } + + private Http2Frame parseContinuationFrame(int frameLength, int streamid, int flags) { + // non-zero stream; + if (streamid == 0) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, + "zero streamId for ContinuationFrame"); + } + return new ContinuationFrame(streamid, flags, getBuffers(false, frameLength)); + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesEncoder.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesEncoder.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,293 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Frames Encoder + * + * Encode framed into ByteBuffers. + * The class is stateless. + */ +public class FramesEncoder { + + + public FramesEncoder() { + } + + public List encodeFrames(List frames) { + List bufs = new ArrayList<>(frames.size() * 2); + for (HeaderFrame f : frames) { + bufs.addAll(encodeFrame(f)); + } + return bufs; + } + + public ByteBuffer encodeConnectionPreface(byte[] preface, SettingsFrame frame) { + final int length = frame.length(); + ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length + preface.length); + buf.put(preface); + putSettingsFrame(buf, frame, length); + buf.flip(); + return buf; + } + + public List encodeFrame(Http2Frame frame) { + switch (frame.type()) { + case DataFrame.TYPE: + return encodeDataFrame((DataFrame) frame); + case HeadersFrame.TYPE: + return encodeHeadersFrame((HeadersFrame) frame); + case PriorityFrame.TYPE: + return encodePriorityFrame((PriorityFrame) frame); + case ResetFrame.TYPE: + return encodeResetFrame((ResetFrame) frame); + case SettingsFrame.TYPE: + return encodeSettingsFrame((SettingsFrame) frame); + case PushPromiseFrame.TYPE: + return encodePushPromiseFrame((PushPromiseFrame) frame); + case PingFrame.TYPE: + return encodePingFrame((PingFrame) frame); + case GoAwayFrame.TYPE: + return encodeGoAwayFrame((GoAwayFrame) frame); + case WindowUpdateFrame.TYPE: + return encodeWindowUpdateFrame((WindowUpdateFrame) frame); + case ContinuationFrame.TYPE: + return encodeContinuationFrame((ContinuationFrame) frame); + default: + throw new UnsupportedOperationException("Not supported frame "+frame.type()+" ("+frame.getClass().getName()+")"); + } + } + + private static final int NO_FLAGS = 0; + private static final int ZERO_STREAM = 0; + + private List encodeDataFrame(DataFrame frame) { + // non-zero stream + assert frame.streamid() != 0; + ByteBuffer buf = encodeDataFrameStart(frame); + if (frame.getFlag(DataFrame.PADDED)) { + return joinWithPadding(buf, frame.getData(), frame.getPadLength()); + } else { + return join(buf, frame.getData()); + } + } + + private ByteBuffer encodeDataFrameStart(DataFrame frame) { + boolean isPadded = frame.getFlag(DataFrame.PADDED); + final int length = frame.getDataLength() + (isPadded ? (frame.getPadLength() + 1) : 0); + ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 1 : 0)); + putHeader(buf, length, DataFrame.TYPE, frame.getFlags(), frame.streamid()); + if (isPadded) { + buf.put((byte) frame.getPadLength()); + } + buf.flip(); + return buf; + } + + private List encodeHeadersFrame(HeadersFrame frame) { + // non-zero stream + assert frame.streamid() != 0; + ByteBuffer buf = encodeHeadersFrameStart(frame); + if (frame.getFlag(HeadersFrame.PADDED)) { + return joinWithPadding(buf, frame.getHeaderBlock(), frame.getPadLength()); + } else { + return join(buf, frame.getHeaderBlock()); + } + } + + private ByteBuffer encodeHeadersFrameStart(HeadersFrame frame) { + boolean isPadded = frame.getFlag(HeadersFrame.PADDED); + boolean hasPriority = frame.getFlag(HeadersFrame.PRIORITY); + final int length = frame.getHeaderLength() + (isPadded ? (frame.getPadLength() + 1) : 0) + (hasPriority ? 5 : 0); + ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 1 : 0) + (hasPriority ? 5 : 0)); + putHeader(buf, length, HeadersFrame.TYPE, frame.getFlags(), frame.streamid()); + if (isPadded) { + buf.put((byte) frame.getPadLength()); + } + if (hasPriority) { + putPriority(buf, frame.getExclusive(), frame.getStreamDependency(), frame.getWeight()); + } + buf.flip(); + return buf; + } + + private List encodePriorityFrame(PriorityFrame frame) { + // non-zero stream; no flags + assert frame.streamid() != 0; + final int length = 5; + ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); + putHeader(buf, length, PriorityFrame.TYPE, NO_FLAGS, frame.streamid()); + putPriority(buf, frame.exclusive(), frame.streamDependency(), frame.weight()); + buf.flip(); + return List.of(buf); + } + + private List encodeResetFrame(ResetFrame frame) { + // non-zero stream; no flags + assert frame.streamid() != 0; + final int length = 4; + ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); + putHeader(buf, length, ResetFrame.TYPE, NO_FLAGS, frame.streamid()); + buf.putInt(frame.getErrorCode()); + buf.flip(); + return List.of(buf); + } + + private List encodeSettingsFrame(SettingsFrame frame) { + // only zero stream + assert frame.streamid() == 0; + final int length = frame.length(); + ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); + putSettingsFrame(buf, frame, length); + buf.flip(); + return List.of(buf); + } + + private List encodePushPromiseFrame(PushPromiseFrame frame) { + // non-zero stream + assert frame.streamid() != 0; + boolean isPadded = frame.getFlag(PushPromiseFrame.PADDED); + final int length = frame.getHeaderLength() + (isPadded ? 5 : 4); + ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 5 : 4)); + putHeader(buf, length, PushPromiseFrame.TYPE, frame.getFlags(), frame.streamid()); + if (isPadded) { + buf.put((byte) frame.getPadLength()); + } + buf.putInt(frame.getPromisedStream()); + buf.flip(); + + if (frame.getFlag(PushPromiseFrame.PADDED)) { + return joinWithPadding(buf, frame.getHeaderBlock(), frame.getPadLength()); + } else { + return join(buf, frame.getHeaderBlock()); + } + } + + private List encodePingFrame(PingFrame frame) { + // only zero stream + assert frame.streamid() == 0; + final int length = 8; + ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); + putHeader(buf, length, PingFrame.TYPE, frame.getFlags(), ZERO_STREAM); + buf.put(frame.getData()); + buf.flip(); + return List.of(buf); + } + + private List encodeGoAwayFrame(GoAwayFrame frame) { + // only zero stream; no flags + assert frame.streamid() == 0; + byte[] debugData = frame.getDebugData(); + final int length = 8 + debugData.length; + ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); + putHeader(buf, length, GoAwayFrame.TYPE, NO_FLAGS, ZERO_STREAM); + buf.putInt(frame.getLastStream()); + buf.putInt(frame.getErrorCode()); + if (debugData.length > 0) { + buf.put(debugData); + } + buf.flip(); + return List.of(buf); + } + + private List encodeWindowUpdateFrame(WindowUpdateFrame frame) { + // any stream; no flags + final int length = 4; + ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); + putHeader(buf, length, WindowUpdateFrame.TYPE, NO_FLAGS, frame.streamid); + buf.putInt(frame.getUpdate()); + buf.flip(); + return List.of(buf); + } + + private List encodeContinuationFrame(ContinuationFrame frame) { + // non-zero stream; + assert frame.streamid() != 0; + final int length = frame.getHeaderLength(); + ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE); + putHeader(buf, length, ContinuationFrame.TYPE, frame.getFlags(), frame.streamid()); + buf.flip(); + return join(buf, frame.getHeaderBlock()); + } + + private List joinWithPadding(ByteBuffer buf, List data, int padLength) { + int len = data.size(); + if (len == 0) return List.of(buf, getPadding(padLength)); + else if (len == 1) return List.of(buf, data.get(0), getPadding(padLength)); + else if (len == 2) return List.of(buf, data.get(0), data.get(1), getPadding(padLength)); + List res = new ArrayList<>(len+2); + res.add(buf); + res.addAll(data); + res.add(getPadding(padLength)); + return res; + } + + private List join(ByteBuffer buf, List data) { + int len = data.size(); + if (len == 0) return List.of(buf); + else if (len == 1) return List.of(buf, data.get(0)); + else if (len == 2) return List.of(buf, data.get(0), data.get(1)); + List joined = new ArrayList<>(len + 1); + joined.add(buf); + joined.addAll(data); + return joined; + } + + private void putSettingsFrame(ByteBuffer buf, SettingsFrame frame, int length) { + // only zero stream; + assert frame.streamid() == 0; + putHeader(buf, length, SettingsFrame.TYPE, frame.getFlags(), ZERO_STREAM); + frame.toByteBuffer(buf); + } + + private void putHeader(ByteBuffer buf, int length, int type, int flags, int streamId) { + int x = (length << 8) + type; + buf.putInt(x); + buf.put((byte) flags); + buf.putInt(streamId); + } + + private void putPriority(ByteBuffer buf, boolean exclusive, int streamDependency, int weight) { + buf.putInt(exclusive ? (1 << 31) + streamDependency : streamDependency); + buf.put((byte) weight); + } + + private ByteBuffer getBuffer(int capacity) { + return ByteBuffer.allocate(capacity); + } + + public ByteBuffer getPadding(int length) { + if (length > 255) { + throw new IllegalArgumentException("Padding too big"); + } + return ByteBuffer.allocate(length); // zeroed! + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/GoAwayFrame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/GoAwayFrame.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class GoAwayFrame extends ErrorFrame { + + private final int lastStream; + private final byte[] debugData; + + public static final int TYPE = 0x7; + + + public GoAwayFrame(int lastStream, int errorCode) { + this(lastStream, errorCode, new byte[0]); + } + + public GoAwayFrame(int lastStream, int errorCode, byte[] debugData) { + super(0, 0, errorCode); + this.lastStream = lastStream; + this.debugData = debugData.clone(); + } + + @Override + public int type() { + return TYPE; + } + + @Override + int length() { + return 8 + debugData.length; + } + + @Override + public String toString() { + return super.toString() + " Debugdata: " + new String(debugData, UTF_8); + } + + public int getLastStream() { + return this.lastStream; + } + + public byte[] getDebugData() { + return debugData.clone(); + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/HeaderFrame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/HeaderFrame.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +import jdk.internal.net.http.common.Utils; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * Either a HeadersFrame or a ContinuationFrame + */ +public abstract class HeaderFrame extends Http2Frame { + + final int headerLength; + final List headerBlocks; + + public static final int END_STREAM = 0x1; + public static final int END_HEADERS = 0x4; + + public HeaderFrame(int streamid, int flags, List headerBlocks) { + super(streamid, flags); + this.headerBlocks = headerBlocks; + this.headerLength = Utils.remaining(headerBlocks, Integer.MAX_VALUE); + } + + @Override + public String flagAsString(int flag) { + switch (flag) { + case END_HEADERS: + return "END_HEADERS"; + case END_STREAM: + return "END_STREAM"; + } + return super.flagAsString(flag); + } + + + public List getHeaderBlock() { + return headerBlocks; + } + + int getHeaderLength() { + return headerLength; + } + + /** + * Returns true if this block is the final block of headers. + */ + public boolean endHeaders() { + return getFlag(END_HEADERS); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/HeadersFrame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/HeadersFrame.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +import java.nio.ByteBuffer; +import java.util.List; + +public class HeadersFrame extends HeaderFrame { + + public static final int TYPE = 0x1; + + // Flags + public static final int END_STREAM = 0x1; + public static final int PADDED = 0x8; + public static final int PRIORITY = 0x20; + + + private int padLength; + private int streamDependency; + private int weight; + private boolean exclusive; + + public HeadersFrame(int streamid, int flags, List headerBlocks, int padLength) { + super(streamid, flags, headerBlocks); + if (padLength > 0) { + setPadLength(padLength); + } + } + + public HeadersFrame(int streamid, int flags, List headerBlocks) { + super(streamid, flags, headerBlocks); + } + + public HeadersFrame(int streamid, int flags, ByteBuffer headerBlock) { + this(streamid, flags, List.of(headerBlock)); + } + + @Override + public int type() { + return TYPE; + } + + @Override + int length() { + return headerLength + + ((flags & PADDED) != 0 ? (1 + padLength) : 0) + + ((flags & PRIORITY) != 0 ? 5 : 0); + } + + @Override + public String flagAsString(int flag) { + switch (flag) { + case END_STREAM: + return "END_STREAM"; + case PADDED: + return "PADDED"; + case PRIORITY: + return "PRIORITY"; + } + return super.flagAsString(flag); + } + + public void setPadLength(int padLength) { + this.padLength = padLength; + flags |= PADDED; + } + + int getPadLength() { + return padLength; + } + + public void setPriority(int streamDependency, boolean exclusive, int weight) { + this.streamDependency = streamDependency; + this.exclusive = exclusive; + this.weight = weight; + this.flags |= PRIORITY; + } + + public int getStreamDependency() { + return streamDependency; + } + + public int getWeight() { + return weight; + } + + public boolean getExclusive() { + return exclusive; + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/Http2Frame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/Http2Frame.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +/** + * When sending a frame, the length field must be set in sub-class + * by calling computeLength() + */ +public abstract class Http2Frame { + + public static final int FRAME_HEADER_SIZE = 9; + + protected int streamid; + protected int flags; + + public Http2Frame(int streamid, int flags) { + this.streamid = streamid; + this.flags = flags; + } + + public int streamid() { + return streamid; + } + + public void setFlag(int flag) { + flags |= flag; + } + + public int getFlags() { + return flags; + } + + public boolean getFlag(int flag) { + return (flags & flag) != 0; + } + +// public void clearFlag(int flag) { +// flags &= 0xffffffff ^ flag; +// } + + public void streamid(int streamid) { + this.streamid = streamid; + } + + + private String typeAsString() { + return asString(type()); + } + + public int type() { + return -1; // Unknown type + } + + int length() { + return -1; // Unknown length + } + + + public static String asString(int type) { + switch (type) { + case DataFrame.TYPE: + return "DATA"; + case HeadersFrame.TYPE: + return "HEADERS"; + case ContinuationFrame.TYPE: + return "CONTINUATION"; + case ResetFrame.TYPE: + return "RESET"; + case PriorityFrame.TYPE: + return "PRIORITY"; + case SettingsFrame.TYPE: + return "SETTINGS"; + case GoAwayFrame.TYPE: + return "GOAWAY"; + case PingFrame.TYPE: + return "PING"; + case PushPromiseFrame.TYPE: + return "PUSH_PROMISE"; + case WindowUpdateFrame.TYPE: + return "WINDOW_UPDATE"; + default: + return "UNKNOWN"; + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(typeAsString()) + .append(": length=") + .append(Integer.toString(length())) + .append(", streamid=") + .append(streamid) + .append(", flags="); + + int f = flags; + int i = 0; + if (f == 0) { + sb.append("0 "); + } else { + while (f != 0) { + if ((f & 1) == 1) { + sb.append(flagAsString(1 << i)) + .append(' '); + } + f = f >> 1; + i++; + } + } + return sb.toString(); + } + + // Override + public String flagAsString(int f) { + return "unknown"; + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/MalformedFrame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/MalformedFrame.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +public class MalformedFrame extends Http2Frame { + + private int errorCode; + // if errorStream == 0 means Connection Error; RFC 7540 5.4.1 + // if errorStream != 0 means Stream Error; RFC 7540 5.4.2 + private int errorStream; + private String msg; + + /** + * Creates Connection Error malformed frame + * @param errorCode - error code, as specified by RFC 7540 + * @param msg - internal debug message + */ + public MalformedFrame(int errorCode, String msg) { + this(errorCode, 0 , msg); + } + + /** + * Creates Stream Error malformed frame + * @param errorCode - error code, as specified by RFC 7540 + * @param errorStream - id of error stream (RST_FRAME will be send for this stream) + * @param msg - internal debug message + */ + public MalformedFrame(int errorCode, int errorStream, String msg) { + super(0, 0); + this.errorCode = errorCode; + this.errorStream = errorStream; + this.msg = msg; + } + + @Override + public String toString() { + return super.toString() + " MalformedFrame, Error: " + ErrorFrame.stringForCode(errorCode) + + " streamid: " + streamid + " reason: " + msg; + } + + public int getErrorCode() { + return errorCode; + } + + public String getMessage() { + return msg; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/OutgoingHeaders.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/OutgoingHeaders.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +import java.net.http.HttpHeaders; + +/** + * Contains all parameters for outgoing headers. Is converted to + * HeadersFrame and ContinuationFrames by Http2Connection. + */ +public class OutgoingHeaders extends Http2Frame { + + int streamDependency; + int weight; + boolean exclusive; + T attachment; + + public static final int PRIORITY = 0x20; + + HttpHeaders user, system; + + public OutgoingHeaders(HttpHeaders hdrs1, HttpHeaders hdrs2, T attachment) { + super(0, 0); + this.user = hdrs2; + this.system = hdrs1; + this.attachment = attachment; + } + + public void setPriority(int streamDependency, boolean exclusive, int weight) { + this.streamDependency = streamDependency; + this.exclusive = exclusive; + this.weight = weight; + this.flags |= PRIORITY; + } + + public int getStreamDependency() { + return streamDependency; + } + + public int getWeight() { + return weight; + } + + public boolean getExclusive() { + return exclusive; + } + + public T getAttachment() { + return attachment; + } + + public HttpHeaders getUserHeaders() { + return user; + } + + public HttpHeaders getSystemHeaders() { + return system; + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/PingFrame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/PingFrame.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +public class PingFrame extends Http2Frame { + + + private final byte[] data; + + public static final int TYPE = 0x6; + + // Flags + public static final int ACK = 0x1; + + public PingFrame(int flags, byte[] data) { + super(0, flags); + assert data.length == 8; + this.data = data.clone(); + } + + @Override + public int type() { + return TYPE; + } + + @Override + int length() { + return 8; + } + + @Override + public String flagAsString(int flag) { + switch (flag) { + case ACK: + return "ACK"; + } + return super.flagAsString(flag); + } + + public byte[] getData() { + return data.clone(); + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/PriorityFrame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/PriorityFrame.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +public class PriorityFrame extends Http2Frame { + + private final int streamDependency; + private final int weight; + private final boolean exclusive; + + public static final int TYPE = 0x2; + + public PriorityFrame(int streamId, int streamDependency, boolean exclusive, int weight) { + super(streamId, 0); + this.streamDependency = streamDependency; + this.exclusive = exclusive; + this.weight = weight; + } + + @Override + public int type() { + return TYPE; + } + + @Override + int length() { + return 5; + } + + public int streamDependency() { + return streamDependency; + } + + public int weight() { + return weight; + } + + public boolean exclusive() { + return exclusive; + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/PushPromiseFrame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/PushPromiseFrame.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +import java.nio.ByteBuffer; +import java.util.List; + +public class PushPromiseFrame extends HeaderFrame { + + private int padLength; + private final int promisedStream; + + public static final int TYPE = 0x5; + + // Flags + public static final int END_HEADERS = 0x4; + public static final int PADDED = 0x8; + + public PushPromiseFrame(int streamid, int flags, int promisedStream, List buffers, int padLength) { + super(streamid, flags, buffers); + this.promisedStream = promisedStream; + if(padLength > 0 ) { + setPadLength(padLength); + } + } + + @Override + public int type() { + return TYPE; + } + + @Override + int length() { + return headerLength + ((flags & PADDED) != 0 ? 5 : 4); + } + + @Override + public String toString() { + return super.toString() + " promisedStreamid: " + promisedStream + + " headerLength: " + headerLength; + } + + @Override + public String flagAsString(int flag) { + switch (flag) { + case PADDED: + return "PADDED"; + case END_HEADERS: + return "END_HEADERS"; + } + return super.flagAsString(flag); + } + + public void setPadLength(int padLength) { + this.padLength = padLength; + flags |= PADDED; + } + + public int getPadLength() { + return padLength; + } + + public int getPromisedStream() { + return promisedStream; + } + + @Override + public boolean endHeaders() { + return getFlag(END_HEADERS); + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/ResetFrame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/ResetFrame.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +public class ResetFrame extends ErrorFrame { + + public static final int TYPE = 0x3; + + // See ErrorFrame for error values + + public ResetFrame(int streamid, int errorCode) { + super(streamid, 0, errorCode); + } + + @Override + public int type() { + return TYPE; + } + + @Override + int length() { + return 4; + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/SettingsFrame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/SettingsFrame.java Wed Feb 07 21:45:37 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.internal.net.http.frame; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +public class SettingsFrame extends Http2Frame { + + private final int[] parameters; + + public static final int TYPE = 0x4; + + // Flags + public static final int ACK = 0x1; + + @Override + public String flagAsString(int flag) { + switch (flag) { + case ACK: + return "ACK"; + } + return super.flagAsString(flag); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(super.toString()) + .append(" Settings: "); + + for (int i = 0; i < MAX_PARAM; i++) { + if (parameters[i] != -1) { + sb.append(name(i)) + .append("=") + .append(Integer.toString(parameters[i])) + .append(' '); + } + } + return sb.toString(); + } + + // Parameters + public static final int HEADER_TABLE_SIZE = 0x1; + public static final int ENABLE_PUSH = 0x2; + public static final int MAX_CONCURRENT_STREAMS = 0x3; + public static final int INITIAL_WINDOW_SIZE = 0x4; + public static final int MAX_FRAME_SIZE = 0x5; + public static final int MAX_HEADER_LIST_SIZE = 0x6; + + private String name(int i) { + switch (i+1) { + case HEADER_TABLE_SIZE: + return "HEADER_TABLE_SIZE"; + case ENABLE_PUSH: + return "ENABLE_PUSH"; + case MAX_CONCURRENT_STREAMS: + return "MAX_CONCURRENT_STREAMS"; + case INITIAL_WINDOW_SIZE: + return "INITIAL_WINDOW_SIZE"; + case MAX_FRAME_SIZE: + return "MAX_FRAME_SIZE"; + case MAX_HEADER_LIST_SIZE: + return "MAX_HEADER_LIST_SIZE"; + } + return "unknown parameter"; + } + public static final int MAX_PARAM = 0x6; + + public SettingsFrame(int flags) { + super(0, flags); + parameters = new int [MAX_PARAM]; + Arrays.fill(parameters, -1); + } + + public SettingsFrame() { + this(0); + } + + public SettingsFrame(SettingsFrame other) { + super(0, other.flags); + parameters = Arrays.copyOf(other.parameters, MAX_PARAM); + } + + @Override + public int type() { + return TYPE; + } + + public int getParameter(int paramID) { + if (paramID > MAX_PARAM) { + throw new IllegalArgumentException("illegal parameter"); + } + return parameters[paramID-1]; + } + + public SettingsFrame setParameter(int paramID, int value) { + if (paramID > MAX_PARAM) { + throw new IllegalArgumentException("illegal parameter"); + } + parameters[paramID-1] = value; + return this; + } + + int length() { + int len = 0; + for (int i : parameters) { + if (i != -1) { + len += 6; + } + } + return len; + } + + void toByteBuffer(ByteBuffer buf) { + for (int i = 0; i < MAX_PARAM; i++) { + if (parameters[i] != -1) { + buf.putShort((short) (i + 1)); + buf.putInt(parameters[i]); + } + } + } + + public byte[] toByteArray() { + byte[] bytes = new byte[length()]; + ByteBuffer buf = ByteBuffer.wrap(bytes); + toByteBuffer(buf); + return bytes; + } + + private static final int K = 1024; + + public static SettingsFrame getDefaultSettings() { + SettingsFrame f = new SettingsFrame(); + // TODO: check these values + f.setParameter(ENABLE_PUSH, 1); + f.setParameter(HEADER_TABLE_SIZE, 4 * K); + f.setParameter(MAX_CONCURRENT_STREAMS, 35); + f.setParameter(INITIAL_WINDOW_SIZE, 64 * K - 1); + f.setParameter(MAX_FRAME_SIZE, 16 * K); + return f; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/frame/WindowUpdateFrame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/WindowUpdateFrame.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +public class WindowUpdateFrame extends Http2Frame { + + private final int windowUpdate; + + public static final int TYPE = 0x8; + + public WindowUpdateFrame(int streamid, int windowUpdate) { + super(streamid, 0); + this.windowUpdate = windowUpdate; + } + + @Override + public int type() { + return TYPE; + } + + @Override + int length() { + return 4; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(super.toString()) + .append(" WindowUpdate: ") + .append(windowUpdate); + return sb.toString(); + } + + public int getUpdate() { + return this.windowUpdate; + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/BinaryRepresentationWriter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/BinaryRepresentationWriter.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import java.nio.ByteBuffer; + +interface BinaryRepresentationWriter { + + boolean write(HeaderTable table, ByteBuffer destination); + + BinaryRepresentationWriter reset(); +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/BulkSizeUpdateWriter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/BulkSizeUpdateWriter.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import java.nio.ByteBuffer; +import java.util.Iterator; + +import static java.util.Objects.requireNonNull; + +final class BulkSizeUpdateWriter implements BinaryRepresentationWriter { + + private final SizeUpdateWriter writer = new SizeUpdateWriter(); + private Iterator maxSizes; + private boolean writing; + private boolean configured; + + BulkSizeUpdateWriter maxHeaderTableSizes(Iterable sizes) { + if (configured) { + throw new IllegalStateException("Already configured"); + } + requireNonNull(sizes, "sizes"); + maxSizes = sizes.iterator(); + configured = true; + return this; + } + + @Override + public boolean write(HeaderTable table, ByteBuffer destination) { + if (!configured) { + throw new IllegalStateException("Configure first"); + } + while (true) { + if (writing) { + if (!writer.write(table, destination)) { + return false; + } + writing = false; + } else if (maxSizes.hasNext()) { + writing = true; + writer.reset(); + writer.maxHeaderTableSize(maxSizes.next()); + } else { + configured = false; + return true; + } + } + } + + @Override + public BulkSizeUpdateWriter reset() { + maxSizes = null; + writing = false; + configured = false; + return this; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,594 @@ +/* + * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import jdk.internal.net.http.hpack.HPACK.Logger; +import jdk.internal.vm.annotation.Stable; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static jdk.internal.net.http.hpack.HPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.hpack.HPACK.Logger.Level.NORMAL; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +/** + * Decodes headers from their binary representation. + * + *

Typical lifecycle looks like this: + * + *

{@link #Decoder(int) new Decoder} + * ({@link #setMaxCapacity(int) setMaxCapacity}? + * {@link #decode(ByteBuffer, boolean, DecodingCallback) decode})* + * + * @apiNote + * + *

The design intentions behind Decoder were to facilitate flexible and + * incremental style of processing. + * + *

{@code Decoder} does not require a complete header block in a single + * {@code ByteBuffer}. The header block can be spread across many buffers of any + * size and decoded one-by-one the way it makes most sense for the user. This + * way also allows not to limit the size of the header block. + * + *

Headers are delivered to the {@linkplain DecodingCallback callback} as + * soon as they become decoded. Using the callback also gives the user a freedom + * to decide how headers are processed. The callback does not limit the number + * of headers decoded during single decoding operation. + * + * @since 9 + */ +public final class Decoder { + + private final Logger logger; + private static final AtomicLong DECODERS_IDS = new AtomicLong(); + + @Stable + private static final State[] states = new State[256]; + + static { + // To be able to do a quick lookup, each of 256 possibilities are mapped + // to corresponding states. + // + // We can safely do this since patterns 1, 01, 001, 0001, 0000 are + // Huffman prefixes and therefore are inherently not ambiguous. + // + // I do it mainly for better debugging (to not go each time step by step + // through if...else tree). As for performance win for the decoding, I + // believe is negligible. + for (int i = 0; i < states.length; i++) { + if ((i & 0b1000_0000) == 0b1000_0000) { + states[i] = State.INDEXED; + } else if ((i & 0b1100_0000) == 0b0100_0000) { + states[i] = State.LITERAL_WITH_INDEXING; + } else if ((i & 0b1110_0000) == 0b0010_0000) { + states[i] = State.SIZE_UPDATE; + } else if ((i & 0b1111_0000) == 0b0001_0000) { + states[i] = State.LITERAL_NEVER_INDEXED; + } else if ((i & 0b1111_0000) == 0b0000_0000) { + states[i] = State.LITERAL; + } else { + throw new InternalError(String.valueOf(i)); + } + } + } + + private final long id; + private final HeaderTable table; + + private State state = State.READY; + private final IntegerReader integerReader; + private final StringReader stringReader; + private final StringBuilder name; + private final StringBuilder value; + private int intValue; + private boolean firstValueRead; + private boolean firstValueIndex; + private boolean nameHuffmanEncoded; + private boolean valueHuffmanEncoded; + private int capacity; + + /** + * Constructs a {@code Decoder} with the specified initial capacity of the + * header table. + * + *

The value has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses HPACK + * (see 4.2. Maximum Table Size). + * + * @param capacity + * a non-negative integer + * + * @throws IllegalArgumentException + * if capacity is negative + */ + public Decoder(int capacity) { + id = DECODERS_IDS.incrementAndGet(); + logger = HPACK.getLogger().subLogger("Decoder#" + id); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("new decoder with maximum table size %s", + capacity)); + } + if (logger.isLoggable(NORMAL)) { + /* To correlate with logging outside HPACK, knowing + hashCode/toString is important */ + logger.log(NORMAL, () -> { + String hashCode = Integer.toHexString( + System.identityHashCode(this)); + return format("toString='%s', identityHashCode=%s", + toString(), hashCode); + }); + } + setMaxCapacity0(capacity); + table = new HeaderTable(capacity, logger.subLogger("HeaderTable")); + integerReader = new IntegerReader(); + stringReader = new StringReader(); + name = new StringBuilder(512); + value = new StringBuilder(1024); + } + + /** + * Sets a maximum capacity of the header table. + * + *

The value has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses HPACK + * (see 4.2. Maximum Table Size). + * + * @param capacity + * a non-negative integer + * + * @throws IllegalArgumentException + * if capacity is negative + */ + public void setMaxCapacity(int capacity) { + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("setting maximum table size to %s", + capacity)); + } + setMaxCapacity0(capacity); + } + + private void setMaxCapacity0(int capacity) { + if (capacity < 0) { + throw new IllegalArgumentException("capacity >= 0: " + capacity); + } + // FIXME: await capacity update if less than what was prior to it + this.capacity = capacity; + } + + /** + * Decodes a header block from the given buffer to the given callback. + * + *

Suppose a header block is represented by a sequence of + * {@code ByteBuffer}s in the form of {@code Iterator}. And the + * consumer of decoded headers is represented by the callback. Then to + * decode the header block, the following approach might be used: + * + *

{@code
+     * while (buffers.hasNext()) {
+     *     ByteBuffer input = buffers.next();
+     *     decoder.decode(input, callback, !buffers.hasNext());
+     * }
+     * }
+ * + *

The decoder reads as much as possible of the header block from the + * given buffer, starting at the buffer's position, and increments its + * position to reflect the bytes read. The buffer's mark and limit will not + * be modified. + * + *

Once the method is invoked with {@code endOfHeaderBlock == true}, the + * current header block is deemed ended, and inconsistencies, if any, are + * reported immediately by throwing an {@code IOException}. + * + *

Each callback method is called only after the implementation has + * processed the corresponding bytes. If the bytes revealed a decoding + * error, the callback method is not called. + * + *

In addition to exceptions thrown directly by the method, any + * exceptions thrown from the {@code callback} will bubble up. + * + * @apiNote The method asks for {@code endOfHeaderBlock} flag instead of + * returning it for two reasons. The first one is that the user of the + * decoder always knows which chunk is the last. The second one is to throw + * the most detailed exception possible, which might be useful for + * diagnosing issues. + * + * @implNote This implementation is not atomic in respect to decoding + * errors. In other words, if the decoding operation has thrown a decoding + * error, the decoder is no longer usable. + * + * @param headerBlock + * the chunk of the header block, may be empty + * @param endOfHeaderBlock + * true if the chunk is the final (or the only one) in the sequence + * + * @param consumer + * the callback + * @throws IOException + * in case of a decoding error + * @throws NullPointerException + * if either headerBlock or consumer are null + */ + public void decode(ByteBuffer headerBlock, + boolean endOfHeaderBlock, + DecodingCallback consumer) throws IOException { + requireNonNull(headerBlock, "headerBlock"); + requireNonNull(consumer, "consumer"); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("reading %s, end of header block? %s", + headerBlock, endOfHeaderBlock)); + } + while (headerBlock.hasRemaining()) { + proceed(headerBlock, consumer); + } + if (endOfHeaderBlock && state != State.READY) { + logger.log(NORMAL, () -> format("unexpected end of %s representation", + state)); + throw new IOException("Unexpected end of header block"); + } + } + + private void proceed(ByteBuffer input, DecodingCallback action) + throws IOException { + switch (state) { + case READY: + resumeReady(input); + break; + case INDEXED: + resumeIndexed(input, action); + break; + case LITERAL: + resumeLiteral(input, action); + break; + case LITERAL_WITH_INDEXING: + resumeLiteralWithIndexing(input, action); + break; + case LITERAL_NEVER_INDEXED: + resumeLiteralNeverIndexed(input, action); + break; + case SIZE_UPDATE: + resumeSizeUpdate(input, action); + break; + default: + throw new InternalError("Unexpected decoder state: " + state); + } + } + + private void resumeReady(ByteBuffer input) { + int b = input.get(input.position()) & 0xff; // absolute read + State s = states[b]; + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("next binary representation %s (first byte 0x%02x)", + s, b)); + } + switch (s) { + case INDEXED: + integerReader.configure(7); + state = State.INDEXED; + firstValueIndex = true; + break; + case LITERAL: + state = State.LITERAL; + firstValueIndex = (b & 0b0000_1111) != 0; + if (firstValueIndex) { + integerReader.configure(4); + } + break; + case LITERAL_WITH_INDEXING: + state = State.LITERAL_WITH_INDEXING; + firstValueIndex = (b & 0b0011_1111) != 0; + if (firstValueIndex) { + integerReader.configure(6); + } + break; + case LITERAL_NEVER_INDEXED: + state = State.LITERAL_NEVER_INDEXED; + firstValueIndex = (b & 0b0000_1111) != 0; + if (firstValueIndex) { + integerReader.configure(4); + } + break; + case SIZE_UPDATE: + integerReader.configure(5); + state = State.SIZE_UPDATE; + firstValueIndex = true; + break; + default: + throw new InternalError(String.valueOf(s)); + } + if (!firstValueIndex) { + input.get(); // advance, next stop: "String Literal" + } + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | Index (7+) | + // +---+---------------------------+ + // + private void resumeIndexed(ByteBuffer input, DecodingCallback action) + throws IOException { + if (!integerReader.read(input)) { + return; + } + intValue = integerReader.get(); + integerReader.reset(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("indexed %s", intValue)); + } + try { + HeaderTable.HeaderField f = getHeaderFieldAt(intValue); + action.onIndexed(intValue, f.name, f.value); + } finally { + state = State.READY; + } + } + + private HeaderTable.HeaderField getHeaderFieldAt(int index) + throws IOException + { + HeaderTable.HeaderField f; + try { + f = table.get(index); + } catch (IndexOutOfBoundsException e) { + throw new IOException("header fields table index", e); + } + return f; + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | Index (4+) | + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | 0 | + // +---+---+-----------------------+ + // | H | Name Length (7+) | + // +---+---------------------------+ + // | Name String (Length octets) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + private void resumeLiteral(ByteBuffer input, DecodingCallback action) + throws IOException { + if (!completeReading(input)) { + return; + } + try { + if (firstValueIndex) { + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')", + intValue, value)); + } + HeaderTable.HeaderField f = getHeaderFieldAt(intValue); + action.onLiteral(intValue, f.name, value, valueHuffmanEncoded); + } else { + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')", + name, value)); + } + action.onLiteral(name, nameHuffmanEncoded, value, valueHuffmanEncoded); + } + } finally { + cleanUpAfterReading(); + } + } + + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | Index (6+) | + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | 0 | + // +---+---+-----------------------+ + // | H | Name Length (7+) | + // +---+---------------------------+ + // | Name String (Length octets) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + private void resumeLiteralWithIndexing(ByteBuffer input, + DecodingCallback action) + throws IOException { + if (!completeReading(input)) { + return; + } + try { + // + // 1. (name, value) will be stored in the table as strings + // 2. Most likely the callback will also create strings from them + // ------------------------------------------------------------------------ + // Let's create those string beforehand (and only once!) to benefit everyone + // + String n; + String v = value.toString(); + if (firstValueIndex) { + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')", + intValue, value)); + } + HeaderTable.HeaderField f = getHeaderFieldAt(intValue); + n = f.name; + action.onLiteralWithIndexing(intValue, n, v, valueHuffmanEncoded); + } else { + n = name.toString(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')", + n, value)); + } + action.onLiteralWithIndexing(n, nameHuffmanEncoded, v, valueHuffmanEncoded); + } + table.put(n, v); + } finally { + cleanUpAfterReading(); + } + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | Index (4+) | + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | 0 | + // +---+---+-----------------------+ + // | H | Name Length (7+) | + // +---+---------------------------+ + // | Name String (Length octets) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + private void resumeLiteralNeverIndexed(ByteBuffer input, + DecodingCallback action) + throws IOException { + if (!completeReading(input)) { + return; + } + try { + if (firstValueIndex) { + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')", + intValue, value)); + } + HeaderTable.HeaderField f = getHeaderFieldAt(intValue); + action.onLiteralNeverIndexed(intValue, f.name, value, valueHuffmanEncoded); + } else { + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')", + name, value)); + } + action.onLiteralNeverIndexed(name, nameHuffmanEncoded, value, valueHuffmanEncoded); + } + } finally { + cleanUpAfterReading(); + } + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | Max size (5+) | + // +---+---------------------------+ + // + private void resumeSizeUpdate(ByteBuffer input, + DecodingCallback action) throws IOException { + if (!integerReader.read(input)) { + return; + } + intValue = integerReader.get(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("dynamic table size update %s", + intValue)); + } + assert intValue >= 0; + if (intValue > capacity) { + throw new IOException( + format("Received capacity exceeds expected: capacity=%s, expected=%s", + intValue, capacity)); + } + integerReader.reset(); + try { + action.onSizeUpdate(intValue); + table.setMaxSize(intValue); + } finally { + state = State.READY; + } + } + + private boolean completeReading(ByteBuffer input) throws IOException { + if (!firstValueRead) { + if (firstValueIndex) { + if (!integerReader.read(input)) { + return false; + } + intValue = integerReader.get(); + integerReader.reset(); + } else { + if (!stringReader.read(input, name)) { + return false; + } + nameHuffmanEncoded = stringReader.isHuffmanEncoded(); + stringReader.reset(); + } + firstValueRead = true; + return false; + } else { + if (!stringReader.read(input, value)) { + return false; + } + } + valueHuffmanEncoded = stringReader.isHuffmanEncoded(); + stringReader.reset(); + return true; + } + + private void cleanUpAfterReading() { + name.setLength(0); + value.setLength(0); + firstValueRead = false; + state = State.READY; + } + + private enum State { + READY, + INDEXED, + LITERAL_NEVER_INDEXED, + LITERAL, + LITERAL_WITH_INDEXING, + SIZE_UPDATE + } + + HeaderTable getTable() { + return table; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/DecodingCallback.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/DecodingCallback.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import java.nio.ByteBuffer; + +/** + * Delivers results of the {@link Decoder#decode(ByteBuffer, boolean, + * DecodingCallback) decoding operation}. + * + *

Methods of the callback are never called by a decoder with any of the + * arguments being {@code null}. + * + * @apiNote + * + *

The callback provides methods for all possible + * binary representations. + * This could be useful for implementing an intermediary, logging, debugging, + * etc. + * + *

The callback is an interface in order to interoperate with lambdas (in + * the most common use case): + *

{@code
+ *     DecodingCallback callback = (name, value) -> System.out.println(name + ", " + value);
+ * }
+ * + *

Names and values are {@link CharSequence}s rather than {@link String}s in + * order to allow users to decide whether or not they need to create objects. A + * {@code CharSequence} might be used in-place, for example, to be appended to + * an {@link Appendable} (e.g. {@link StringBuilder}) and then discarded. + * + *

That said, if a passed {@code CharSequence} needs to outlast the method + * call, it needs to be copied. + * + * @since 9 + */ +@FunctionalInterface +public interface DecodingCallback { + + /** + * A method the more specific methods of the callback forward their calls + * to. + * + * @param name + * header name + * @param value + * header value + */ + void onDecoded(CharSequence name, CharSequence value); + + /** + * A more finer-grained version of {@link #onDecoded(CharSequence, + * CharSequence)} that also reports on value sensitivity. + * + *

Value sensitivity must be considered, for example, when implementing + * an intermediary. A {@code value} is sensitive if it was represented as Literal Header + * Field Never Indexed. + * + *

It is required that intermediaries MUST use the {@linkplain + * Encoder#header(CharSequence, CharSequence, boolean) same representation} + * for encoding this header field in order to protect its value which is not + * to be put at risk by compressing it. + * + * @implSpec + * + *

The default implementation invokes {@code onDecoded(name, value)}. + * + * @param name + * header name + * @param value + * header value + * @param sensitive + * whether or not the value is sensitive + * + * @see #onLiteralNeverIndexed(int, CharSequence, CharSequence, boolean) + * @see #onLiteralNeverIndexed(CharSequence, boolean, CharSequence, boolean) + */ + default void onDecoded(CharSequence name, + CharSequence value, + boolean sensitive) { + onDecoded(name, value); + } + + /** + * An Indexed + * Header Field decoded. + * + * @implSpec + * + *

The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param index + * index of an entry in the table + * @param name + * header name + * @param value + * header value + */ + default void onIndexed(int index, CharSequence name, CharSequence value) { + onDecoded(name, value, false); + } + + /** + * A Literal + * Header Field without Indexing decoded, where a {@code name} was + * referred by an {@code index}. + * + * @implSpec + * + *

The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param index + * index of an entry in the table + * @param name + * header name + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteral(int index, + CharSequence name, + CharSequence value, + boolean valueHuffman) { + onDecoded(name, value, false); + } + + /** + * A Literal + * Header Field without Indexing decoded, where both a {@code name} and + * a {@code value} were literal. + * + * @implSpec + * + *

The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param name + * header name + * @param nameHuffman + * if the {@code name} was Huffman encoded + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteral(CharSequence name, + boolean nameHuffman, + CharSequence value, + boolean valueHuffman) { + onDecoded(name, value, false); + } + + /** + * A Literal + * Header Field Never Indexed decoded, where a {@code name} + * was referred by an {@code index}. + * + * @implSpec + * + *

The default implementation invokes + * {@code onDecoded(name, value, true)}. + * + * @param index + * index of an entry in the table + * @param name + * header name + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteralNeverIndexed(int index, + CharSequence name, + CharSequence value, + boolean valueHuffman) { + onDecoded(name, value, true); + } + + /** + * A Literal + * Header Field Never Indexed decoded, where both a {@code + * name} and a {@code value} were literal. + * + * @implSpec + * + *

The default implementation invokes + * {@code onDecoded(name, value, true)}. + * + * @param name + * header name + * @param nameHuffman + * if the {@code name} was Huffman encoded + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteralNeverIndexed(CharSequence name, + boolean nameHuffman, + CharSequence value, + boolean valueHuffman) { + onDecoded(name, value, true); + } + + /** + * A Literal + * Header Field with Incremental Indexing decoded, where a {@code name} + * was referred by an {@code index}. + * + * @implSpec + * + *

The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param index + * index of an entry in the table + * @param name + * header name + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteralWithIndexing(int index, + CharSequence name, + CharSequence value, + boolean valueHuffman) { + onDecoded(name, value, false); + } + + /** + * A Literal + * Header Field with Incremental Indexing decoded, where both a {@code + * name} and a {@code value} were literal. + * + * @implSpec + * + *

The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param name + * header name + * @param nameHuffman + * if the {@code name} was Huffman encoded + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteralWithIndexing(CharSequence name, + boolean nameHuffman, + CharSequence value, + boolean valueHuffman) { + onDecoded(name, value, false); + } + + /** + * A Dynamic Table + * Size Update decoded. + * + * @implSpec + * + *

The default implementation does nothing. + * + * @param capacity + * new capacity of the header table + */ + default void onSizeUpdate(int capacity) { } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/Encoder.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Encoder.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,525 @@ +/* + * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import jdk.internal.net.http.hpack.HPACK.Logger; + +import java.nio.ByteBuffer; +import java.nio.ReadOnlyBufferException; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.hpack.HPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.hpack.HPACK.Logger.Level.NORMAL; + +/** + * Encodes headers to their binary representation. + * + *

Typical lifecycle looks like this: + * + *

{@link #Encoder(int) new Encoder} + * ({@link #setMaxCapacity(int) setMaxCapacity}? + * {@link #encode(ByteBuffer) encode})* + * + *

Suppose headers are represented by {@code Map>}. + * A supplier and a consumer of {@link ByteBuffer}s in forms of + * {@code Supplier} and {@code Consumer} respectively. + * Then to encode headers, the following approach might be used: + * + *

{@code
+ *     for (Map.Entry> h : headers.entrySet()) {
+ *         String name = h.getKey();
+ *         for (String value : h.getValue()) {
+ *             encoder.header(name, value);        // Set up header
+ *             boolean encoded;
+ *             do {
+ *                 ByteBuffer b = buffersSupplier.get();
+ *                 encoded = encoder.encode(b);    // Encode the header
+ *                 buffersConsumer.accept(b);
+ *             } while (!encoded);
+ *         }
+ *     }
+ * }
+ * + *

Though the specification does not define + * how an encoder is to be implemented, a default implementation is provided by + * the method {@link #header(CharSequence, CharSequence, boolean)}. + * + *

To provide a custom encoding implementation, {@code Encoder} has to be + * extended. A subclass then can access methods for encoding using specific + * representations (e.g. {@link #literal(int, CharSequence, boolean) literal}, + * {@link #indexed(int) indexed}, etc.) + * + * @apiNote + * + *

An Encoder provides an incremental way of encoding headers. + * {@link #encode(ByteBuffer)} takes a buffer a returns a boolean indicating + * whether, or not, the buffer was sufficiently sized to hold the + * remaining of the encoded representation. + * + *

This way, there's no need to provide a buffer of a specific size, or to + * resize (and copy) the buffer on demand, when the remaining encoded + * representation will not fit in the buffer's remaining space. Instead, an + * array of existing buffers can be used, prepended with a frame that encloses + * the resulting header block afterwards. + * + *

Splitting the encoding operation into header set up and header encoding, + * separates long lived arguments ({@code name}, {@code value}, + * {@code sensitivity}, etc.) from the short lived ones (e.g. {@code buffer}), + * simplifying each operation itself. + * + * @implNote + * + *

The default implementation does not use dynamic table. It reports to a + * coupled Decoder a size update with the value of {@code 0}, and never changes + * it afterwards. + * + * @since 9 + */ +public class Encoder { + + private static final AtomicLong ENCODERS_IDS = new AtomicLong(); + + // TODO: enum: no huffman/smart huffman/always huffman + private static final boolean DEFAULT_HUFFMAN = true; + + private final Logger logger; + private final long id; + private final IndexedWriter indexedWriter = new IndexedWriter(); + private final LiteralWriter literalWriter = new LiteralWriter(); + private final LiteralNeverIndexedWriter literalNeverIndexedWriter + = new LiteralNeverIndexedWriter(); + private final LiteralWithIndexingWriter literalWithIndexingWriter + = new LiteralWithIndexingWriter(); + private final SizeUpdateWriter sizeUpdateWriter = new SizeUpdateWriter(); + private final BulkSizeUpdateWriter bulkSizeUpdateWriter + = new BulkSizeUpdateWriter(); + + private BinaryRepresentationWriter writer; + private final HeaderTable headerTable; + + private boolean encoding; + + private int maxCapacity; + private int currCapacity; + private int lastCapacity; + private long minCapacity; + private boolean capacityUpdate; + private boolean configuredCapacityUpdate; + + /** + * Constructs an {@code Encoder} with the specified maximum capacity of the + * header table. + * + *

The value has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses HPACK + * (see 4.2. Maximum Table Size). + * + * @param maxCapacity + * a non-negative integer + * + * @throws IllegalArgumentException + * if maxCapacity is negative + */ + public Encoder(int maxCapacity) { + id = ENCODERS_IDS.incrementAndGet(); + this.logger = HPACK.getLogger().subLogger("Encoder#" + id); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("new encoder with maximum table size %s", + maxCapacity)); + } + if (logger.isLoggable(EXTRA)) { + /* To correlate with logging outside HPACK, knowing + hashCode/toString is important */ + logger.log(EXTRA, () -> { + String hashCode = Integer.toHexString( + System.identityHashCode(this)); + /* Since Encoder can be subclassed hashCode AND identity + hashCode might be different. So let's print both. */ + return format("toString='%s', hashCode=%s, identityHashCode=%s", + toString(), hashCode(), hashCode); + }); + } + if (maxCapacity < 0) { + throw new IllegalArgumentException( + "maxCapacity >= 0: " + maxCapacity); + } + // Initial maximum capacity update mechanics + minCapacity = Long.MAX_VALUE; + currCapacity = -1; + setMaxCapacity0(maxCapacity); + headerTable = new HeaderTable(lastCapacity, logger.subLogger("HeaderTable")); + } + + /** + * Sets up the given header {@code (name, value)}. + * + *

Fixates {@code name} and {@code value} for the duration of encoding. + * + * @param name + * the name + * @param value + * the value + * + * @throws NullPointerException + * if any of the arguments are {@code null} + * @throws IllegalStateException + * if the encoder hasn't fully encoded the previous header, or + * hasn't yet started to encode it + * @see #header(CharSequence, CharSequence, boolean) + */ + public void header(CharSequence name, CharSequence value) + throws IllegalStateException { + header(name, value, false); + } + + /** + * Sets up the given header {@code (name, value)} with possibly sensitive + * value. + * + *

If the {@code value} is sensitive (think security, secrecy, etc.) + * this encoder will compress it using a special representation + * (see 6.2.3. Literal Header Field Never Indexed). + * + *

Fixates {@code name} and {@code value} for the duration of encoding. + * + * @param name + * the name + * @param value + * the value + * @param sensitive + * whether or not the value is sensitive + * + * @throws NullPointerException + * if any of the arguments are {@code null} + * @throws IllegalStateException + * if the encoder hasn't fully encoded the previous header, or + * hasn't yet started to encode it + * @see #header(CharSequence, CharSequence) + * @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean) + */ + public void header(CharSequence name, + CharSequence value, + boolean sensitive) throws IllegalStateException { + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("encoding ('%s', '%s'), sensitive: %s", + name, value, sensitive)); + } + // Arguably a good balance between complexity of implementation and + // efficiency of encoding + requireNonNull(name, "name"); + requireNonNull(value, "value"); + HeaderTable t = getHeaderTable(); + int index = t.indexOf(name, value); + if (index > 0) { + indexed(index); + } else if (index < 0) { + if (sensitive) { + literalNeverIndexed(-index, value, DEFAULT_HUFFMAN); + } else { + literal(-index, value, DEFAULT_HUFFMAN); + } + } else { + if (sensitive) { + literalNeverIndexed(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN); + } else { + literal(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN); + } + } + } + + /** + * Sets a maximum capacity of the header table. + * + *

The value has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses HPACK + * (see 4.2. Maximum Table Size). + * + *

May be called any number of times after or before a complete header + * has been encoded. + * + *

If the encoder decides to change the actual capacity, an update will + * be encoded before a new encoding operation starts. + * + * @param capacity + * a non-negative integer + * + * @throws IllegalArgumentException + * if capacity is negative + * @throws IllegalStateException + * if the encoder hasn't fully encoded the previous header, or + * hasn't yet started to encode it + */ + public void setMaxCapacity(int capacity) { + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("setting maximum table size to %s", + capacity)); + } + setMaxCapacity0(capacity); + } + + private void setMaxCapacity0(int capacity) { + checkEncoding(); + if (capacity < 0) { + throw new IllegalArgumentException("capacity >= 0: " + capacity); + } + int calculated = calculateCapacity(capacity); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("actual maximum table size will be %s", + calculated)); + } + if (calculated < 0 || calculated > capacity) { + throw new IllegalArgumentException( + format("0 <= calculated <= capacity: calculated=%s, capacity=%s", + calculated, capacity)); + } + capacityUpdate = true; + // maxCapacity needs to be updated unconditionally, so the encoder + // always has the newest one (in case it decides to update it later + // unsolicitedly) + // Suppose maxCapacity = 4096, and the encoder has decided to use only + // 2048. It later can choose anything else from the region [0, 4096]. + maxCapacity = capacity; + lastCapacity = calculated; + minCapacity = Math.min(minCapacity, lastCapacity); + } + + /** + * Calculates actual capacity to be used by this encoder in response to + * a request to update maximum table size. + * + *

Default implementation does not add anything to the headers table, + * hence this method returns {@code 0}. + * + *

It is an error to return a value {@code c}, where {@code c < 0} or + * {@code c > maxCapacity}. + * + * @param maxCapacity + * upper bound + * + * @return actual capacity + */ + protected int calculateCapacity(int maxCapacity) { + return 0; + } + + /** + * Encodes the {@linkplain #header(CharSequence, CharSequence) set up} + * header into the given buffer. + * + *

The encoder writes as much as possible of the header's binary + * representation into the given buffer, starting at the buffer's position, + * and increments its position to reflect the bytes written. The buffer's + * mark and limit will not be modified. + * + *

Once the method has returned {@code true}, the current header is + * deemed encoded. A new header may be set up. + * + * @param headerBlock + * the buffer to encode the header into, may be empty + * + * @return {@code true} if the current header has been fully encoded, + * {@code false} otherwise + * + * @throws NullPointerException + * if the buffer is {@code null} + * @throws ReadOnlyBufferException + * if this buffer is read-only + * @throws IllegalStateException + * if there is no set up header + */ + public final boolean encode(ByteBuffer headerBlock) { + if (!encoding) { + throw new IllegalStateException("A header hasn't been set up"); + } + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("writing to %s", headerBlock)); + } + if (!prependWithCapacityUpdate(headerBlock)) { // TODO: log + return false; + } + boolean done = writer.write(headerTable, headerBlock); + if (done) { + writer.reset(); // FIXME: WHY? + encoding = false; + } + return done; + } + + private boolean prependWithCapacityUpdate(ByteBuffer headerBlock) { + if (capacityUpdate) { + if (!configuredCapacityUpdate) { + List sizes = new LinkedList<>(); + if (minCapacity < currCapacity) { + sizes.add((int) minCapacity); + if (minCapacity != lastCapacity) { + sizes.add(lastCapacity); + } + } else if (lastCapacity != currCapacity) { + sizes.add(lastCapacity); + } + bulkSizeUpdateWriter.maxHeaderTableSizes(sizes); + configuredCapacityUpdate = true; + } + boolean done = bulkSizeUpdateWriter.write(headerTable, headerBlock); + if (done) { + minCapacity = lastCapacity; + currCapacity = lastCapacity; + bulkSizeUpdateWriter.reset(); + capacityUpdate = false; + configuredCapacityUpdate = false; + } + return done; + } + return true; + } + + protected final void indexed(int index) throws IndexOutOfBoundsException { + checkEncoding(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("indexed %s", index)); + } + encoding = true; + writer = indexedWriter.index(index); + } + + protected final void literal(int index, + CharSequence value, + boolean useHuffman) + throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')", + index, value)); + } + checkEncoding(); + encoding = true; + writer = literalWriter + .index(index).value(value, useHuffman); + } + + protected final void literal(CharSequence name, + boolean nameHuffman, + CharSequence value, + boolean valueHuffman) { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')", + name, value)); + } + checkEncoding(); + encoding = true; + writer = literalWriter + .name(name, nameHuffman).value(value, valueHuffman); + } + + protected final void literalNeverIndexed(int index, + CharSequence value, + boolean valueHuffman) + throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')", + index, value)); + } + checkEncoding(); + encoding = true; + writer = literalNeverIndexedWriter + .index(index).value(value, valueHuffman); + } + + protected final void literalNeverIndexed(CharSequence name, + boolean nameHuffman, + CharSequence value, + boolean valueHuffman) { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')", + name, value)); + } + checkEncoding(); + encoding = true; + writer = literalNeverIndexedWriter + .name(name, nameHuffman).value(value, valueHuffman); + } + + protected final void literalWithIndexing(int index, + CharSequence value, + boolean valueHuffman) + throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')", + index, value)); + } + checkEncoding(); + encoding = true; + writer = literalWithIndexingWriter + .index(index).value(value, valueHuffman); + } + + protected final void literalWithIndexing(CharSequence name, + boolean nameHuffman, + CharSequence value, + boolean valueHuffman) { + if (logger.isLoggable(EXTRA)) { // TODO: include huffman info? + logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')", + name, value)); + } + checkEncoding(); + encoding = true; + writer = literalWithIndexingWriter + .name(name, nameHuffman).value(value, valueHuffman); + } + + protected final void sizeUpdate(int capacity) + throws IllegalArgumentException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("dynamic table size update %s", + capacity)); + } + checkEncoding(); + // Ensure subclass follows the contract + if (capacity > this.maxCapacity) { + throw new IllegalArgumentException( + format("capacity <= maxCapacity: capacity=%s, maxCapacity=%s", + capacity, maxCapacity)); + } + writer = sizeUpdateWriter.maxHeaderTableSize(capacity); + } + + protected final int getMaxCapacity() { + return maxCapacity; + } + + protected final HeaderTable getHeaderTable() { + return headerTable; + } + + protected final void checkEncoding() { // TODO: better name e.g. checkIfEncodingInProgress() + if (encoding) { + throw new IllegalStateException( + "Previous encoding operation hasn't finished yet"); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/HPACK.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/HPACK.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.hpack.HPACK.Logger.Level; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.function.Supplier; + +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; +import static jdk.internal.net.http.hpack.HPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.hpack.HPACK.Logger.Level.NONE; +import static jdk.internal.net.http.hpack.HPACK.Logger.Level.NORMAL; + +/** + * Internal utilities and stuff. + */ +public final class HPACK { + + private static final RootLogger LOGGER; + private static final Map logLevels = + Map.of("NORMAL", NORMAL, "EXTRA", EXTRA); + + static { + String PROPERTY = "jdk.internal.httpclient.hpack.log.level"; + + String value = AccessController.doPrivileged( + (PrivilegedAction) () -> System.getProperty(PROPERTY)); + + if (value == null) { + LOGGER = new RootLogger(NONE); + } else { + String upperCasedValue = value.toUpperCase(); + Level l = logLevels.get(upperCasedValue); + if (l == null) { + LOGGER = new RootLogger(NONE); + LOGGER.log(System.Logger.Level.INFO, + () -> format("%s value '%s' not recognized (use %s); logging disabled", + PROPERTY, value, logLevels.keySet().stream().collect(joining(", ")))); + } else { + LOGGER = new RootLogger(l); + LOGGER.log(System.Logger.Level.DEBUG, + () -> format("logging level %s", l)); + } + } + } + + public static Logger getLogger() { + return LOGGER; + } + + private HPACK() { } + + /** + * The purpose of this logger is to provide means of diagnosing issues _in + * the HPACK implementation_. It's not a general purpose logger. + */ + // implements System.Logger to make it possible to skip this class + // when looking for the Caller. + public static class Logger implements System.Logger { + + /** + * Log detail level. + */ + public enum Level { + + NONE(0, System.Logger.Level.OFF), + NORMAL(1, System.Logger.Level.DEBUG), + EXTRA(2, System.Logger.Level.TRACE); + + private final int level; + final System.Logger.Level systemLevel; + + Level(int i, System.Logger.Level system) { + level = i; + systemLevel = system; + } + + public final boolean implies(Level other) { + return this.level >= other.level; + } + } + + private final String name; + private final Level level; + private final String path; + private final System.Logger logger; + + private Logger(String path, String name, Level level) { + this(path, name, level, null); + } + + private Logger(String p, String name, Level level, System.Logger logger) { + this.path = p; + this.name = name; + this.level = level; + this.logger = Utils.getHpackLogger(path::toString, level.systemLevel); + } + + public final String getName() { + return name; + } + + @Override + public boolean isLoggable(System.Logger.Level level) { + return logger.isLoggable(level); + } + + @Override + public void log(System.Logger.Level level, ResourceBundle bundle, String msg, Throwable thrown) { + logger.log(level, bundle, msg,thrown); + } + + @Override + public void log(System.Logger.Level level, ResourceBundle bundle, String format, Object... params) { + logger.log(level, bundle, format, params); + } + + /* + * Usual performance trick for logging, reducing performance overhead in + * the case where logging with the specified level is a NOP. + */ + + public boolean isLoggable(Level level) { + return this.level.implies(level); + } + + public void log(Level level, Supplier s) { + if (this.level.implies(level)) { + logger.log(level.systemLevel, s); + } + } + + public Logger subLogger(String name) { + return new Logger(path + "/" + name, name, level); + } + + } + + private static final class RootLogger extends Logger { + + protected RootLogger(Level level) { + super("hpack", "hpack", level); + } + + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/HeaderTable.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/HeaderTable.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,546 @@ +/* + * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import jdk.internal.net.http.hpack.HPACK.Logger; +import jdk.internal.vm.annotation.Stable; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +import static java.lang.String.format; +import static jdk.internal.net.http.hpack.HPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.hpack.HPACK.Logger.Level.NORMAL; + +// +// Header Table combined from two tables: static and dynamic. +// +// There is a single address space for index values. Index-aware methods +// correspond to the table as a whole. Size-aware methods only to the dynamic +// part of it. +// +final class HeaderTable { + + @Stable + private static final HeaderField[] staticTable = { + null, // To make index 1-based, instead of 0-based + new HeaderField(":authority"), + new HeaderField(":method", "GET"), + new HeaderField(":method", "POST"), + new HeaderField(":path", "/"), + new HeaderField(":path", "/index.html"), + new HeaderField(":scheme", "http"), + new HeaderField(":scheme", "https"), + new HeaderField(":status", "200"), + new HeaderField(":status", "204"), + new HeaderField(":status", "206"), + new HeaderField(":status", "304"), + new HeaderField(":status", "400"), + new HeaderField(":status", "404"), + new HeaderField(":status", "500"), + new HeaderField("accept-charset"), + new HeaderField("accept-encoding", "gzip, deflate"), + new HeaderField("accept-language"), + new HeaderField("accept-ranges"), + new HeaderField("accept"), + new HeaderField("access-control-allow-origin"), + new HeaderField("age"), + new HeaderField("allow"), + new HeaderField("authorization"), + new HeaderField("cache-control"), + new HeaderField("content-disposition"), + new HeaderField("content-encoding"), + new HeaderField("content-language"), + new HeaderField("content-length"), + new HeaderField("content-location"), + new HeaderField("content-range"), + new HeaderField("content-type"), + new HeaderField("cookie"), + new HeaderField("date"), + new HeaderField("etag"), + new HeaderField("expect"), + new HeaderField("expires"), + new HeaderField("from"), + new HeaderField("host"), + new HeaderField("if-match"), + new HeaderField("if-modified-since"), + new HeaderField("if-none-match"), + new HeaderField("if-range"), + new HeaderField("if-unmodified-since"), + new HeaderField("last-modified"), + new HeaderField("link"), + new HeaderField("location"), + new HeaderField("max-forwards"), + new HeaderField("proxy-authenticate"), + new HeaderField("proxy-authorization"), + new HeaderField("range"), + new HeaderField("referer"), + new HeaderField("refresh"), + new HeaderField("retry-after"), + new HeaderField("server"), + new HeaderField("set-cookie"), + new HeaderField("strict-transport-security"), + new HeaderField("transfer-encoding"), + new HeaderField("user-agent"), + new HeaderField("vary"), + new HeaderField("via"), + new HeaderField("www-authenticate") + }; + + private static final int STATIC_TABLE_LENGTH = staticTable.length - 1; + private static final int ENTRY_SIZE = 32; + private static final Map> staticIndexes; + + static { + staticIndexes = new HashMap<>(STATIC_TABLE_LENGTH); // TODO: Map.of + for (int i = 1; i <= STATIC_TABLE_LENGTH; i++) { + HeaderField f = staticTable[i]; + Map values = staticIndexes + .computeIfAbsent(f.name, k -> new LinkedHashMap<>()); + values.put(f.value, i); + } + } + + private final Logger logger; + private final Table dynamicTable = new Table(0); + private int maxSize; + private int size; + + public HeaderTable(int maxSize, Logger logger) { + this.logger = logger; + setMaxSize(maxSize); + } + + // + // The method returns: + // + // * a positive integer i where i (i = [1..Integer.MAX_VALUE]) is an + // index of an entry with a header (n, v), where n.equals(name) && + // v.equals(value) + // + // * a negative integer j where j (j = [-Integer.MAX_VALUE..-1]) is an + // index of an entry with a header (n, v), where n.equals(name) + // + // * 0 if there's no entry e such that e.getName().equals(name) + // + // The rationale behind this design is to allow to pack more useful data + // into a single invocation, facilitating a single pass where possible + // (the idea is the same as in java.util.Arrays.binarySearch(int[], int)). + // + public int indexOf(CharSequence name, CharSequence value) { + // Invoking toString() will possibly allocate Strings for the sake of + // the search, which doesn't feel right. + String n = name.toString(); + String v = value.toString(); + + // 1. Try exact match in the static region + Map values = staticIndexes.get(n); + if (values != null) { + Integer idx = values.get(v); + if (idx != null) { + return idx; + } + } + // 2. Try exact match in the dynamic region + int didx = dynamicTable.indexOf(n, v); + if (didx > 0) { + return STATIC_TABLE_LENGTH + didx; + } else if (didx < 0) { + if (values != null) { + // 3. Return name match from the static region + return -values.values().iterator().next(); // Iterator allocation + } else { + // 4. Return name match from the dynamic region + return -STATIC_TABLE_LENGTH + didx; + } + } else { + if (values != null) { + // 3. Return name match from the static region + return -values.values().iterator().next(); // Iterator allocation + } else { + return 0; + } + } + } + + public int size() { + return size; + } + + public int maxSize() { + return maxSize; + } + + public int length() { + return STATIC_TABLE_LENGTH + dynamicTable.size(); + } + + HeaderField get(int index) { + checkIndex(index); + if (index <= STATIC_TABLE_LENGTH) { + return staticTable[index]; + } else { + return dynamicTable.get(index - STATIC_TABLE_LENGTH); + } + } + + void put(CharSequence name, CharSequence value) { + // Invoking toString() will possibly allocate Strings. But that's + // unavoidable at this stage. If a CharSequence is going to be stored in + // the table, it must not be mutable (e.g. for the sake of hashing). + put(new HeaderField(name.toString(), value.toString())); + } + + private void put(HeaderField h) { + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("adding ('%s', '%s')", + h.name, h.value)); + } + int entrySize = sizeOf(h); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("size of ('%s', '%s') is %s", + h.name, h.value, entrySize)); + } + while (entrySize > maxSize - size && size != 0) { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("insufficient space %s, must evict entry", + (maxSize - size))); + } + evictEntry(); + } + if (entrySize > maxSize - size) { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("not adding ('%s, '%s'), too big", + h.name, h.value)); + } + return; + } + size += entrySize; + dynamicTable.add(h); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("('%s, '%s') added", h.name, h.value)); + logger.log(EXTRA, this::toString); + } + } + + void setMaxSize(int maxSize) { + if (maxSize < 0) { + throw new IllegalArgumentException( + "maxSize >= 0: maxSize=" + maxSize); + } + while (maxSize < size && size != 0) { + evictEntry(); + } + this.maxSize = maxSize; + int upperBound = (maxSize / ENTRY_SIZE) + 1; + this.dynamicTable.setCapacity(upperBound); + } + + HeaderField evictEntry() { + HeaderField f = dynamicTable.remove(); + int s = sizeOf(f); + this.size -= s; + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("evicted entry ('%s', '%s') of size %s", + f.name, f.value, s)); + logger.log(EXTRA, this::toString); + } + return f; + } + + @Override + public String toString() { + double used = maxSize == 0 ? 0 : 100 * (((double) size) / maxSize); + return format("dynamic length: %d, full length: %s, used space: %s/%s (%.1f%%)", + dynamicTable.size(), length(), size, maxSize, used); + } + + private int checkIndex(int index) { + int len = length(); + if (index < 1 || index > len) { + throw new IndexOutOfBoundsException( + format("1 <= index <= length(): index=%s, length()=%s", + index, len)); + } + return index; + } + + int sizeOf(HeaderField f) { + return f.name.length() + f.value.length() + ENTRY_SIZE; + } + + // + // Diagnostic information in the form used in the RFC 7541 + // + String getStateString() { + if (size == 0) { + return "empty."; + } + + StringBuilder b = new StringBuilder(); + for (int i = 1, size = dynamicTable.size(); i <= size; i++) { + HeaderField e = dynamicTable.get(i); + b.append(format("[%3d] (s = %3d) %s: %s\n", i, + sizeOf(e), e.name, e.value)); + } + b.append(format(" Table size:%4s", this.size)); + return b.toString(); + } + + // Convert to a Value Object (JDK-8046159)? + static final class HeaderField { + + final String name; + final String value; + + public HeaderField(String name) { + this(name, ""); + } + + public HeaderField(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public String toString() { + return value.isEmpty() ? name : name + ": " + value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + HeaderField that = (HeaderField) o; + return name.equals(that.name) && value.equals(that.value); + } + + @Override + public int hashCode() { + return 31 * name.hashCode() + value.hashCode(); + } + } + + // + // To quickly find an index of an entry in the dynamic table with the given + // contents an effective inverse mapping is needed. Here's a simple idea + // behind such a mapping. + // + // # The problem: + // + // We have a queue with an O(1) lookup by index: + // + // get: index -> x + // + // What we want is an O(1) reverse lookup: + // + // indexOf: x -> index + // + // # Solution: + // + // Let's store an inverse mapping in a Map. This have a problem + // that when a new element is added to the queue, all indexes in the map + // become invalid. Namely, the new element is assigned with an index of 1, + // and each index i, i > 1 becomes shifted by 1 to the left: + // + // 1, 1, 2, 3, ... , n-1, n + // + // Re-establishing the invariant would seem to require a pass through the + // map incrementing all indexes (map values) by 1, which is O(n). + // + // The good news is we can do much better then this! + // + // Let's create a single field of type long, called 'counter'. Then each + // time a new element 'x' is added to the queue, a value of this field gets + // incremented. Then the resulting value of the 'counter_x' is then put as a + // value under key 'x' to the map: + // + // map.put(x, counter_x) + // + // It gives us a map that maps an element to a value the counter had at the + // time the element had been added. + // + // In order to retrieve an index of any element 'x' in the queue (at any + // given time) we simply need to subtract the value (the snapshot of the + // counter at the time when the 'x' was added) from the current value of the + // counter. This operation basically answers the question: + // + // How many elements ago 'x' was the tail of the queue? + // + // Which is the same as its index in the queue now. Given, of course, it's + // still in the queue. + // + // I'm pretty sure in a real life long overflow will never happen, so it's + // not too practical to add recalibrating code, but a pedantic person might + // want to do so: + // + // if (counter == Long.MAX_VALUE) { + // recalibrate(); + // } + // + // Where 'recalibrate()' goes through the table doing this: + // + // value -= counter + // + // That's given, of course, the size of the table itself is less than + // Long.MAX_VALUE :-) + // + private static final class Table { + + private final Map> map; + private final CircularBuffer buffer; + private long counter = 1; + + Table(int capacity) { + buffer = new CircularBuffer<>(capacity); + map = new HashMap<>(capacity); + } + + void add(HeaderField f) { + buffer.add(f); + Map values = map.computeIfAbsent(f.name, k -> new HashMap<>()); + values.put(f.value, counter++); + } + + HeaderField get(int index) { + return buffer.get(index - 1); + } + + int indexOf(String name, String value) { + Map values = map.get(name); + if (values == null) { + return 0; + } + Long index = values.get(value); + if (index != null) { + return (int) (counter - index); + } else { + assert !values.isEmpty(); + Long any = values.values().iterator().next(); // Iterator allocation + return -(int) (counter - any); + } + } + + HeaderField remove() { + HeaderField f = buffer.remove(); + Map values = map.get(f.name); + Long index = values.remove(f.value); + assert index != null; + if (values.isEmpty()) { + map.remove(f.name); + } + return f; + } + + int size() { + return buffer.size; + } + + public void setCapacity(int capacity) { + buffer.resize(capacity); + } + } + + // head + // v + // [ ][ ][A][B][C][D][ ][ ][ ] + // ^ + // tail + // + // |<- size ->| (4) + // |<------ capacity ------->| (9) + // + static final class CircularBuffer { + + int tail, head, size, capacity; + Object[] elements; + + CircularBuffer(int capacity) { + this.capacity = capacity; + elements = new Object[capacity]; + } + + void add(E elem) { + if (size == capacity) { + throw new IllegalStateException( + format("No room for '%s': capacity=%s", elem, capacity)); + } + elements[head] = elem; + head = (head + 1) % capacity; + size++; + } + + @SuppressWarnings("unchecked") + E remove() { + if (size == 0) { + throw new NoSuchElementException("Empty"); + } + E elem = (E) elements[tail]; + elements[tail] = null; + tail = (tail + 1) % capacity; + size--; + return elem; + } + + @SuppressWarnings("unchecked") + E get(int index) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException( + format("0 <= index <= capacity: index=%s, capacity=%s", + index, capacity)); + } + int idx = (tail + (size - index - 1)) % capacity; + return (E) elements[idx]; + } + + public void resize(int newCapacity) { + if (newCapacity < size) { + throw new IllegalStateException( + format("newCapacity >= size: newCapacity=%s, size=%s", + newCapacity, size)); + } + + Object[] newElements = new Object[newCapacity]; + + if (tail < head || size == 0) { + System.arraycopy(elements, tail, newElements, 0, size); + } else { + System.arraycopy(elements, tail, newElements, 0, elements.length - tail); + System.arraycopy(elements, 0, newElements, elements.length - tail, head); + } + + elements = newElements; + tail = 0; + head = size; + this.capacity = newCapacity; + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/Huffman.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Huffman.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,681 @@ +/* + * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import static java.lang.String.format; + +/** + * Huffman coding table. + * + *

Instances of this class are safe for use by multiple threads. + * + * @since 9 + */ +public final class Huffman { + + // TODO: check if reset is done in both reader and writer + + static final class Reader { + + private Node curr; // position in the trie + private int len; // length of the path from the root to 'curr' + private int p; // byte probe + + { + reset(); + } + + public void read(ByteBuffer source, + Appendable destination, + boolean isLast) throws IOException { + read(source, destination, true, isLast); + } + + // Takes 'isLast' rather than returns whether the reading is done or + // not, for more informative exceptions. + void read(ByteBuffer source, + Appendable destination, + boolean reportEOS, /* reportEOS is exposed for tests */ + boolean isLast) throws IOException { + Node c = curr; + int l = len; + /* + Since ByteBuffer is itself stateful, its position is + remembered here NOT as a part of Reader's state, + but to set it back in the case of a failure + */ + int pos = source.position(); + + while (source.hasRemaining()) { + int d = source.get(); + for (; p != 0; p >>= 1) { + c = c.getChild(p & d); + l++; + if (c.isLeaf()) { + if (reportEOS && c.isEOSPath) { + throw new IOException("Encountered EOS"); + } + char ch; + try { + ch = c.getChar(); + } catch (IllegalStateException e) { + source.position(pos); // do we need this? + throw new IOException(e); + } + try { + destination.append(ch); + } catch (IOException e) { + source.position(pos); // do we need this? + throw e; + } + c = INSTANCE.root; + l = 0; + } + curr = c; + len = l; + } + resetProbe(); + pos++; + } + if (!isLast) { + return; // it's too early to jump to any conclusions, let's wait + } + if (c.isLeaf()) { + return; // it's perfectly ok, no extra padding bits + } + if (c.isEOSPath && len <= 7) { + return; // it's ok, some extra padding bits + } + if (c.isEOSPath) { + throw new IOException( + "Padding is too long (len=" + len + ") " + + "or unexpected end of data"); + } + throw new IOException( + "Not a EOS prefix padding or unexpected end of data"); + } + + public void reset() { + curr = INSTANCE.root; + len = 0; + resetProbe(); + } + + private void resetProbe() { + p = 0x80; + } + } + + static final class Writer { + + private int pos; // position in 'source' + private int avail = 8; // number of least significant bits available in 'curr' + private int curr; // next byte to put to the destination + private int rem; // number of least significant bits in 'code' yet to be processed + private int code; // current code being written + + private CharSequence source; + private int end; + + public Writer from(CharSequence input, int start, int end) { + if (start < 0 || end < 0 || end > input.length() || start > end) { + throw new IndexOutOfBoundsException( + String.format("input.length()=%s, start=%s, end=%s", + input.length(), start, end)); + } + pos = start; + this.end = end; + this.source = input; + return this; + } + + public boolean write(ByteBuffer destination) { + for (; pos < end; pos++) { + if (rem == 0) { + Code desc = INSTANCE.codeOf(source.charAt(pos)); + rem = desc.length; + code = desc.code; + } + while (rem > 0) { + if (rem < avail) { + curr |= (code << (avail - rem)); + avail -= rem; + rem = 0; + } else { + int c = (curr | (code >>> (rem - avail))); + if (destination.hasRemaining()) { + destination.put((byte) c); + } else { + return false; + } + curr = c; + code <<= (32 - rem + avail); // throw written bits off the cliff (is this Sparta?) + code >>>= (32 - rem + avail); // return to the position + rem -= avail; + curr = 0; + avail = 8; + } + } + } + + if (avail < 8) { // have to pad + if (destination.hasRemaining()) { + destination.put((byte) (curr | (INSTANCE.EOS.code >>> (INSTANCE.EOS.length - avail)))); + avail = 8; + } else { + return false; + } + } + + return true; + } + + public Writer reset() { + source = null; + end = -1; + pos = -1; + avail = 8; + curr = 0; + code = 0; + return this; + } + } + + /** + * Shared instance. + */ + public static final Huffman INSTANCE = new Huffman(); + + private final Code EOS = new Code(0x3fffffff, 30); + private final Code[] codes = new Code[257]; + private final Node root = new Node() { + @Override + public String toString() { return "root"; } + }; + + // TODO: consider builder and immutable trie + private Huffman() { + // @formatter:off + addChar(0, 0x1ff8, 13); + addChar(1, 0x7fffd8, 23); + addChar(2, 0xfffffe2, 28); + addChar(3, 0xfffffe3, 28); + addChar(4, 0xfffffe4, 28); + addChar(5, 0xfffffe5, 28); + addChar(6, 0xfffffe6, 28); + addChar(7, 0xfffffe7, 28); + addChar(8, 0xfffffe8, 28); + addChar(9, 0xffffea, 24); + addChar(10, 0x3ffffffc, 30); + addChar(11, 0xfffffe9, 28); + addChar(12, 0xfffffea, 28); + addChar(13, 0x3ffffffd, 30); + addChar(14, 0xfffffeb, 28); + addChar(15, 0xfffffec, 28); + addChar(16, 0xfffffed, 28); + addChar(17, 0xfffffee, 28); + addChar(18, 0xfffffef, 28); + addChar(19, 0xffffff0, 28); + addChar(20, 0xffffff1, 28); + addChar(21, 0xffffff2, 28); + addChar(22, 0x3ffffffe, 30); + addChar(23, 0xffffff3, 28); + addChar(24, 0xffffff4, 28); + addChar(25, 0xffffff5, 28); + addChar(26, 0xffffff6, 28); + addChar(27, 0xffffff7, 28); + addChar(28, 0xffffff8, 28); + addChar(29, 0xffffff9, 28); + addChar(30, 0xffffffa, 28); + addChar(31, 0xffffffb, 28); + addChar(32, 0x14, 6); + addChar(33, 0x3f8, 10); + addChar(34, 0x3f9, 10); + addChar(35, 0xffa, 12); + addChar(36, 0x1ff9, 13); + addChar(37, 0x15, 6); + addChar(38, 0xf8, 8); + addChar(39, 0x7fa, 11); + addChar(40, 0x3fa, 10); + addChar(41, 0x3fb, 10); + addChar(42, 0xf9, 8); + addChar(43, 0x7fb, 11); + addChar(44, 0xfa, 8); + addChar(45, 0x16, 6); + addChar(46, 0x17, 6); + addChar(47, 0x18, 6); + addChar(48, 0x0, 5); + addChar(49, 0x1, 5); + addChar(50, 0x2, 5); + addChar(51, 0x19, 6); + addChar(52, 0x1a, 6); + addChar(53, 0x1b, 6); + addChar(54, 0x1c, 6); + addChar(55, 0x1d, 6); + addChar(56, 0x1e, 6); + addChar(57, 0x1f, 6); + addChar(58, 0x5c, 7); + addChar(59, 0xfb, 8); + addChar(60, 0x7ffc, 15); + addChar(61, 0x20, 6); + addChar(62, 0xffb, 12); + addChar(63, 0x3fc, 10); + addChar(64, 0x1ffa, 13); + addChar(65, 0x21, 6); + addChar(66, 0x5d, 7); + addChar(67, 0x5e, 7); + addChar(68, 0x5f, 7); + addChar(69, 0x60, 7); + addChar(70, 0x61, 7); + addChar(71, 0x62, 7); + addChar(72, 0x63, 7); + addChar(73, 0x64, 7); + addChar(74, 0x65, 7); + addChar(75, 0x66, 7); + addChar(76, 0x67, 7); + addChar(77, 0x68, 7); + addChar(78, 0x69, 7); + addChar(79, 0x6a, 7); + addChar(80, 0x6b, 7); + addChar(81, 0x6c, 7); + addChar(82, 0x6d, 7); + addChar(83, 0x6e, 7); + addChar(84, 0x6f, 7); + addChar(85, 0x70, 7); + addChar(86, 0x71, 7); + addChar(87, 0x72, 7); + addChar(88, 0xfc, 8); + addChar(89, 0x73, 7); + addChar(90, 0xfd, 8); + addChar(91, 0x1ffb, 13); + addChar(92, 0x7fff0, 19); + addChar(93, 0x1ffc, 13); + addChar(94, 0x3ffc, 14); + addChar(95, 0x22, 6); + addChar(96, 0x7ffd, 15); + addChar(97, 0x3, 5); + addChar(98, 0x23, 6); + addChar(99, 0x4, 5); + addChar(100, 0x24, 6); + addChar(101, 0x5, 5); + addChar(102, 0x25, 6); + addChar(103, 0x26, 6); + addChar(104, 0x27, 6); + addChar(105, 0x6, 5); + addChar(106, 0x74, 7); + addChar(107, 0x75, 7); + addChar(108, 0x28, 6); + addChar(109, 0x29, 6); + addChar(110, 0x2a, 6); + addChar(111, 0x7, 5); + addChar(112, 0x2b, 6); + addChar(113, 0x76, 7); + addChar(114, 0x2c, 6); + addChar(115, 0x8, 5); + addChar(116, 0x9, 5); + addChar(117, 0x2d, 6); + addChar(118, 0x77, 7); + addChar(119, 0x78, 7); + addChar(120, 0x79, 7); + addChar(121, 0x7a, 7); + addChar(122, 0x7b, 7); + addChar(123, 0x7ffe, 15); + addChar(124, 0x7fc, 11); + addChar(125, 0x3ffd, 14); + addChar(126, 0x1ffd, 13); + addChar(127, 0xffffffc, 28); + addChar(128, 0xfffe6, 20); + addChar(129, 0x3fffd2, 22); + addChar(130, 0xfffe7, 20); + addChar(131, 0xfffe8, 20); + addChar(132, 0x3fffd3, 22); + addChar(133, 0x3fffd4, 22); + addChar(134, 0x3fffd5, 22); + addChar(135, 0x7fffd9, 23); + addChar(136, 0x3fffd6, 22); + addChar(137, 0x7fffda, 23); + addChar(138, 0x7fffdb, 23); + addChar(139, 0x7fffdc, 23); + addChar(140, 0x7fffdd, 23); + addChar(141, 0x7fffde, 23); + addChar(142, 0xffffeb, 24); + addChar(143, 0x7fffdf, 23); + addChar(144, 0xffffec, 24); + addChar(145, 0xffffed, 24); + addChar(146, 0x3fffd7, 22); + addChar(147, 0x7fffe0, 23); + addChar(148, 0xffffee, 24); + addChar(149, 0x7fffe1, 23); + addChar(150, 0x7fffe2, 23); + addChar(151, 0x7fffe3, 23); + addChar(152, 0x7fffe4, 23); + addChar(153, 0x1fffdc, 21); + addChar(154, 0x3fffd8, 22); + addChar(155, 0x7fffe5, 23); + addChar(156, 0x3fffd9, 22); + addChar(157, 0x7fffe6, 23); + addChar(158, 0x7fffe7, 23); + addChar(159, 0xffffef, 24); + addChar(160, 0x3fffda, 22); + addChar(161, 0x1fffdd, 21); + addChar(162, 0xfffe9, 20); + addChar(163, 0x3fffdb, 22); + addChar(164, 0x3fffdc, 22); + addChar(165, 0x7fffe8, 23); + addChar(166, 0x7fffe9, 23); + addChar(167, 0x1fffde, 21); + addChar(168, 0x7fffea, 23); + addChar(169, 0x3fffdd, 22); + addChar(170, 0x3fffde, 22); + addChar(171, 0xfffff0, 24); + addChar(172, 0x1fffdf, 21); + addChar(173, 0x3fffdf, 22); + addChar(174, 0x7fffeb, 23); + addChar(175, 0x7fffec, 23); + addChar(176, 0x1fffe0, 21); + addChar(177, 0x1fffe1, 21); + addChar(178, 0x3fffe0, 22); + addChar(179, 0x1fffe2, 21); + addChar(180, 0x7fffed, 23); + addChar(181, 0x3fffe1, 22); + addChar(182, 0x7fffee, 23); + addChar(183, 0x7fffef, 23); + addChar(184, 0xfffea, 20); + addChar(185, 0x3fffe2, 22); + addChar(186, 0x3fffe3, 22); + addChar(187, 0x3fffe4, 22); + addChar(188, 0x7ffff0, 23); + addChar(189, 0x3fffe5, 22); + addChar(190, 0x3fffe6, 22); + addChar(191, 0x7ffff1, 23); + addChar(192, 0x3ffffe0, 26); + addChar(193, 0x3ffffe1, 26); + addChar(194, 0xfffeb, 20); + addChar(195, 0x7fff1, 19); + addChar(196, 0x3fffe7, 22); + addChar(197, 0x7ffff2, 23); + addChar(198, 0x3fffe8, 22); + addChar(199, 0x1ffffec, 25); + addChar(200, 0x3ffffe2, 26); + addChar(201, 0x3ffffe3, 26); + addChar(202, 0x3ffffe4, 26); + addChar(203, 0x7ffffde, 27); + addChar(204, 0x7ffffdf, 27); + addChar(205, 0x3ffffe5, 26); + addChar(206, 0xfffff1, 24); + addChar(207, 0x1ffffed, 25); + addChar(208, 0x7fff2, 19); + addChar(209, 0x1fffe3, 21); + addChar(210, 0x3ffffe6, 26); + addChar(211, 0x7ffffe0, 27); + addChar(212, 0x7ffffe1, 27); + addChar(213, 0x3ffffe7, 26); + addChar(214, 0x7ffffe2, 27); + addChar(215, 0xfffff2, 24); + addChar(216, 0x1fffe4, 21); + addChar(217, 0x1fffe5, 21); + addChar(218, 0x3ffffe8, 26); + addChar(219, 0x3ffffe9, 26); + addChar(220, 0xffffffd, 28); + addChar(221, 0x7ffffe3, 27); + addChar(222, 0x7ffffe4, 27); + addChar(223, 0x7ffffe5, 27); + addChar(224, 0xfffec, 20); + addChar(225, 0xfffff3, 24); + addChar(226, 0xfffed, 20); + addChar(227, 0x1fffe6, 21); + addChar(228, 0x3fffe9, 22); + addChar(229, 0x1fffe7, 21); + addChar(230, 0x1fffe8, 21); + addChar(231, 0x7ffff3, 23); + addChar(232, 0x3fffea, 22); + addChar(233, 0x3fffeb, 22); + addChar(234, 0x1ffffee, 25); + addChar(235, 0x1ffffef, 25); + addChar(236, 0xfffff4, 24); + addChar(237, 0xfffff5, 24); + addChar(238, 0x3ffffea, 26); + addChar(239, 0x7ffff4, 23); + addChar(240, 0x3ffffeb, 26); + addChar(241, 0x7ffffe6, 27); + addChar(242, 0x3ffffec, 26); + addChar(243, 0x3ffffed, 26); + addChar(244, 0x7ffffe7, 27); + addChar(245, 0x7ffffe8, 27); + addChar(246, 0x7ffffe9, 27); + addChar(247, 0x7ffffea, 27); + addChar(248, 0x7ffffeb, 27); + addChar(249, 0xffffffe, 28); + addChar(250, 0x7ffffec, 27); + addChar(251, 0x7ffffed, 27); + addChar(252, 0x7ffffee, 27); + addChar(253, 0x7ffffef, 27); + addChar(254, 0x7fffff0, 27); + addChar(255, 0x3ffffee, 26); + addEOS (256, EOS.code, EOS.length); + // @formatter:on + } + + + /** + * Calculates the number of bytes required to represent the given {@code + * CharSequence} with the Huffman coding. + * + * @param value + * characters + * + * @return number of bytes + * + * @throws NullPointerException + * if the value is null + */ + public int lengthOf(CharSequence value) { + return lengthOf(value, 0, value.length()); + } + + /** + * Calculates the number of bytes required to represent a subsequence of the + * given {@code CharSequence} with the Huffman coding. + * + * @param value + * characters + * @param start + * the start index, inclusive + * @param end + * the end index, exclusive + * + * @return number of bytes + * + * @throws NullPointerException + * if the value is null + * @throws IndexOutOfBoundsException + * if any invocation of {@code value.charAt(i)}, where + * {@code start <= i < end} would throw an IndexOutOfBoundsException + */ + public int lengthOf(CharSequence value, int start, int end) { + int len = 0; + for (int i = start; i < end; i++) { + char c = value.charAt(i); + len += INSTANCE.codeOf(c).length; + } + // Integer division with ceiling, assumption: + assert (len / 8 + (len % 8 != 0 ? 1 : 0)) == (len + 7) / 8 : len; + return (len + 7) / 8; + } + + private void addChar(int c, int code, int bitLength) { + addLeaf(c, code, bitLength, false); + codes[c] = new Code(code, bitLength); + } + + private void addEOS(int c, int code, int bitLength) { + addLeaf(c, code, bitLength, true); + codes[c] = new Code(code, bitLength); + } + + private void addLeaf(int c, int code, int bitLength, boolean isEOS) { + if (bitLength < 1) { + throw new IllegalArgumentException("bitLength < 1"); + } + Node curr = root; + for (int p = 1 << bitLength - 1; p != 0 && !curr.isLeaf(); p = p >> 1) { + curr.isEOSPath |= isEOS; // If it's already true, it can't become false + curr = curr.addChildIfAbsent(p & code); + } + curr.isEOSPath |= isEOS; // The last one needs to have this property as well + if (curr.isLeaf()) { + throw new IllegalStateException("Specified code is already taken"); + } + curr.setChar((char) c); + } + + private Code codeOf(char c) { + if (c > 255) { + throw new IllegalArgumentException("char=" + ((int) c)); + } + return codes[c]; + } + + // + // For debugging/testing purposes + // + Node getRoot() { + return root; + } + + // + // Guarantees: + // + // if (isLeaf() == true) => getChar() is a legal call + // if (isLeaf() == false) => getChild(i) is a legal call (though it can + // return null) + // + static class Node { + + Node left; + Node right; + boolean isEOSPath; + + boolean charIsSet; + char c; + + Node getChild(int selector) { + if (isLeaf()) { + throw new IllegalStateException("This is a leaf node"); + } + Node result = selector == 0 ? left : right; + if (result == null) { + throw new IllegalStateException(format( + "Node doesn't have a child (selector=%s)", selector)); + } + return result; + } + + boolean isLeaf() { + return charIsSet; + } + + char getChar() { + if (!isLeaf()) { + throw new IllegalStateException("This node is not a leaf node"); + } + return c; + } + + void setChar(char c) { + if (charIsSet) { + throw new IllegalStateException( + "This node has been taken already"); + } + if (left != null || right != null) { + throw new IllegalStateException("The node cannot be made " + + "a leaf as it's already has a child"); + } + this.c = c; + charIsSet = true; + } + + Node addChildIfAbsent(int i) { + if (charIsSet) { + throw new IllegalStateException("The node cannot have a child " + + "as it's already a leaf node"); + } + Node child; + if (i == 0) { + if ((child = left) == null) { + child = left = new Node(); + } + } else { + if ((child = right) == null) { + child = right = new Node(); + } + } + return child; + } + + @Override + public String toString() { + if (isLeaf()) { + if (isEOSPath) { + return "EOS"; + } else { + return format("char: (%3s) '%s'", (int) c, c); + } + } + return "/\\"; + } + } + + // TODO: value-based class? + // FIXME: can we re-use Node instead of this class? + private static final class Code { + + final int code; + final int length; + + private Code(int code, int length) { + this.code = code; + this.length = length; + } + + public int getCode() { + return code; + } + + public int getLength() { + return length; + } + + @Override + public String toString() { + long p = 1 << length; + return Long.toBinaryString(code + p).substring(1) + + ", length=" + length; + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import java.io.IOException; +import java.nio.ByteBuffer; + +// +// Custom implementation of ISO/IEC 8859-1:1998 +// +// The rationale behind this is not to deal with CharsetEncoder/CharsetDecoder, +// basically because it would require wrapping every single CharSequence into a +// CharBuffer and then copying it back. +// +// But why not to give a CharBuffer instead of Appendable? Because I can choose +// an Appendable (e.g. StringBuilder) that adjusts its length when needed and +// therefore not to deal with pre-sized CharBuffers or copying. +// +// The encoding is simple and well known: 1 byte <-> 1 char +// +final class ISO_8859_1 { + + private ISO_8859_1() { } + + public static final class Reader { + + public void read(ByteBuffer source, Appendable destination) + throws IOException { + for (int i = 0, len = source.remaining(); i < len; i++) { + char c = (char) (source.get() & 0xff); + try { + destination.append(c); + } catch (IOException e) { + throw new IOException( + "Error appending to the destination", e); + } + } + } + + public Reader reset() { + return this; + } + } + + public static final class Writer { + + private CharSequence source; + private int pos; + private int end; + + public Writer configure(CharSequence source, int start, int end) { + this.source = source; + this.pos = start; + this.end = end; + return this; + } + + public boolean write(ByteBuffer destination) { + for (; pos < end; pos++) { + char c = source.charAt(pos); + if (c > '\u00FF') { + throw new IllegalArgumentException( + "Illegal ISO-8859-1 char: " + (int) c); + } + if (destination.hasRemaining()) { + destination.put((byte) c); + } else { + return false; + } + } + return true; + } + + public Writer reset() { + source = null; + pos = -1; + end = -1; + return this; + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/IndexNameValueWriter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/IndexNameValueWriter.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import java.nio.ByteBuffer; + +abstract class IndexNameValueWriter implements BinaryRepresentationWriter { + + private final int pattern; + private final int prefix; + private final IntegerWriter intWriter = new IntegerWriter(); + private final StringWriter nameWriter = new StringWriter(); + private final StringWriter valueWriter = new StringWriter(); + + protected boolean indexedRepresentation; + + private static final int NEW = 0; + private static final int NAME_PART_WRITTEN = 1; + private static final int VALUE_WRITTEN = 2; + + private int state = NEW; + + protected IndexNameValueWriter(int pattern, int prefix) { + this.pattern = pattern; + this.prefix = prefix; + } + + IndexNameValueWriter index(int index) { + indexedRepresentation = true; + intWriter.configure(index, prefix, pattern); + return this; + } + + IndexNameValueWriter name(CharSequence name, boolean useHuffman) { + indexedRepresentation = false; + intWriter.configure(0, prefix, pattern); + nameWriter.configure(name, useHuffman); + return this; + } + + IndexNameValueWriter value(CharSequence value, boolean useHuffman) { + valueWriter.configure(value, useHuffman); + return this; + } + + @Override + public boolean write(HeaderTable table, ByteBuffer destination) { + if (state < NAME_PART_WRITTEN) { + if (indexedRepresentation) { + if (!intWriter.write(destination)) { + return false; + } + } else { + if (!intWriter.write(destination) || + !nameWriter.write(destination)) { + return false; + } + } + state = NAME_PART_WRITTEN; + } + if (state < VALUE_WRITTEN) { + if (!valueWriter.write(destination)) { + return false; + } + state = VALUE_WRITTEN; + } + return state == VALUE_WRITTEN; + } + + @Override + public IndexNameValueWriter reset() { + intWriter.reset(); + if (!indexedRepresentation) { + nameWriter.reset(); + } + valueWriter.reset(); + state = NEW; + return this; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/IndexedWriter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/IndexedWriter.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import java.nio.ByteBuffer; + +final class IndexedWriter implements BinaryRepresentationWriter { + + private final IntegerWriter intWriter = new IntegerWriter(); + + IndexedWriter() { } + + IndexedWriter index(int index) { + intWriter.configure(index, 7, 0b1000_0000); + return this; + } + + @Override + public boolean write(HeaderTable table, ByteBuffer destination) { + return intWriter.write(destination); + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + return this; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/IntegerReader.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/IntegerReader.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static java.lang.String.format; + +final class IntegerReader { + + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int FIRST_BYTE_READ = 2; + private static final int DONE = 4; + + private int state = NEW; + + private int N; + private int maxValue; + private int value; + private long r; + private long b = 1; + + public IntegerReader configure(int N) { + return configure(N, Integer.MAX_VALUE); + } + + // + // Why is it important to configure 'maxValue' here. After all we can wait + // for the integer to be fully read and then check it. Can't we? + // + // Two reasons. + // + // 1. Value wraps around long won't be unnoticed. + // 2. It can spit out an exception as soon as it becomes clear there's + // an overflow. Therefore, no need to wait for the value to be fully read. + // + public IntegerReader configure(int N, int maxValue) { + if (state != NEW) { + throw new IllegalStateException("Already configured"); + } + checkPrefix(N); + if (maxValue < 0) { + throw new IllegalArgumentException( + "maxValue >= 0: maxValue=" + maxValue); + } + this.maxValue = maxValue; + this.N = N; + state = CONFIGURED; + return this; + } + + public boolean read(ByteBuffer input) throws IOException { + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (state == DONE) { + return true; + } + if (!input.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + int max = (2 << (N - 1)) - 1; + int n = input.get() & max; + if (n != max) { + value = n; + state = DONE; + return true; + } else { + r = max; + } + state = FIRST_BYTE_READ; + } + if (state == FIRST_BYTE_READ) { + // variable-length quantity (VLQ) + byte i; + do { + if (!input.hasRemaining()) { + return false; + } + i = input.get(); + long increment = b * (i & 127); + if (r + increment > maxValue) { + throw new IOException(format( + "Integer overflow: maxValue=%,d, value=%,d", + maxValue, r + increment)); + } + r += increment; + b *= 128; + } while ((128 & i) == 128); + + value = (int) r; + state = DONE; + return true; + } + throw new InternalError(Arrays.toString( + new Object[]{state, N, maxValue, value, r, b})); + } + + public int get() throws IllegalStateException { + if (state != DONE) { + throw new IllegalStateException("Has not been fully read yet"); + } + return value; + } + + private static void checkPrefix(int N) { + if (N < 1 || N > 8) { + throw new IllegalArgumentException("1 <= N <= 8: N= " + N); + } + } + + public IntegerReader reset() { + b = 1; + state = NEW; + return this; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/IntegerWriter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/IntegerWriter.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +final class IntegerWriter { + + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int FIRST_BYTE_WRITTEN = 2; + private static final int DONE = 4; + + private int state = NEW; + + private int payload; + private int N; + private int value; + + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | | | | | | | | | + // +---+---+---+-------------------+ + // |<--------->|<----------------->| + // payload N=5 + // + // payload is the contents of the left-hand side part of the octet; + // it is truncated to fit into 8-N bits, where 1 <= N <= 8; + // + public IntegerWriter configure(int value, int N, int payload) { + if (state != NEW) { + throw new IllegalStateException("Already configured"); + } + if (value < 0) { + throw new IllegalArgumentException("value >= 0: value=" + value); + } + checkPrefix(N); + this.value = value; + this.N = N; + this.payload = payload & 0xFF & (0xFFFFFFFF << N); + state = CONFIGURED; + return this; + } + + public boolean write(ByteBuffer output) { + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (state == DONE) { + return true; + } + + if (!output.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + int max = (2 << (N - 1)) - 1; + if (value < max) { + output.put((byte) (payload | value)); + state = DONE; + return true; + } + output.put((byte) (payload | max)); + value -= max; + state = FIRST_BYTE_WRITTEN; + } + if (state == FIRST_BYTE_WRITTEN) { + while (value >= 128 && output.hasRemaining()) { + output.put((byte) (value % 128 + 128)); + value /= 128; + } + if (!output.hasRemaining()) { + return false; + } + output.put((byte) value); + state = DONE; + return true; + } + throw new InternalError(Arrays.toString( + new Object[]{state, payload, N, value})); + } + + private static void checkPrefix(int N) { + if (N < 1 || N > 8) { + throw new IllegalArgumentException("1 <= N <= 8: N= " + N); + } + } + + public IntegerWriter reset() { + state = NEW; + return this; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/LiteralNeverIndexedWriter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/LiteralNeverIndexedWriter.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +final class LiteralNeverIndexedWriter extends IndexNameValueWriter { + + LiteralNeverIndexedWriter() { + super(0b0001_0000, 4); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/LiteralWithIndexingWriter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/LiteralWithIndexingWriter.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import java.nio.ByteBuffer; + +final class LiteralWithIndexingWriter extends IndexNameValueWriter { + + private boolean tableUpdated; + + private CharSequence name; + private CharSequence value; + private int index; + + LiteralWithIndexingWriter() { + super(0b0100_0000, 6); + } + + @Override + LiteralWithIndexingWriter index(int index) { + super.index(index); + this.index = index; + return this; + } + + @Override + LiteralWithIndexingWriter name(CharSequence name, boolean useHuffman) { + super.name(name, useHuffman); + this.name = name; + return this; + } + + @Override + LiteralWithIndexingWriter value(CharSequence value, boolean useHuffman) { + super.value(value, useHuffman); + this.value = value; + return this; + } + + @Override + public boolean write(HeaderTable table, ByteBuffer destination) { + if (!tableUpdated) { + CharSequence n; + if (indexedRepresentation) { + n = table.get(index).name; + } else { + n = name; + } + table.put(n, value); + tableUpdated = true; + } + return super.write(table, destination); + } + + @Override + public IndexNameValueWriter reset() { + tableUpdated = false; + name = null; + value = null; + index = -1; + return super.reset(); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/LiteralWriter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/LiteralWriter.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +final class LiteralWriter extends IndexNameValueWriter { + + LiteralWriter() { + super(0b0000_0000, 4); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/SizeUpdateWriter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/SizeUpdateWriter.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import java.nio.ByteBuffer; + +final class SizeUpdateWriter implements BinaryRepresentationWriter { + + private final IntegerWriter intWriter = new IntegerWriter(); + private int maxSize; + private boolean tableUpdated; + + SizeUpdateWriter() { } + + SizeUpdateWriter maxHeaderTableSize(int size) { + intWriter.configure(size, 5, 0b0010_0000); + this.maxSize = size; + return this; + } + + @Override + public boolean write(HeaderTable table, ByteBuffer destination) { + if (!tableUpdated) { + table.setMaxSize(maxSize); + tableUpdated = true; + } + return intWriter.write(destination); + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + maxSize = -1; + tableUpdated = false; + return this; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/StringReader.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/StringReader.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | H | String Length (7+) | +// +---+---------------------------+ +// | String Data (Length octets) | +// +-------------------------------+ +// +final class StringReader { + + private static final int NEW = 0; + private static final int FIRST_BYTE_READ = 1; + private static final int LENGTH_READ = 2; + private static final int DONE = 4; + + private final IntegerReader intReader = new IntegerReader(); + private final Huffman.Reader huffmanReader = new Huffman.Reader(); + private final ISO_8859_1.Reader plainReader = new ISO_8859_1.Reader(); + + private int state = NEW; + + private boolean huffman; + private int remainingLength; + + boolean read(ByteBuffer input, Appendable output) throws IOException { + if (state == DONE) { + return true; + } + if (!input.hasRemaining()) { + return false; + } + if (state == NEW) { + int p = input.position(); + huffman = (input.get(p) & 0b10000000) != 0; + state = FIRST_BYTE_READ; + intReader.configure(7); + } + if (state == FIRST_BYTE_READ) { + boolean lengthRead = intReader.read(input); + if (!lengthRead) { + return false; + } + remainingLength = intReader.get(); + state = LENGTH_READ; + } + if (state == LENGTH_READ) { + boolean isLast = input.remaining() >= remainingLength; + int oldLimit = input.limit(); + if (isLast) { + input.limit(input.position() + remainingLength); + } + remainingLength -= Math.min(input.remaining(), remainingLength); + if (huffman) { + huffmanReader.read(input, output, isLast); + } else { + plainReader.read(input, output); + } + if (isLast) { + input.limit(oldLimit); + state = DONE; + } + return isLast; + } + throw new InternalError(Arrays.toString( + new Object[]{state, huffman, remainingLength})); + } + + boolean isHuffmanEncoded() { + if (state < FIRST_BYTE_READ) { + throw new IllegalStateException("Has not been fully read yet"); + } + return huffman; + } + + void reset() { + if (huffman) { + huffmanReader.reset(); + } else { + plainReader.reset(); + } + intReader.reset(); + state = NEW; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/StringWriter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/StringWriter.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | H | String Length (7+) | +// +---+---------------------------+ +// | String Data (Length octets) | +// +-------------------------------+ +// +// StringWriter does not require a notion of endOfInput (isLast) in 'write' +// methods due to the nature of string representation in HPACK. Namely, the +// length of the string is put before string's contents. Therefore the length is +// always known beforehand. +// +// Expected use: +// +// configure write* (reset configure write*)* +// +final class StringWriter { + + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int LENGTH_WRITTEN = 2; + private static final int DONE = 4; + + private final IntegerWriter intWriter = new IntegerWriter(); + private final Huffman.Writer huffmanWriter = new Huffman.Writer(); + private final ISO_8859_1.Writer plainWriter = new ISO_8859_1.Writer(); + + private int state = NEW; + private boolean huffman; + + StringWriter configure(CharSequence input, boolean huffman) { + return configure(input, 0, input.length(), huffman); + } + + StringWriter configure(CharSequence input, + int start, + int end, + boolean huffman) { + if (start < 0 || end < 0 || end > input.length() || start > end) { + throw new IndexOutOfBoundsException( + String.format("input.length()=%s, start=%s, end=%s", + input.length(), start, end)); + } + if (!huffman) { + plainWriter.configure(input, start, end); + intWriter.configure(end - start, 7, 0b0000_0000); + } else { + huffmanWriter.from(input, start, end); + intWriter.configure(Huffman.INSTANCE.lengthOf(input, start, end), + 7, 0b1000_0000); + } + + this.huffman = huffman; + state = CONFIGURED; + return this; + } + + boolean write(ByteBuffer output) { + if (state == DONE) { + return true; + } + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (!output.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + if (intWriter.write(output)) { + state = LENGTH_WRITTEN; + } else { + return false; + } + } + if (state == LENGTH_WRITTEN) { + boolean written = huffman + ? huffmanWriter.write(output) + : plainWriter.write(output); + if (written) { + state = DONE; + return true; + } else { + return false; + } + } + throw new InternalError(Arrays.toString(new Object[]{state, huffman})); + } + + void reset() { + intWriter.reset(); + if (huffman) { + huffmanWriter.reset(); + } else { + plainWriter.reset(); + } + state = NEW; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/hpack/package-info.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/package-info.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +/** + * HPACK (Header Compression for HTTP/2) implementation conforming to + * RFC 7541. + * + *

Headers can be decoded and encoded by {@link jdk.internal.net.http.hpack.Decoder} + * and {@link jdk.internal.net.http.hpack.Encoder} respectively. + * + *

Instances of these classes are not safe for use by multiple threads. + */ +package jdk.internal.net.http.hpack; diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/BuilderImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/BuilderImpl.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.net.http.WebSocket.Builder; +import java.net.http.WebSocket.Listener; +import jdk.internal.net.http.common.Pair; + +import java.net.ProxySelector; +import java.net.URI; +import java.time.Duration; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.common.Pair.pair; + +public final class BuilderImpl implements Builder { + + private final HttpClient client; + private URI uri; + private Listener listener; + private final Optional proxySelector; + private final Collection> headers; + private final Collection subprotocols; + private Duration timeout; + + public BuilderImpl(HttpClient client, ProxySelector proxySelector) + { + this(client, null, null, Optional.ofNullable(proxySelector), + new LinkedList<>(), new LinkedList<>(), null); + } + + private BuilderImpl(HttpClient client, + URI uri, + Listener listener, + Optional proxySelector, + Collection> headers, + Collection subprotocols, + Duration timeout) { + this.client = client; + this.uri = uri; + this.listener = listener; + this.proxySelector = proxySelector; + // If a proxy selector was supplied by the user, it should be present + // on the client and should be the same that what we got as an argument + assert !client.proxy().isPresent() + || client.proxy().equals(proxySelector); + this.headers = headers; + this.subprotocols = subprotocols; + this.timeout = timeout; + } + + @Override + public Builder header(String name, String value) { + requireNonNull(name, "name"); + requireNonNull(value, "value"); + headers.add(pair(name, value)); + return this; + } + + @Override + public Builder subprotocols(String mostPreferred, String... lesserPreferred) + { + requireNonNull(mostPreferred, "mostPreferred"); + requireNonNull(lesserPreferred, "lesserPreferred"); + List subprotocols = new LinkedList<>(); + subprotocols.add(mostPreferred); + for (int i = 0; i < lesserPreferred.length; i++) { + String p = lesserPreferred[i]; + requireNonNull(p, "lesserPreferred[" + i + "]"); + subprotocols.add(p); + } + this.subprotocols.clear(); + this.subprotocols.addAll(subprotocols); + return this; + } + + @Override + public Builder connectTimeout(Duration timeout) { + this.timeout = requireNonNull(timeout, "timeout"); + return this; + } + + @Override + public CompletableFuture buildAsync(URI uri, Listener listener) { + this.uri = requireNonNull(uri, "uri"); + this.listener = requireNonNull(listener, "listener"); + // A snapshot of builder inaccessible for further modification + // from the outside + BuilderImpl copy = immutableCopy(); + return WebSocketImpl.newInstanceAsync(copy); + } + + HttpClient getClient() { return client; } + + URI getUri() { return uri; } + + Listener getListener() { return listener; } + + Collection> getHeaders() { return headers; } + + Collection getSubprotocols() { return subprotocols; } + + Duration getConnectTimeout() { return timeout; } + + Optional getProxySelector() { return proxySelector; } + + private BuilderImpl immutableCopy() { + @SuppressWarnings({"unchecked", "rawtypes"}) + BuilderImpl copy = new BuilderImpl( + client, + uri, + listener, + proxySelector, + List.of(this.headers.toArray(new Pair[0])), + List.of(this.subprotocols.toArray(new String[0])), + timeout); + return copy; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/CheckFailedException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/CheckFailedException.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +/* + * Used as a context-neutral exception which can be wrapped into (for example) + * a `ProtocolException` or an `IllegalArgumentException` depending on who's + * doing the check. + */ +final class CheckFailedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + CheckFailedException(String message) { + super(message); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/FailWebSocketException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/FailWebSocketException.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import static jdk.internal.net.http.websocket.StatusCodes.PROTOCOL_ERROR; + +/* + * Used as a marker for protocol issues in the incoming data, so that the + * implementation could "Fail the WebSocket Connection" with a status code in + * the Close message that fits the situation the most. + * + * https://tools.ietf.org/html/rfc6455#section-7.1.7 + */ +final class FailWebSocketException extends RuntimeException { + + private static final long serialVersionUID = 1L; + private final int statusCode; + + FailWebSocketException(String detail) { + this(detail, PROTOCOL_ERROR); + } + + FailWebSocketException(String detail, int statusCode) { + super(detail); + this.statusCode = statusCode; + } + + int getStatusCode() { + return statusCode; + } + + @Override + public FailWebSocketException initCause(Throwable cause) { + return (FailWebSocketException) super.initCause(cause); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/Frame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/Frame.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,499 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import jdk.internal.vm.annotation.Stable; + +import java.nio.ByteBuffer; + +import static jdk.internal.net.http.common.Utils.dump; +import static jdk.internal.net.http.websocket.Frame.Opcode.ofCode; + +/* + * A collection of utilities for reading, writing, and masking frames. + */ +final class Frame { + + private Frame() { } + + static final int MAX_HEADER_SIZE_BYTES = 2 + 8 + 4; + + enum Opcode { + + CONTINUATION (0x0), + TEXT (0x1), + BINARY (0x2), + NON_CONTROL_0x3(0x3), + NON_CONTROL_0x4(0x4), + NON_CONTROL_0x5(0x5), + NON_CONTROL_0x6(0x6), + NON_CONTROL_0x7(0x7), + CLOSE (0x8), + PING (0x9), + PONG (0xA), + CONTROL_0xB (0xB), + CONTROL_0xC (0xC), + CONTROL_0xD (0xD), + CONTROL_0xE (0xE), + CONTROL_0xF (0xF); + + @Stable + private static final Opcode[] opcodes; + + static { + Opcode[] values = values(); + opcodes = new Opcode[values.length]; + for (Opcode c : values) { + opcodes[c.code] = c; + } + } + + private final byte code; + + Opcode(int code) { + this.code = (byte) code; + } + + boolean isControl() { + return (code & 0x8) != 0; + } + + static Opcode ofCode(int code) { + return opcodes[code & 0xF]; + } + } + + /* + * A utility for masking frame payload data. + */ + static final class Masker { + + // Exploiting ByteBuffer's ability to read/write multi-byte integers + private final ByteBuffer acc = ByteBuffer.allocate(8); + private final int[] maskBytes = new int[4]; + private int offset; + private long maskLong; + + /* + * Reads all remaining bytes from the given input buffer, masks them + * with the supplied mask and writes the resulting bytes to the given + * output buffer. + * + * The source and the destination buffers may be the same instance. + */ + static void transferMasking(ByteBuffer src, ByteBuffer dst, int mask) { + if (src.remaining() > dst.remaining()) { + throw new IllegalArgumentException(dump(src, dst)); + } + new Masker().mask(mask).transferMasking(src, dst); + } + + /* + * Clears this instance's state and sets the mask. + * + * The behaviour is as if the mask was set on a newly created instance. + */ + Masker mask(int value) { + acc.clear().putInt(value).putInt(value).flip(); + for (int i = 0; i < maskBytes.length; i++) { + maskBytes[i] = acc.get(i); + } + offset = 0; + maskLong = acc.getLong(0); + return this; + } + + /* + * Reads as many remaining bytes as possible from the given input + * buffer, masks them with the previously set mask and writes the + * resulting bytes to the given output buffer. + * + * The source and the destination buffers may be the same instance. If + * the mask hasn't been previously set it is assumed to be 0. + */ + Masker transferMasking(ByteBuffer src, ByteBuffer dst) { + begin(src, dst); + loop(src, dst); + end(src, dst); + return this; + } + + /* + * Applies up to 3 remaining from the previous pass bytes of the mask. + */ + private void begin(ByteBuffer src, ByteBuffer dst) { + if (offset == 0) { // No partially applied mask from the previous invocation + return; + } + int i = src.position(), j = dst.position(); + final int srcLim = src.limit(), dstLim = dst.limit(); + for (; offset < 4 && i < srcLim && j < dstLim; i++, j++, offset++) + { + dst.put(j, (byte) (src.get(i) ^ maskBytes[offset])); + } + offset &= 3; // Will become 0 if the mask has been fully applied + src.position(i); + dst.position(j); + } + + /* + * Gallops one long (mask + mask) at a time. + */ + private void loop(ByteBuffer src, ByteBuffer dst) { + int i = src.position(); + int j = dst.position(); + final int srcLongLim = src.limit() - 7, dstLongLim = dst.limit() - 7; + for (; i < srcLongLim && j < dstLongLim; i += 8, j += 8) { + dst.putLong(j, src.getLong(i) ^ maskLong); + } + if (i > src.limit()) { + src.position(i - 8); + } else { + src.position(i); + } + if (j > dst.limit()) { + dst.position(j - 8); + } else { + dst.position(j); + } + } + + /* + * Applies up to 7 remaining from the "galloping" phase bytes of the + * mask. + */ + private void end(ByteBuffer src, ByteBuffer dst) { + assert Math.min(src.remaining(), dst.remaining()) < 8; + final int srcLim = src.limit(), dstLim = dst.limit(); + int i = src.position(), j = dst.position(); + for (; i < srcLim && j < dstLim; + i++, j++, offset = (offset + 1) & 3) // offset cycles through 0..3 + { + dst.put(j, (byte) (src.get(i) ^ maskBytes[offset])); + } + src.position(i); + dst.position(j); + } + } + + /* + * A builder-style writer of frame headers. + * + * The writer does not enforce any protocol-level rules, it simply writes a + * header structure to the given buffer. The order of calls to intermediate + * methods is NOT significant. + */ + static final class HeaderWriter { + + private char firstChar; + private long payloadLen; + private int maskingKey; + private boolean mask; + + HeaderWriter fin(boolean value) { + if (value) { + firstChar |= 0b10000000_00000000; + } else { + firstChar &= ~0b10000000_00000000; + } + return this; + } + + HeaderWriter rsv1(boolean value) { + if (value) { + firstChar |= 0b01000000_00000000; + } else { + firstChar &= ~0b01000000_00000000; + } + return this; + } + + HeaderWriter rsv2(boolean value) { + if (value) { + firstChar |= 0b00100000_00000000; + } else { + firstChar &= ~0b00100000_00000000; + } + return this; + } + + HeaderWriter rsv3(boolean value) { + if (value) { + firstChar |= 0b00010000_00000000; + } else { + firstChar &= ~0b00010000_00000000; + } + return this; + } + + HeaderWriter opcode(Opcode value) { + firstChar = (char) ((firstChar & 0xF0FF) | (value.code << 8)); + return this; + } + + HeaderWriter payloadLen(long value) { + if (value < 0) { + throw new IllegalArgumentException("Negative: " + value); + } + payloadLen = value; + firstChar &= 0b11111111_10000000; // Clear previous payload length leftovers + if (payloadLen < 126) { + firstChar |= payloadLen; + } else if (payloadLen < 65536) { + firstChar |= 126; + } else { + firstChar |= 127; + } + return this; + } + + HeaderWriter mask(int value) { + firstChar |= 0b00000000_10000000; + maskingKey = value; + mask = true; + return this; + } + + HeaderWriter noMask() { + firstChar &= ~0b00000000_10000000; + mask = false; + return this; + } + + /* + * Writes the header to the given buffer. + * + * The buffer must have at least MAX_HEADER_SIZE_BYTES remaining. The + * buffer's position is incremented by the number of bytes written. + */ + void write(ByteBuffer buffer) { + buffer.putChar(firstChar); + if (payloadLen >= 126) { + if (payloadLen < 65536) { + buffer.putChar((char) payloadLen); + } else { + buffer.putLong(payloadLen); + } + } + if (mask) { + buffer.putInt(maskingKey); + } + } + } + + /* + * A consumer of frame parts. + * + * Frame.Reader invokes the consumer's methods in the following order: + * + * fin rsv1 rsv2 rsv3 opcode mask payloadLength maskingKey? payloadData+ endFrame + */ + interface Consumer { + + void fin(boolean value); + + void rsv1(boolean value); + + void rsv2(boolean value); + + void rsv3(boolean value); + + void opcode(Opcode value); + + void mask(boolean value); + + void payloadLen(long value); + + void maskingKey(int value); + + /* + * Called by the Frame.Reader when a part of the (or a complete) payload + * is ready to be consumed. + * + * The sum of numbers of bytes consumed in each invocation of this + * method corresponding to the given frame WILL be equal to + * 'payloadLen', reported to `void payloadLen(long value)` before that. + * + * In particular, if `payloadLen` is 0, then there WILL be a single + * invocation to this method. + * + * No unmasking is done. + */ + void payloadData(ByteBuffer data); + + void endFrame(); + } + + /* + * A Reader of frames. + * + * No protocol-level rules are checked. + */ + static final class Reader { + + private static final int AWAITING_FIRST_BYTE = 1; + private static final int AWAITING_SECOND_BYTE = 2; + private static final int READING_16_LENGTH = 4; + private static final int READING_64_LENGTH = 8; + private static final int READING_MASK = 16; + private static final int READING_PAYLOAD = 32; + + // Exploiting ByteBuffer's ability to read multi-byte integers + private final ByteBuffer accumulator = ByteBuffer.allocate(8); + private int state = AWAITING_FIRST_BYTE; + private boolean mask; + private long remainingPayloadLength; + + /* + * Reads at most one frame from the given buffer invoking the consumer's + * methods corresponding to the frame parts found. + * + * As much of the frame's payload, if any, is read. The buffer's + * position is updated to reflect the number of bytes read. + * + * Throws FailWebSocketException if detects the frame is malformed. + */ + void readFrame(ByteBuffer input, Consumer consumer) { + loop: + while (true) { + byte b; + switch (state) { + case AWAITING_FIRST_BYTE: + if (!input.hasRemaining()) { + break loop; + } + b = input.get(); + consumer.fin( (b & 0b10000000) != 0); + consumer.rsv1((b & 0b01000000) != 0); + consumer.rsv2((b & 0b00100000) != 0); + consumer.rsv3((b & 0b00010000) != 0); + consumer.opcode(ofCode(b)); + state = AWAITING_SECOND_BYTE; + continue loop; + case AWAITING_SECOND_BYTE: + if (!input.hasRemaining()) { + break loop; + } + b = input.get(); + consumer.mask(mask = (b & 0b10000000) != 0); + byte p1 = (byte) (b & 0b01111111); + if (p1 < 126) { + assert p1 >= 0 : p1; + consumer.payloadLen(remainingPayloadLength = p1); + state = mask ? READING_MASK : READING_PAYLOAD; + } else if (p1 < 127) { + state = READING_16_LENGTH; + } else { + state = READING_64_LENGTH; + } + continue loop; + case READING_16_LENGTH: + if (!input.hasRemaining()) { + break loop; + } + b = input.get(); + if (accumulator.put(b).position() < 2) { + continue loop; + } + remainingPayloadLength = accumulator.flip().getChar(); + if (remainingPayloadLength < 126) { + throw notMinimalEncoding(remainingPayloadLength); + } + consumer.payloadLen(remainingPayloadLength); + accumulator.clear(); + state = mask ? READING_MASK : READING_PAYLOAD; + continue loop; + case READING_64_LENGTH: + if (!input.hasRemaining()) { + break loop; + } + b = input.get(); + if (accumulator.put(b).position() < 8) { + continue loop; + } + remainingPayloadLength = accumulator.flip().getLong(); + if (remainingPayloadLength < 0) { + throw negativePayload(remainingPayloadLength); + } else if (remainingPayloadLength < 65536) { + throw notMinimalEncoding(remainingPayloadLength); + } + consumer.payloadLen(remainingPayloadLength); + accumulator.clear(); + state = mask ? READING_MASK : READING_PAYLOAD; + continue loop; + case READING_MASK: + if (!input.hasRemaining()) { + break loop; + } + b = input.get(); + if (accumulator.put(b).position() != 4) { + continue loop; + } + consumer.maskingKey(accumulator.flip().getInt()); + accumulator.clear(); + state = READING_PAYLOAD; + continue loop; + case READING_PAYLOAD: + // This state does not require any bytes to be available + // in the input buffer in order to proceed + int deliverable = (int) Math.min(remainingPayloadLength, + input.remaining()); + int oldLimit = input.limit(); + input.limit(input.position() + deliverable); + if (deliverable != 0 || remainingPayloadLength == 0) { + consumer.payloadData(input); + } + int consumed = deliverable - input.remaining(); + if (consumed < 0) { + // Consumer cannot consume more than there was available + throw new InternalError(); + } + input.limit(oldLimit); + remainingPayloadLength -= consumed; + if (remainingPayloadLength == 0) { + consumer.endFrame(); + state = AWAITING_FIRST_BYTE; + } + break loop; + default: + throw new InternalError(String.valueOf(state)); + } + } + } + + private static FailWebSocketException negativePayload(long payloadLength) + { + return new FailWebSocketException( + "Negative payload length: " + payloadLength); + } + + private static FailWebSocketException notMinimalEncoding(long payloadLength) + { + return new FailWebSocketException( + "Not minimally-encoded payload length:" + payloadLength); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/FrameConsumer.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/FrameConsumer.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import java.net.http.WebSocket.MessagePart; +import jdk.internal.net.http.websocket.Frame.Opcode; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; + +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.common.Utils.dump; +import static jdk.internal.net.http.websocket.StatusCodes.NO_STATUS_CODE; +import static jdk.internal.net.http.websocket.StatusCodes.isLegalToReceiveFromServer; + +/* + * Consumes frame parts and notifies a message consumer, when there is + * sufficient data to produce a message, or part thereof. + * + * Data consumed but not yet translated is accumulated until it's sufficient to + * form a message. + */ +/* Non-final for testing purposes only */ +class FrameConsumer implements Frame.Consumer { + + private final static boolean DEBUG = false; + private final MessageStreamConsumer output; + private final UTF8AccumulatingDecoder decoder = new UTF8AccumulatingDecoder(); + private boolean fin; + private Opcode opcode, originatingOpcode; + private MessagePart part = MessagePart.WHOLE; + private long payloadLen; + private long unconsumedPayloadLen; + private ByteBuffer binaryData; + + FrameConsumer(MessageStreamConsumer output) { + this.output = requireNonNull(output); + } + + /* Exposed for testing purposes only */ + MessageStreamConsumer getOutput() { + return output; + } + + @Override + public void fin(boolean value) { + if (DEBUG) { + System.out.printf("Reading fin: %s%n", value); + } + fin = value; + } + + @Override + public void rsv1(boolean value) { + if (DEBUG) { + System.out.printf("Reading rsv1: %s%n", value); + } + if (value) { + throw new FailWebSocketException("Unexpected rsv1 bit"); + } + } + + @Override + public void rsv2(boolean value) { + if (DEBUG) { + System.out.printf("Reading rsv2: %s%n", value); + } + if (value) { + throw new FailWebSocketException("Unexpected rsv2 bit"); + } + } + + @Override + public void rsv3(boolean value) { + if (DEBUG) { + System.out.printf("Reading rsv3: %s%n", value); + } + if (value) { + throw new FailWebSocketException("Unexpected rsv3 bit"); + } + } + + @Override + public void opcode(Opcode v) { + if (DEBUG) { + System.out.printf("Reading opcode: %s%n", v); + } + if (v == Opcode.PING || v == Opcode.PONG || v == Opcode.CLOSE) { + if (!fin) { + throw new FailWebSocketException("Fragmented control frame " + v); + } + opcode = v; + } else if (v == Opcode.TEXT || v == Opcode.BINARY) { + if (originatingOpcode != null) { + throw new FailWebSocketException( + format("Unexpected frame %s (fin=%s)", v, fin)); + } + opcode = v; + if (!fin) { + originatingOpcode = v; + } + } else if (v == Opcode.CONTINUATION) { + if (originatingOpcode == null) { + throw new FailWebSocketException( + format("Unexpected frame %s (fin=%s)", v, fin)); + } + opcode = v; + } else { + throw new FailWebSocketException("Unexpected opcode " + v); + } + } + + @Override + public void mask(boolean value) { + if (DEBUG) { + System.out.printf("Reading mask: %s%n", value); + } + if (value) { + throw new FailWebSocketException("Masked frame received"); + } + } + + @Override + public void payloadLen(long value) { + if (DEBUG) { + System.out.printf("Reading payloadLen: %s%n", value); + } + if (opcode.isControl()) { + if (value > 125) { + throw new FailWebSocketException( + format("%s's payload length %s", opcode, value)); + } + assert Opcode.CLOSE.isControl(); + if (opcode == Opcode.CLOSE && value == 1) { + throw new FailWebSocketException("Incomplete status code"); + } + } + payloadLen = value; + unconsumedPayloadLen = value; + } + + @Override + public void maskingKey(int value) { + // `FrameConsumer.mask(boolean)` is where a masked frame is detected and + // reported on; `FrameConsumer.mask(boolean)` MUST be invoked before + // this method; + // So this method (`maskingKey`) is not supposed to be invoked while + // reading a frame that has came from the server. If this method is + // invoked, then it's an error in implementation, thus InternalError + throw new InternalError(); + } + + @Override + public void payloadData(ByteBuffer data) { + if (DEBUG) { + System.out.printf("Reading payloadData: %s%n", data); + } + unconsumedPayloadLen -= data.remaining(); + boolean isLast = unconsumedPayloadLen == 0; + if (opcode.isControl()) { + if (binaryData != null) { // An intermediate or the last chunk + binaryData.put(data); + } else if (!isLast) { // The first chunk + int remaining = data.remaining(); + // It shouldn't be 125, otherwise the next chunk will be of size + // 0, which is not what Reader promises to deliver (eager + // reading) + assert remaining < 125 : dump(remaining); + binaryData = ByteBuffer.allocate(125).put(data); + } else { // The only chunk + binaryData = ByteBuffer.allocate(data.remaining()).put(data); + } + } else { + part = determinePart(isLast); + boolean text = opcode == Opcode.TEXT || originatingOpcode == Opcode.TEXT; + if (!text) { + output.onBinary(data.slice(), part); + data.position(data.limit()); // Consume + } else { + boolean binaryNonEmpty = data.hasRemaining(); + CharBuffer textData; + try { + textData = decoder.decode(data, part == MessagePart.WHOLE || part == MessagePart.LAST); + } catch (CharacterCodingException e) { + throw new FailWebSocketException( + "Invalid UTF-8 in frame " + opcode, StatusCodes.NOT_CONSISTENT) + .initCause(e); + } + if (!(binaryNonEmpty && !textData.hasRemaining())) { + // If there's a binary data, that result in no text, then we + // don't deliver anything + output.onText(textData, part); + } + } + } + } + + @Override + public void endFrame() { + if (DEBUG) { + System.out.println("End frame"); + } + if (opcode.isControl()) { + binaryData.flip(); + } + switch (opcode) { + case CLOSE: + char statusCode = NO_STATUS_CODE; + String reason = ""; + if (payloadLen != 0) { + int len = binaryData.remaining(); + assert 2 <= len && len <= 125 : dump(len, payloadLen); + statusCode = binaryData.getChar(); + if (!isLegalToReceiveFromServer(statusCode)) { + throw new FailWebSocketException( + "Illegal status code: " + statusCode); + } + try { + reason = UTF_8.newDecoder().decode(binaryData).toString(); + } catch (CharacterCodingException e) { + throw new FailWebSocketException("Illegal close reason") + .initCause(e); + } + } + output.onClose(statusCode, reason); + break; + case PING: + output.onPing(binaryData); + binaryData = null; + break; + case PONG: + output.onPong(binaryData); + binaryData = null; + break; + default: + assert opcode == Opcode.TEXT || opcode == Opcode.BINARY + || opcode == Opcode.CONTINUATION : dump(opcode); + if (fin) { + // It is always the last chunk: + // either TEXT(FIN=TRUE)/BINARY(FIN=TRUE) or CONT(FIN=TRUE) + originatingOpcode = null; + } + break; + } + payloadLen = 0; + opcode = null; + } + + private MessagePart determinePart(boolean isLast) { + boolean lastChunk = fin && isLast; + switch (part) { + case LAST: + case WHOLE: + return lastChunk ? MessagePart.WHOLE : MessagePart.FIRST; + case FIRST: + case PART: + return lastChunk ? MessagePart.LAST : MessagePart.PART; + default: + throw new InternalError(String.valueOf(part)); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/MessageStreamConsumer.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/MessageStreamConsumer.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import java.net.http.WebSocket.MessagePart; + +import java.nio.ByteBuffer; + +/* + * A callback for consuming messages and related events on the stream. + */ +interface MessageStreamConsumer { + + void onText(CharSequence data, MessagePart part); + + void onBinary(ByteBuffer data, MessagePart part); + + void onPing(ByteBuffer data); + + void onPong(ByteBuffer data); + + void onClose(int statusCode, CharSequence reason); + + /* + * Indicates the end of stream has been reached and there will be no further + * messages. + */ + void onComplete(); + + void onError(Throwable e); +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/OpeningHandshake.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/OpeningHandshake.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,392 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.WebSocketHandshakeException; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Pair; +import jdk.internal.net.http.common.Utils; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLPermission; +import java.nio.charset.StandardCharsets; +import java.security.AccessController; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivilegedAction; +import java.security.SecureRandom; +import java.time.Duration; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.lang.String.format; +import static jdk.internal.net.http.common.Utils.isValidName; +import static jdk.internal.net.http.common.Utils.permissionForProxy; +import static jdk.internal.net.http.common.Utils.stringOf; + +public class OpeningHandshake { + + private static final String HEADER_CONNECTION = "Connection"; + private static final String HEADER_UPGRADE = "Upgrade"; + private static final String HEADER_ACCEPT = "Sec-WebSocket-Accept"; + private static final String HEADER_EXTENSIONS = "Sec-WebSocket-Extensions"; + private static final String HEADER_KEY = "Sec-WebSocket-Key"; + private static final String HEADER_PROTOCOL = "Sec-WebSocket-Protocol"; + private static final String HEADER_VERSION = "Sec-WebSocket-Version"; + private static final String VERSION = "13"; // WebSocket's lucky number + + private static final Set ILLEGAL_HEADERS; + + static { + ILLEGAL_HEADERS = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + ILLEGAL_HEADERS.addAll(List.of(HEADER_ACCEPT, + HEADER_EXTENSIONS, + HEADER_KEY, + HEADER_PROTOCOL, + HEADER_VERSION)); + } + + private static final SecureRandom random = new SecureRandom(); + + private final MessageDigest sha1; + private final HttpClient client; + + { + try { + sha1 = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + // Shouldn't happen: SHA-1 must be available in every Java platform + // implementation + throw new InternalError("Minimum requirements", e); + } + } + + private final HttpRequest request; + private final Collection subprotocols; + private final String nonce; + + public OpeningHandshake(BuilderImpl b) { + checkURI(b.getUri()); + Proxy proxy = proxyFor(b.getProxySelector(), b.getUri()); + checkPermissions(b, proxy); + this.client = b.getClient(); + URI httpURI = createRequestURI(b.getUri()); + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(httpURI); + Duration connectTimeout = b.getConnectTimeout(); + if (connectTimeout != null) { + requestBuilder.timeout(connectTimeout); + } + for (Pair p : b.getHeaders()) { + if (ILLEGAL_HEADERS.contains(p.first)) { + throw illegal("Illegal header: " + p.first); + } + requestBuilder.header(p.first, p.second); + } + this.subprotocols = createRequestSubprotocols(b.getSubprotocols()); + if (!this.subprotocols.isEmpty()) { + String p = this.subprotocols.stream().collect(Collectors.joining(", ")); + requestBuilder.header(HEADER_PROTOCOL, p); + } + requestBuilder.header(HEADER_VERSION, VERSION); + this.nonce = createNonce(); + requestBuilder.header(HEADER_KEY, this.nonce); + // Setting request version to HTTP/1.1 forcibly, since it's not possible + // to upgrade from HTTP/2 to WebSocket (as of August 2016): + // + // https://tools.ietf.org/html/draft-hirano-httpbis-websocket-over-http2-00 + this.request = requestBuilder.version(Version.HTTP_1_1).GET().build(); + WebSocketRequest r = (WebSocketRequest) this.request; + r.isWebSocket(true); + r.setSystemHeader(HEADER_UPGRADE, "websocket"); + r.setSystemHeader(HEADER_CONNECTION, "Upgrade"); + r.setProxy(proxy); + } + + private static Collection createRequestSubprotocols( + Collection subprotocols) + { + LinkedHashSet sp = new LinkedHashSet<>(subprotocols.size(), 1); + for (String s : subprotocols) { + if (s.trim().isEmpty() || !isValidName(s)) { + throw illegal("Bad subprotocol syntax: " + s); + } + if (!sp.add(s)) { + throw illegal("Duplicating subprotocol: " + s); + } + } + return Collections.unmodifiableCollection(sp); + } + + /* + * Checks the given URI for being a WebSocket URI and translates it into a + * target HTTP URI for the Opening Handshake. + * + * https://tools.ietf.org/html/rfc6455#section-3 + */ + static URI createRequestURI(URI uri) { + String s = uri.getScheme(); + assert "ws".equalsIgnoreCase(s) || "wss".equalsIgnoreCase(s); + String scheme = "ws".equalsIgnoreCase(s) ? "http" : "https"; + try { + return new URI(scheme, + uri.getUserInfo(), + uri.getHost(), + uri.getPort(), + uri.getPath(), + uri.getQuery(), + null); // No fragment + } catch (URISyntaxException e) { + // Shouldn't happen: URI invariant + throw new InternalError(e); + } + } + + public CompletableFuture send() { + PrivilegedAction> pa = () -> + client.sendAsync(this.request, BodyHandler.discard()) + .thenCompose(this::resultFrom); + return AccessController.doPrivileged(pa); + } + + /* + * The result of the opening handshake. + */ + static final class Result { + + final String subprotocol; + final TransportFactory transport; + + private Result(String subprotocol, TransportFactory transport) { + this.subprotocol = subprotocol; + this.transport = transport; + } + } + + private CompletableFuture resultFrom(HttpResponse response) { + // Do we need a special treatment for SSLHandshakeException? + // Namely, invoking + // + // Listener.onClose(StatusCodes.TLS_HANDSHAKE_FAILURE, "") + // + // See https://tools.ietf.org/html/rfc6455#section-7.4.1 + Result result = null; + Exception exception = null; + try { + result = handleResponse(response); + } catch (IOException e) { + exception = e; + } catch (Exception e) { + exception = new WebSocketHandshakeException(response).initCause(e); + } + if (exception == null) { + return MinimalFuture.completedFuture(result); + } + try { + ((RawChannel.Provider) response).rawChannel().close(); + } catch (IOException e) { + exception.addSuppressed(e); + } + return MinimalFuture.failedFuture(exception); + } + + private Result handleResponse(HttpResponse response) throws IOException { + // By this point all redirects, authentications, etc. (if any) MUST have + // been done by the HttpClient used by the WebSocket; so only 101 is + // expected + int c = response.statusCode(); + if (c != 101) { + throw checkFailed("Unexpected HTTP response status code " + c); + } + HttpHeaders headers = response.headers(); + String upgrade = requireSingle(headers, HEADER_UPGRADE); + if (!upgrade.equalsIgnoreCase("websocket")) { + throw checkFailed("Bad response field: " + HEADER_UPGRADE); + } + String connection = requireSingle(headers, HEADER_CONNECTION); + if (!connection.equalsIgnoreCase("Upgrade")) { + throw checkFailed("Bad response field: " + HEADER_CONNECTION); + } + Optional version = requireAtMostOne(headers, HEADER_VERSION); + if (version.isPresent() && !version.get().equals(VERSION)) { + throw checkFailed("Bad response field: " + HEADER_VERSION); + } + requireAbsent(headers, HEADER_EXTENSIONS); + String x = this.nonce + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + this.sha1.update(x.getBytes(StandardCharsets.ISO_8859_1)); + String expected = Base64.getEncoder().encodeToString(this.sha1.digest()); + String actual = requireSingle(headers, HEADER_ACCEPT); + if (!actual.trim().equals(expected)) { + throw checkFailed("Bad " + HEADER_ACCEPT); + } + String subprotocol = checkAndReturnSubprotocol(headers); + RawChannel channel = ((RawChannel.Provider) response).rawChannel(); + return new Result(subprotocol, new TransportFactoryImpl(channel)); + } + + private String checkAndReturnSubprotocol(HttpHeaders responseHeaders) + throws CheckFailedException + { + Optional opt = responseHeaders.firstValue(HEADER_PROTOCOL); + if (!opt.isPresent()) { + // If there is no such header in the response, then the server + // doesn't want to use any subprotocol + return ""; + } + String s = requireSingle(responseHeaders, HEADER_PROTOCOL); + // An empty string as a subprotocol's name is not allowed by the spec + // and the check below will detect such responses too + if (this.subprotocols.contains(s)) { + return s; + } else { + throw checkFailed("Unexpected subprotocol: " + s); + } + } + + private static void requireAbsent(HttpHeaders responseHeaders, + String headerName) + { + List values = responseHeaders.allValues(headerName); + if (!values.isEmpty()) { + throw checkFailed(format("Response field '%s' present: %s", + headerName, + stringOf(values))); + } + } + + private static Optional requireAtMostOne(HttpHeaders responseHeaders, + String headerName) + { + List values = responseHeaders.allValues(headerName); + if (values.size() > 1) { + throw checkFailed(format("Response field '%s' multivalued: %s", + headerName, + stringOf(values))); + } + return values.stream().findFirst(); + } + + private static String requireSingle(HttpHeaders responseHeaders, + String headerName) + { + List values = responseHeaders.allValues(headerName); + if (values.isEmpty()) { + throw checkFailed("Response field missing: " + headerName); + } else if (values.size() > 1) { + throw checkFailed(format("Response field '%s' multivalued: %s", + headerName, + stringOf(values))); + } + return values.get(0); + } + + private static String createNonce() { + byte[] bytes = new byte[16]; + OpeningHandshake.random.nextBytes(bytes); + return Base64.getEncoder().encodeToString(bytes); + } + + private static CheckFailedException checkFailed(String message) { + throw new CheckFailedException(message); + } + + private static URI checkURI(URI uri) { + String scheme = uri.getScheme(); + if (!("ws".equalsIgnoreCase(scheme) || "wss".equalsIgnoreCase(scheme))) + throw illegal("invalid URI scheme: " + scheme); + if (uri.getHost() == null) + throw illegal("URI must contain a host: " + uri); + if (uri.getFragment() != null) + throw illegal("URI must not contain a fragment: " + uri); + return uri; + } + + private static IllegalArgumentException illegal(String message) { + return new IllegalArgumentException(message); + } + + /** + * Returns the proxy for the given URI when sent through the given client, + * or {@code null} if none is required or applicable. + */ + private static Proxy proxyFor(Optional selector, URI uri) { + if (!selector.isPresent()) { + return null; + } + URI requestURI = createRequestURI(uri); // Based on the HTTP scheme + List pl = selector.get().select(requestURI); + if (pl.isEmpty()) { + return null; + } + Proxy proxy = pl.get(0); + if (proxy.type() != Proxy.Type.HTTP) { + return null; + } + return proxy; + } + + /** + * Performs the necessary security permissions checks to connect ( possibly + * through a proxy ) to the builders WebSocket URI. + * + * @throws SecurityException if the security manager denies access + */ + static void checkPermissions(BuilderImpl b, Proxy proxy) { + SecurityManager sm = System.getSecurityManager(); + if (sm == null) { + return; + } + Stream headers = b.getHeaders().stream().map(p -> p.first).distinct(); + URLPermission perm1 = Utils.permissionForServer(b.getUri(), "", headers); + sm.checkPermission(perm1); + if (proxy == null) { + return; + } + URLPermission perm2 = permissionForProxy((InetSocketAddress) proxy.address()); + if (perm2 != null) { + sm.checkPermission(perm2); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/OutgoingMessage.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/OutgoingMessage.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import jdk.internal.net.http.websocket.Frame.Opcode; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.security.SecureRandom; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.common.Utils.EMPTY_BYTEBUFFER; +import static jdk.internal.net.http.websocket.Frame.MAX_HEADER_SIZE_BYTES; +import static jdk.internal.net.http.websocket.Frame.Opcode.BINARY; +import static jdk.internal.net.http.websocket.Frame.Opcode.CLOSE; +import static jdk.internal.net.http.websocket.Frame.Opcode.CONTINUATION; +import static jdk.internal.net.http.websocket.Frame.Opcode.PING; +import static jdk.internal.net.http.websocket.Frame.Opcode.PONG; +import static jdk.internal.net.http.websocket.Frame.Opcode.TEXT; + +/* + * A stateful object that represents a WebSocket message being sent to the + * channel. + * + * Data provided to the constructors is copied. Otherwise we would have to deal + * with mutability, security, masking/unmasking, readonly status, etc. So + * copying greatly simplifies the implementation. + * + * In the case of memory-sensitive environments an alternative implementation + * could use an internal pool of buffers though at the cost of extra complexity + * and possible performance degradation. + */ +abstract class OutgoingMessage { + + // Share per WebSocket? + private static final SecureRandom maskingKeys = new SecureRandom(); + + protected ByteBuffer[] frame; + protected int offset; + + /* + * Performs contextualization. This method is not a part of the constructor + * so it would be possible to defer the work it does until the most + * convenient moment (up to the point where sentTo is invoked). + */ + protected boolean contextualize(Context context) { + // masking and charset decoding should be performed here rather than in + // the constructor (as of today) + if (context.isCloseSent()) { + throw new IllegalStateException("Close sent"); + } + return true; + } + + protected boolean sendTo(RawChannel channel) throws IOException { + while ((offset = nextUnwrittenIndex()) != -1) { + long n = channel.write(frame, offset, frame.length - offset); + if (n == 0) { + return false; + } + } + return true; + } + + private int nextUnwrittenIndex() { + for (int i = offset; i < frame.length; i++) { + if (frame[i].hasRemaining()) { + return i; + } + } + return -1; + } + + static final class Text extends OutgoingMessage { + + private final ByteBuffer payload; + private final boolean isLast; + + Text(CharSequence characters, boolean isLast) { + CharsetEncoder encoder = UTF_8.newEncoder(); // Share per WebSocket? + try { + payload = encoder.encode(CharBuffer.wrap(characters)); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException( + "Malformed UTF-8 text message"); + } + this.isLast = isLast; + } + + @Override + protected boolean contextualize(Context context) { + super.contextualize(context); + if (context.isPreviousBinary() && !context.isPreviousLast()) { + throw new IllegalStateException("Unexpected text message"); + } + frame = getDataMessageBuffers( + TEXT, context.isPreviousLast(), isLast, payload, payload); + context.setPreviousBinary(false); + context.setPreviousText(true); + context.setPreviousLast(isLast); + return true; + } + } + + static final class Binary extends OutgoingMessage { + + private final ByteBuffer payload; + private final boolean isLast; + + Binary(ByteBuffer payload, boolean isLast) { + this.payload = requireNonNull(payload); + this.isLast = isLast; + } + + @Override + protected boolean contextualize(Context context) { + super.contextualize(context); + if (context.isPreviousText() && !context.isPreviousLast()) { + throw new IllegalStateException("Unexpected binary message"); + } + ByteBuffer newBuffer = ByteBuffer.allocate(payload.remaining()); + frame = getDataMessageBuffers( + BINARY, context.isPreviousLast(), isLast, payload, newBuffer); + context.setPreviousText(false); + context.setPreviousBinary(true); + context.setPreviousLast(isLast); + return true; + } + } + + static final class Ping extends OutgoingMessage { + + Ping(ByteBuffer payload) { + frame = getControlMessageBuffers(PING, payload); + } + } + + static final class Pong extends OutgoingMessage { + + Pong(ByteBuffer payload) { + frame = getControlMessageBuffers(PONG, payload); + } + } + + static final class Close extends OutgoingMessage { + + Close() { + frame = getControlMessageBuffers(CLOSE, EMPTY_BYTEBUFFER); + } + + Close(int statusCode, CharSequence reason) { + ByteBuffer payload = ByteBuffer.allocate(125) + .putChar((char) statusCode); + CoderResult result = UTF_8.newEncoder() + .encode(CharBuffer.wrap(reason), + payload, + true); + if (result.isOverflow()) { + throw new IllegalArgumentException("Long reason"); + } else if (result.isError()) { + try { + result.throwException(); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException( + "Malformed UTF-8 reason", e); + } + } + payload.flip(); + frame = getControlMessageBuffers(CLOSE, payload); + } + + @Override + protected boolean contextualize(Context context) { + if (context.isCloseSent()) { + return false; + } else { + context.setCloseSent(); + return true; + } + } + } + + private static ByteBuffer[] getControlMessageBuffers(Opcode opcode, + ByteBuffer payload) { + assert opcode.isControl() : opcode; + int remaining = payload.remaining(); + if (remaining > 125) { + throw new IllegalArgumentException + ("Long message: " + remaining); + } + ByteBuffer frame = ByteBuffer.allocate(MAX_HEADER_SIZE_BYTES + remaining); + int mask = maskingKeys.nextInt(); + new Frame.HeaderWriter() + .fin(true) + .opcode(opcode) + .payloadLen(remaining) + .mask(mask) + .write(frame); + Frame.Masker.transferMasking(payload, frame, mask); + frame.flip(); + return new ByteBuffer[]{frame}; + } + + private static ByteBuffer[] getDataMessageBuffers(Opcode type, + boolean isPreviousLast, + boolean isLast, + ByteBuffer payloadSrc, + ByteBuffer payloadDst) { + assert !type.isControl() && type != CONTINUATION : type; + ByteBuffer header = ByteBuffer.allocate(MAX_HEADER_SIZE_BYTES); + int mask = maskingKeys.nextInt(); + new Frame.HeaderWriter() + .fin(isLast) + .opcode(isPreviousLast ? type : CONTINUATION) + .payloadLen(payloadDst.remaining()) + .mask(mask) + .write(header); + header.flip(); + Frame.Masker.transferMasking(payloadSrc, payloadDst, mask); + payloadDst.flip(); + return new ByteBuffer[]{header, payloadDst}; + } + + /* + * An instance of this class is passed sequentially between messages, so + * every message in a sequence can check the context it is in and update it + * if necessary. + */ + public static class Context { + + boolean previousLast = true; + boolean previousBinary; + boolean previousText; + boolean closeSent; + + private boolean isPreviousText() { + return this.previousText; + } + + private void setPreviousText(boolean value) { + this.previousText = value; + } + + private boolean isPreviousBinary() { + return this.previousBinary; + } + + private void setPreviousBinary(boolean value) { + this.previousBinary = value; + } + + private boolean isPreviousLast() { + return this.previousLast; + } + + private void setPreviousLast(boolean value) { + this.previousLast = value; + } + + private boolean isCloseSent() { + return closeSent; + } + + private void setCloseSent() { + closeSent = true; + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/RawChannel.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/RawChannel.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; + +/* + * I/O abstraction used to implement WebSocket. + * + * @since 9 + */ +public interface RawChannel extends Closeable { + + interface Provider { + + RawChannel rawChannel() throws IOException; + } + + interface RawEvent { + + /* + * Returns the selector op flags this event is interested in. + */ + int interestOps(); + + /* + * Called when event occurs. + */ + void handle(); + } + + /* + * Registers given event whose callback will be called once only (i.e. + * register new event for each callback). + * + * Memory consistency effects: actions in a thread calling registerEvent + * happen-before any subsequent actions in the thread calling event.handle + */ + void registerEvent(RawEvent event) throws IOException; + + /** + * Hands over the initial bytes. Once the bytes have been returned they are + * no longer available and the method will throw an {@link + * IllegalStateException} on each subsequent invocation. + * + * @return the initial bytes + * @throws IllegalStateException + * if the method has been already invoked + */ + ByteBuffer initialByteBuffer() throws IllegalStateException; + + /* + * Returns a ByteBuffer with the data read or null if EOF is reached. Has no + * remaining bytes if no data available at the moment. + */ + ByteBuffer read() throws IOException; + + /* + * Writes a sequence of bytes to this channel from a subsequence of the + * given buffers. + */ + long write(ByteBuffer[] srcs, int offset, int length) throws IOException; + + /** + * Shutdown the connection for reading without closing the channel. + * + *

Once shutdown for reading then further reads on the channel will + * return {@code null}, the end-of-stream indication. If the input side of + * the connection is already shutdown then invoking this method has no + * effect. + * + * @throws ClosedChannelException + * If this channel is closed + * @throws IOException + * If some other I/O error occurs + */ + void shutdownInput() throws IOException; + + /** + * Shutdown the connection for writing without closing the channel. + * + *

Once shutdown for writing then further attempts to write to the + * channel will throw {@link ClosedChannelException}. If the output side of + * the connection is already shutdown then invoking this method has no + * effect. + * + * @throws ClosedChannelException + * If this channel is closed + * @throws IOException + * If some other I/O error occurs + */ + void shutdownOutput() throws IOException; + + /** + * Closes this channel. + * + * @throws IOException + * If an I/O error occurs + */ + @Override + void close() throws IOException; +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/StatusCodes.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/StatusCodes.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +/* + * Utilities for WebSocket status codes. + * + * 1. https://tools.ietf.org/html/rfc6455#section-7.4 + * 2. http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number + */ +final class StatusCodes { + + static final int PROTOCOL_ERROR = 1002; + static final int NO_STATUS_CODE = 1005; + static final int CLOSED_ABNORMALLY = 1006; + static final int NOT_CONSISTENT = 1007; + + private StatusCodes() { } + + static boolean isLegalToSendFromClient(int code) { + if (!isLegal(code)) { + return false; + } + // Codes from unreserved range + if (code > 4999) { + return false; + } + // Codes below are not allowed to be sent using a WebSocket client API + switch (code) { + case PROTOCOL_ERROR: + case NOT_CONSISTENT: + case 1003: + case 1009: + case 1010: + case 1012: // code sent by servers + case 1013: // code sent by servers + case 1014: // code sent by servers + return false; + default: + return true; + } + } + + static boolean isLegalToReceiveFromServer(int code) { + if (!isLegal(code)) { + return false; + } + return code != 1010; // code sent by clients + } + + private static boolean isLegal(int code) { + // 2-byte unsigned integer excluding first 1000 numbers from the range + // [0, 999] which are never used + if (code < 1000 || code > 65535) { + return false; + } + // Codes from the range below has no known meaning under the WebSocket + // specification (i.e. unassigned/undefined) + if ((code >= 1016 && code <= 2999) || code == 1004) { + return false; + } + // Codes below cannot appear on the wire. It's always an error either + // to send a frame with such a code or to receive one. + switch (code) { + case NO_STATUS_CODE: + case CLOSED_ABNORMALLY: + case 1015: + return false; + default: + return true; + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/Transport.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/Transport.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; + +/* + * Transport needs some way to asynchronously notify the send operation has been + * completed. It can have several different designs each of which has its own + * pros and cons: + * + * (1) void sendMessage(..., Callback) + * (2) CompletableFuture sendMessage(...) + * (3) CompletableFuture sendMessage(..., Callback) + * (4) boolean sendMessage(..., Callback) throws IOException + * ... + * + * If Transport's users use CFs, (1) forces these users to create CFs and pass + * them to the callback. If any additional (dependant) action needs to be + * attached to the returned CF, this means an extra object (CF) must be created + * in (2). (3) and (4) solves both issues, however (4) does not abstract out + * when exactly the operation has been performed. So the handling code needs to + * be repeated twice. And that leads to 2 different code paths (more bugs). + * Unless designed for this, the user should not assume any specific order of + * completion in (3) (e.g. callback first and then the returned CF). + * + * The only parametrization of Transport used is Transport. The + * type parameter T was introduced solely to avoid circular dependency between + * Transport and WebSocket. After all, instances of T are used solely to + * complete CompletableFutures. Transport doesn't care about the exact type of + * T. + * + * This way the Transport is fully in charge of creating CompletableFutures. + * On the one hand, Transport may use it to cache/reuse CompletableFutures. On + * the other hand, the class that uses Transport, may benefit by not converting + * from CompletableFuture returned from Transport, to CompletableFuture + * needed by the said class. + */ +public interface Transport { + + CompletableFuture sendText(CharSequence message, boolean isLast); + + CompletableFuture sendBinary(ByteBuffer message, boolean isLast); + + CompletableFuture sendPing(ByteBuffer message); + + CompletableFuture sendPong(ByteBuffer message); + + CompletableFuture sendClose(int statusCode, String reason); + + void request(long n); + + /* + * Why is this method needed? Since Receiver operates through callbacks + * this method allows to abstract out what constitutes as a message being + * received (i.e. to decide outside this type when exactly one should + * decrement the demand). + */ + void acknowledgeReception(); + + void closeOutput() throws IOException; + + void closeInput() throws IOException; +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/TransportFactory.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/TransportFactory.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,10 @@ +package jdk.internal.net.http.websocket; + +import java.util.function.Supplier; + +@FunctionalInterface +public interface TransportFactory { + + Transport createTransport(Supplier sendResultSupplier, + MessageStreamConsumer consumer); +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/TransportFactoryImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/TransportFactoryImpl.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.websocket; + +import java.util.function.Supplier; + +public class TransportFactoryImpl implements TransportFactory { + + private final RawChannel channel; + + public TransportFactoryImpl(RawChannel channel) { + this.channel = channel; + } + + @Override + public Transport createTransport(Supplier sendResultSupplier, + MessageStreamConsumer consumer) { + return new TransportImpl(sendResultSupplier, consumer, channel); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/TransportImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/TransportImpl.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,383 @@ +/* + * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import jdk.internal.net.http.common.Demand; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Pair; +import jdk.internal.net.http.common.SequentialScheduler; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.common.MinimalFuture.failedFuture; +import static jdk.internal.net.http.common.Pair.pair; + +public class TransportImpl implements Transport { + + /* This flag is used solely for assertions */ + private final AtomicBoolean busy = new AtomicBoolean(); + private OutgoingMessage message; + private Consumer completionHandler; + private final RawChannel channel; + private final RawChannel.RawEvent writeEvent = createWriteEvent(); + private final SequentialScheduler sendScheduler = new SequentialScheduler(new SendTask()); + private final Queue>> + queue = new ConcurrentLinkedQueue<>(); + private final OutgoingMessage.Context context = new OutgoingMessage.Context(); + private final Supplier resultSupplier; + + private final MessageStreamConsumer messageConsumer; + private final FrameConsumer frameConsumer; + private final Frame.Reader reader = new Frame.Reader(); + private final RawChannel.RawEvent readEvent = createReadEvent(); + private final Demand demand = new Demand(); + private final SequentialScheduler receiveScheduler; + + private ByteBuffer data; + private volatile int state; + + private static final int UNREGISTERED = 0; + private static final int AVAILABLE = 1; + private static final int WAITING = 2; + + private final Object lock = new Object(); + private boolean inputClosed; + private boolean outputClosed; + + public TransportImpl(Supplier sendResultSupplier, + MessageStreamConsumer consumer, + RawChannel channel) { + this.resultSupplier = sendResultSupplier; + this.messageConsumer = consumer; + this.channel = channel; + this.frameConsumer = new FrameConsumer(this.messageConsumer); + this.data = channel.initialByteBuffer(); + // To ensure the initial non-final `data` will be visible + // (happens-before) when `readEvent.handle()` invokes `receiveScheduler` + // the following assignment is done last: + receiveScheduler = new SequentialScheduler(new ReceiveTask()); + } + + /** + * The supplied handler may be invoked in the calling thread. + * A {@code StackOverflowError} may thus occur if there's a possibility + * that this method is called again by the supplied handler. + */ + public void send(OutgoingMessage message, + Consumer completionHandler) { + requireNonNull(message); + requireNonNull(completionHandler); + if (!busy.compareAndSet(false, true)) { + throw new IllegalStateException(); + } + send0(message, completionHandler); + } + + private RawChannel.RawEvent createWriteEvent() { + return new RawChannel.RawEvent() { + + @Override + public int interestOps() { + return SelectionKey.OP_WRITE; + } + + @Override + public void handle() { + // registerEvent(e) happens-before subsequent e.handle(), so + // we're fine reading the stored message and the completionHandler + send0(message, completionHandler); + } + }; + } + + private void send0(OutgoingMessage message, Consumer handler) { + boolean b = busy.get(); + assert b; // Please don't inline this, as busy.get() has memory + // visibility effects and we don't want the program behaviour + // to depend on whether the assertions are turned on + // or turned off + try { + boolean sent = message.sendTo(channel); + if (sent) { + busy.set(false); + handler.accept(null); + } else { + // The message has not been fully sent, the transmitter needs to + // remember the message until it can continue with sending it + this.message = message; + this.completionHandler = handler; + try { + channel.registerEvent(writeEvent); + } catch (IOException e) { + this.message = null; + this.completionHandler = null; + busy.set(false); + handler.accept(e); + } + } + } catch (IOException e) { + busy.set(false); + handler.accept(e); + } + } + + public CompletableFuture sendText(CharSequence message, + boolean isLast) { + OutgoingMessage.Text m; + try { + m = new OutgoingMessage.Text(message, isLast); + } catch (IllegalArgumentException e) { + return failedFuture(e); + } + return enqueue(m); + } + + public CompletableFuture sendBinary(ByteBuffer message, + boolean isLast) { + return enqueue(new OutgoingMessage.Binary(message, isLast)); + } + + public CompletableFuture sendPing(ByteBuffer message) { + OutgoingMessage.Ping m; + try { + m = new OutgoingMessage.Ping(message); + } catch (IllegalArgumentException e) { + return failedFuture(e); + } + return enqueue(m); + } + + public CompletableFuture sendPong(ByteBuffer message) { + OutgoingMessage.Pong m; + try { + m = new OutgoingMessage.Pong(message); + } catch (IllegalArgumentException e) { + return failedFuture(e); + } + return enqueue(m); + } + + public CompletableFuture sendClose(int statusCode, String reason) { + OutgoingMessage.Close m; + try { + m = new OutgoingMessage.Close(statusCode, reason); + } catch (IllegalArgumentException e) { + return failedFuture(e); + } + return enqueue(m); + } + + private CompletableFuture enqueue(OutgoingMessage m) { + CompletableFuture cf = new MinimalFuture<>(); + boolean added = queue.add(pair(m, cf)); + if (!added) { + // The queue is supposed to be unbounded + throw new InternalError(); + } + sendScheduler.runOrSchedule(); + return cf; + } + + /* + * This is a message sending task. It pulls messages from the queue one by + * one and sends them. It may be run in different threads, but never + * concurrently. + */ + private class SendTask implements SequentialScheduler.RestartableTask { + + @Override + public void run(SequentialScheduler.DeferredCompleter taskCompleter) { + Pair> p = queue.poll(); + if (p == null) { + taskCompleter.complete(); + return; + } + OutgoingMessage message = p.first; + CompletableFuture cf = p.second; + try { + if (!message.contextualize(context)) { // Do not send the message + cf.complete(resultSupplier.get()); + repeat(taskCompleter); + return; + } + Consumer h = e -> { + if (e == null) { + cf.complete(resultSupplier.get()); + } else { + cf.completeExceptionally(e); + } + repeat(taskCompleter); + }; + send(message, h); + } catch (Throwable t) { + cf.completeExceptionally(t); + repeat(taskCompleter); + } + } + + private void repeat(SequentialScheduler.DeferredCompleter taskCompleter) { + taskCompleter.complete(); + // More than a single message may have been enqueued while + // the task has been busy with the current message, but + // there is only a single signal recorded + sendScheduler.runOrSchedule(); + } + } + + private RawChannel.RawEvent createReadEvent() { + return new RawChannel.RawEvent() { + + @Override + public int interestOps() { + return SelectionKey.OP_READ; + } + + @Override + public void handle() { + state = AVAILABLE; + receiveScheduler.runOrSchedule(); + } + }; + } + + @Override + public void request(long n) { + if (demand.increase(n)) { + receiveScheduler.runOrSchedule(); + } + } + + @Override + public void acknowledgeReception() { + boolean decremented = demand.tryDecrement(); + if (!decremented) { + throw new InternalError(); + } + } + + private class ReceiveTask extends SequentialScheduler.CompleteRestartableTask { + + @Override + public void run() { + while (!receiveScheduler.isStopped()) { + if (data.hasRemaining()) { + if (!demand.isFulfilled()) { + try { + int oldPos = data.position(); + reader.readFrame(data, frameConsumer); + int newPos = data.position(); + assert oldPos != newPos : data; // reader always consumes bytes + } catch (Throwable e) { + receiveScheduler.stop(); + messageConsumer.onError(e); + } + continue; + } + break; + } + switch (state) { + case WAITING: + return; + case UNREGISTERED: + try { + state = WAITING; + channel.registerEvent(readEvent); + } catch (Throwable e) { + receiveScheduler.stop(); + messageConsumer.onError(e); + } + return; + case AVAILABLE: + try { + data = channel.read(); + } catch (Throwable e) { + receiveScheduler.stop(); + messageConsumer.onError(e); + return; + } + if (data == null) { // EOF + receiveScheduler.stop(); + messageConsumer.onComplete(); + return; + } else if (!data.hasRemaining()) { + // No data at the moment Pretty much a "goto", + // reusing the existing code path for registration + state = UNREGISTERED; + } + continue; + default: + throw new InternalError(String.valueOf(state)); + } + } + } + } + + /* + * Permanently stops reading from the channel and delivering messages + * regardless of the current demand and data availability. + */ + @Override + public void closeInput() throws IOException { + synchronized (lock) { + if (!inputClosed) { + inputClosed = true; + try { + receiveScheduler.stop(); + channel.shutdownInput(); + } finally { + if (outputClosed) { + channel.close(); + } + } + } + } + } + + @Override + public void closeOutput() throws IOException { + synchronized (lock) { + if (!outputClosed) { + outputClosed = true; + try { + channel.shutdownOutput(); + } finally { + if (inputClosed) { + channel.close(); + } + } + } + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/UTF8AccumulatingDecoder.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/UTF8AccumulatingDecoder.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import jdk.internal.net.http.common.Log; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static jdk.internal.net.http.common.Utils.EMPTY_BYTEBUFFER; + +final class UTF8AccumulatingDecoder { + + private final CharsetDecoder decoder = UTF_8.newDecoder(); + + { + decoder.onMalformedInput(CodingErrorAction.REPORT); + decoder.onUnmappableCharacter(CodingErrorAction.REPORT); + } + + private ByteBuffer leftovers = EMPTY_BYTEBUFFER; + + CharBuffer decode(ByteBuffer in, boolean endOfInput) + throws CharacterCodingException + { + ByteBuffer b; + int rem = leftovers.remaining(); + if (rem != 0) { + // We won't need this wasteful allocation & copying when JDK-8155222 + // has been resolved + b = ByteBuffer.allocate(rem + in.remaining()); + b.put(leftovers).put(in).flip(); + } else { + b = in; + } + CharBuffer out = CharBuffer.allocate(b.remaining()); + CoderResult r = decoder.decode(b, out, endOfInput); + if (r.isError()) { + r.throwException(); + } + if (b.hasRemaining()) { + leftovers = ByteBuffer.allocate(b.remaining()).put(b).flip(); + } else { + leftovers = EMPTY_BYTEBUFFER; + } + // Since it's UTF-8, the assumption is leftovers.remaining() < 4 + // (i.e. small). Otherwise a shared buffer should be used + if (!(leftovers.remaining() < 4)) { + Log.logError("The size of decoding leftovers is greater than expected: {0}", + leftovers.remaining()); + } + b.position(b.limit()); // As if we always read to the end + // Decoder promises that in the case of endOfInput == true: + // "...any remaining undecoded input will be treated as being + // malformed" + assert !(endOfInput && leftovers.hasRemaining()) : endOfInput + ", " + leftovers; + if (endOfInput) { + r = decoder.flush(out); + decoder.reset(); + if (r.isOverflow()) { + // FIXME: for now I know flush() does nothing. But the + // implementation of UTF8 decoder might change. And if now + // flush() is a no-op, it is not guaranteed to remain so in + // the future + throw new InternalError("Not yet implemented"); + } + } + return out.flip(); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/WebSocketImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/WebSocketImpl.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,533 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import java.net.http.WebSocket; +import jdk.internal.net.http.common.Demand; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.websocket.OpeningHandshake.Result; + +import java.io.IOException; +import java.lang.ref.Reference; +import java.net.ProtocolException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.common.MinimalFuture.failedFuture; +import static jdk.internal.net.http.websocket.StatusCodes.CLOSED_ABNORMALLY; +import static jdk.internal.net.http.websocket.StatusCodes.NO_STATUS_CODE; +import static jdk.internal.net.http.websocket.StatusCodes.isLegalToSendFromClient; +import static jdk.internal.net.http.websocket.WebSocketImpl.State.BINARY; +import static jdk.internal.net.http.websocket.WebSocketImpl.State.CLOSE; +import static jdk.internal.net.http.websocket.WebSocketImpl.State.ERROR; +import static jdk.internal.net.http.websocket.WebSocketImpl.State.IDLE; +import static jdk.internal.net.http.websocket.WebSocketImpl.State.OPEN; +import static jdk.internal.net.http.websocket.WebSocketImpl.State.PING; +import static jdk.internal.net.http.websocket.WebSocketImpl.State.PONG; +import static jdk.internal.net.http.websocket.WebSocketImpl.State.TEXT; +import static jdk.internal.net.http.websocket.WebSocketImpl.State.WAITING; + +/* + * A WebSocket client. + */ +public final class WebSocketImpl implements WebSocket { + + enum State { + OPEN, + IDLE, + WAITING, + TEXT, + BINARY, + PING, + PONG, + CLOSE, + ERROR; + } + + private volatile boolean inputClosed; + private volatile boolean outputClosed; + + private final AtomicReference state = new AtomicReference<>(OPEN); + + /* Components of calls to Listener's methods */ + private MessagePart part; + private ByteBuffer binaryData; + private CharSequence text; + private int statusCode; + private String reason; + private final AtomicReference error = new AtomicReference<>(); + + private final URI uri; + private final String subprotocol; + private final Listener listener; + + private final AtomicBoolean outstandingSend = new AtomicBoolean(); + private final Transport transport; + private final SequentialScheduler receiveScheduler = new SequentialScheduler(new ReceiveTask()); + private final Demand demand = new Demand(); + + public static CompletableFuture newInstanceAsync(BuilderImpl b) { + Function newWebSocket = r -> { + WebSocket ws = newInstance(b.getUri(), + r.subprotocol, + b.getListener(), + r.transport); + // Make sure we don't release the builder until this lambda + // has been executed. The builder has a strong reference to + // the HttpClientFacade, and we want to keep that live until + // after the raw channel is created and passed to WebSocketImpl. + Reference.reachabilityFence(b); + return ws; + }; + OpeningHandshake h; + try { + h = new OpeningHandshake(b); + } catch (Throwable e) { + return failedFuture(e); + } + return h.send().thenApply(newWebSocket); + } + + /* Exposed for testing purposes */ + static WebSocketImpl newInstance(URI uri, + String subprotocol, + Listener listener, + TransportFactory transport) { + WebSocketImpl ws = new WebSocketImpl(uri, subprotocol, listener, transport); + // This initialisation is outside of the constructor for the sake of + // safe publication of WebSocketImpl.this + ws.signalOpen(); + return ws; + } + + private WebSocketImpl(URI uri, + String subprotocol, + Listener listener, + TransportFactory transportFactory) { + this.uri = requireNonNull(uri); + this.subprotocol = requireNonNull(subprotocol); + this.listener = requireNonNull(listener); + this.transport = transportFactory.createTransport( + () -> WebSocketImpl.this, // What about escape of WebSocketImpl.this? + new SignallingMessageConsumer()); + } + + @Override + public CompletableFuture sendText(CharSequence message, + boolean isLast) { + Objects.requireNonNull(message); + if (!outstandingSend.compareAndSet(false, true)) { + return failedFuture(new IllegalStateException("Send pending")); + } + CompletableFuture cf = transport.sendText(message, isLast); + return cf.whenComplete((r, e) -> outstandingSend.set(false)); + } + + @Override + public CompletableFuture sendBinary(ByteBuffer message, + boolean isLast) { + Objects.requireNonNull(message); + if (!outstandingSend.compareAndSet(false, true)) { + return failedFuture(new IllegalStateException("Send pending")); + } + CompletableFuture cf = transport.sendBinary(message, isLast); + // Optimize? + // if (cf.isDone()) { + // outstandingSend.set(false); + // } else { + // cf.whenComplete((r, e) -> outstandingSend.set(false)); + // } + return cf.whenComplete((r, e) -> outstandingSend.set(false)); + } + + @Override + public CompletableFuture sendPing(ByteBuffer message) { + return transport.sendPing(message); + } + + @Override + public CompletableFuture sendPong(ByteBuffer message) { + return transport.sendPong(message); + } + + @Override + public CompletableFuture sendClose(int statusCode, String reason) { + Objects.requireNonNull(reason); + if (!isLegalToSendFromClient(statusCode)) { + return failedFuture(new IllegalArgumentException("statusCode")); + } + return sendClose0(statusCode, reason); + } + + /* + * Sends a Close message, then shuts down the output since no more + * messages are expected to be sent after this. + */ + private CompletableFuture sendClose0(int statusCode, String reason ) { + outputClosed = true; + return transport.sendClose(statusCode, reason) + .whenComplete((result, error) -> { + try { + transport.closeOutput(); + } catch (IOException e) { + Log.logError(e); + } + Throwable cause = Utils.getCompletionCause(error); + if (cause instanceof TimeoutException) { + try { + transport.closeInput(); + } catch (IOException e) { + Log.logError(e); + } + } + }); + } + + @Override + public void request(long n) { + if (demand.increase(n)) { + receiveScheduler.runOrSchedule(); + } + } + + @Override + public String getSubprotocol() { + return subprotocol; + } + + @Override + public boolean isOutputClosed() { + return outputClosed; + } + + @Override + public boolean isInputClosed() { + return inputClosed; + } + + @Override + public void abort() { + inputClosed = true; + outputClosed = true; + receiveScheduler.stop(); + close(); + } + + @Override + public String toString() { + return super.toString() + + "[uri=" + uri + + (!subprotocol.isEmpty() ? ", subprotocol=" + subprotocol : "") + + "]"; + } + + /* + * The assumptions about order is as follows: + * + * - state is never changed more than twice inside the `run` method: + * x --(1)--> IDLE --(2)--> y (otherwise we're loosing events, or + * overwriting parts of messages creating a mess since there's no + * queueing) + * - OPEN is always the first state + * - no messages are requested/delivered before onOpen is called (this + * is implemented by making WebSocket instance accessible first in + * onOpen) + * - after the state has been observed as CLOSE/ERROR, the scheduler + * is stopped + */ + private class ReceiveTask extends SequentialScheduler.CompleteRestartableTask { + + // Transport only asked here and nowhere else because we must make sure + // onOpen is invoked first and no messages become pending before onOpen + // finishes + + @Override + public void run() { + while (true) { + State s = state.get(); + try { + switch (s) { + case OPEN: + processOpen(); + tryChangeState(OPEN, IDLE); + break; + case TEXT: + processText(); + tryChangeState(TEXT, IDLE); + break; + case BINARY: + processBinary(); + tryChangeState(BINARY, IDLE); + break; + case PING: + processPing(); + tryChangeState(PING, IDLE); + break; + case PONG: + processPong(); + tryChangeState(PONG, IDLE); + break; + case CLOSE: + processClose(); + return; + case ERROR: + processError(); + return; + case IDLE: + if (demand.tryDecrement() + && tryChangeState(IDLE, WAITING)) { + transport.request(1); + } + return; + case WAITING: + // For debugging spurious signalling: when there was a + // signal, but apparently nothing has changed + return; + default: + throw new InternalError(String.valueOf(s)); + } + } catch (Throwable t) { + signalError(t); + } + } + } + + private void processError() throws IOException { + transport.closeInput(); + receiveScheduler.stop(); + Throwable err = error.get(); + if (err instanceof FailWebSocketException) { + int code1 = ((FailWebSocketException) err).getStatusCode(); + err = new ProtocolException().initCause(err); + sendClose0(code1, "") + .whenComplete( + (r, e) -> { + if (e != null) { + Log.logError(e); + } + }); + } + listener.onError(WebSocketImpl.this, err); + } + + private void processClose() throws IOException { + transport.closeInput(); + receiveScheduler.stop(); + CompletionStage readyToClose; + readyToClose = listener.onClose(WebSocketImpl.this, statusCode, reason); + if (readyToClose == null) { + readyToClose = MinimalFuture.completedFuture(null); + } + int code; + if (statusCode == NO_STATUS_CODE || statusCode == CLOSED_ABNORMALLY) { + code = NORMAL_CLOSURE; + } else { + code = statusCode; + } + readyToClose.whenComplete((r, e) -> { + sendClose0(code, "") + .whenComplete((r1, e1) -> { + if (e1 != null) { + Log.logError(e1); + } + }); + }); + } + + private void processPong() { + listener.onPong(WebSocketImpl.this, binaryData); + } + + private void processPing() { + // Let's make a full copy of this tiny data. What we want here + // is to rule out a possibility the shared data we send might be + // corrupted by processing in the listener. + ByteBuffer slice = binaryData.slice(); + ByteBuffer copy = ByteBuffer.allocate(binaryData.remaining()) + .put(binaryData) + .flip(); + // Non-exclusive send; + CompletableFuture pongSent = transport.sendPong(copy); + pongSent.whenComplete( + (r, e) -> { + if (e != null) { + signalError(Utils.getCompletionCause(e)); + } + } + ); + listener.onPing(WebSocketImpl.this, slice); + } + + private void processBinary() { + listener.onBinary(WebSocketImpl.this, binaryData, part); + } + + private void processText() { + listener.onText(WebSocketImpl.this, text, part); + } + + private void processOpen() { + listener.onOpen(WebSocketImpl.this); + } + } + + private void signalOpen() { + receiveScheduler.runOrSchedule(); + } + + private void signalError(Throwable error) { + inputClosed = true; + outputClosed = true; + if (!this.error.compareAndSet(null, error) || !trySetState(ERROR)) { + Log.logError(error); + } else { + close(); + } + } + + private void close() { + try { + try { + transport.closeInput(); + } finally { + transport.closeOutput(); + } + } catch (Throwable t) { + Log.logError(t); + } + } + + /* + * Signals a Close event (might not correspond to anything happened on the + * channel, i.e. might be synthetic). + */ + private void signalClose(int statusCode, String reason) { + inputClosed = true; + this.statusCode = statusCode; + this.reason = reason; + if (!trySetState(CLOSE)) { + Log.logTrace("Close: {0}, ''{1}''", statusCode, reason); + } else { + try { + transport.closeInput(); + } catch (Throwable t) { + Log.logError(t); + } + } + } + + private class SignallingMessageConsumer implements MessageStreamConsumer { + + @Override + public void onText(CharSequence data, MessagePart part) { + transport.acknowledgeReception(); + text = data; + WebSocketImpl.this.part = part; + tryChangeState(WAITING, TEXT); + } + + @Override + public void onBinary(ByteBuffer data, MessagePart part) { + transport.acknowledgeReception(); + binaryData = data; + WebSocketImpl.this.part = part; + tryChangeState(WAITING, BINARY); + } + + @Override + public void onPing(ByteBuffer data) { + transport.acknowledgeReception(); + binaryData = data; + tryChangeState(WAITING, PING); + } + + @Override + public void onPong(ByteBuffer data) { + transport.acknowledgeReception(); + binaryData = data; + tryChangeState(WAITING, PONG); + } + + @Override + public void onClose(int statusCode, CharSequence reason) { + transport.acknowledgeReception(); + signalClose(statusCode, reason.toString()); + } + + @Override + public void onComplete() { + transport.acknowledgeReception(); + signalClose(CLOSED_ABNORMALLY, ""); + } + + @Override + public void onError(Throwable error) { + signalError(error); + } + } + + private boolean trySetState(State newState) { + while (true) { + State currentState = state.get(); + if (currentState == ERROR || currentState == CLOSE) { + return false; + } else if (state.compareAndSet(currentState, newState)) { + receiveScheduler.runOrSchedule(); + return true; + } + } + } + + private boolean tryChangeState(State expectedState, State newState) { + State witness = state.compareAndExchange(expectedState, newState); + if (witness == expectedState) { + receiveScheduler.runOrSchedule(); + return true; + } + // This should be the only reason for inability to change the state from + // IDLE to WAITING: the state has changed to terminal + if (witness != ERROR && witness != CLOSE) { + throw new InternalError(); + } + return false; + } + + /* Exposed for testing purposes */ + protected final Transport transport() { + return transport; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d src/java.net.http/share/classes/jdk/internal/net/http/websocket/WebSocketRequest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/websocket/WebSocketRequest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import java.net.Proxy; + +/* + * https://tools.ietf.org/html/rfc6455#section-4.1 + */ +public interface WebSocketRequest { + + /* + * If set to `true` and a proxy is used, instructs the implementation that + * a TCP tunnel must be opened. + */ + void isWebSocket(boolean flag); + + /* + * Needed for setting "Connection" and "Upgrade" headers as required by the + * WebSocket specification. + */ + void setSystemHeader(String name, String value); + + /* + * Sets the proxy for this request. + */ + void setProxy(Proxy proxy); +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/ConcurrentResponses.java --- a/test/jdk/java/net/httpclient/ConcurrentResponses.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/ConcurrentResponses.java Wed Feb 07 21:45:37 2018 +0000 @@ -27,9 +27,9 @@ * @summary Buffers given to response body subscribers should not contain * unprocessed HTTP data * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * java.logging * jdk.httpserver * @library /lib/testlibrary http2/server diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/CustomRequestPublisher.java --- a/test/jdk/java/net/httpclient/CustomRequestPublisher.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/CustomRequestPublisher.java Wed Feb 07 21:45:37 2018 +0000 @@ -25,9 +25,9 @@ * @test * @summary Checks correct handling of Publishers that call onComplete without demand * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * java.logging * jdk.httpserver * @library /lib/testlibrary http2/server diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/CustomResponseSubscriber.java --- a/test/jdk/java/net/httpclient/CustomResponseSubscriber.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/CustomResponseSubscriber.java Wed Feb 07 21:45:37 2018 +0000 @@ -27,9 +27,9 @@ * @library /lib/testlibrary http2/server * @build jdk.testlibrary.SimpleSSLContext * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * @run testng/othervm CustomResponseSubscriber */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/DigestEchoClient.java --- a/test/jdk/java/net/httpclient/DigestEchoClient.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/DigestEchoClient.java Wed Feb 07 21:45:37 2018 +0000 @@ -63,9 +63,9 @@ * @bug 8087112 * @library /lib/testlibrary http2/server * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters DigestEchoServer DigestEchoClient - * @modules java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * @modules java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * java.logging * java.base/sun.net.www.http * java.base/sun.net.www diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/DigestEchoClientSSL.java --- a/test/jdk/java/net/httpclient/DigestEchoClientSSL.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/DigestEchoClientSSL.java Wed Feb 07 21:45:37 2018 +0000 @@ -28,9 +28,9 @@ * headers directly when connecting with a server over SSL. * @library /lib/testlibrary http2/server * @build jdk.testlibrary.SimpleSSLContext DigestEchoServer DigestEchoClient DigestEchoClientSSL - * @modules java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * @modules java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * java.logging * java.base/sun.net.www.http * java.base/sun.net.www diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java --- a/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -61,9 +61,9 @@ * @test * @summary Basic tests for Flow adapter Publishers * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * java.logging * jdk.httpserver * @library /lib/testlibrary http2/server diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/FlowAdapterSubscriberTest.java --- a/test/jdk/java/net/httpclient/FlowAdapterSubscriberTest.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/FlowAdapterSubscriberTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -64,9 +64,9 @@ * @test * @summary Basic tests for Flow adapter Subscribers * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * java.logging * jdk.httpserver * @library /lib/testlibrary http2/server diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/HttpServerAdapters.java --- a/test/jdk/java/net/httpclient/HttpServerAdapters.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/HttpServerAdapters.java Wed Feb 07 21:45:37 2018 +0000 @@ -28,7 +28,7 @@ import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import java.net.http.HttpClient.Version; -import java.net.http.internal.common.HttpHeadersImpl; +import jdk.internal.net.http.common.HttpHeadersImpl; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/ImmutableFlowItems.java --- a/test/jdk/java/net/httpclient/ImmutableFlowItems.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/ImmutableFlowItems.java Wed Feb 07 21:45:37 2018 +0000 @@ -28,9 +28,9 @@ * @library /lib/testlibrary http2/server * @build jdk.testlibrary.SimpleSSLContext * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * @run testng/othervm ImmutableFlowItems */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/LineBodyHandlerTest.java --- a/test/jdk/java/net/httpclient/LineBodyHandlerTest.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/LineBodyHandlerTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -75,9 +75,9 @@ * the BodyHandlers returned by BodyHandler::fromLineSubscriber * and BodyHandler::asLines * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * java.logging * jdk.httpserver * @library /lib/testlibrary http2/server diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/MappedResponseSubscriber.java --- a/test/jdk/java/net/httpclient/MappedResponseSubscriber.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/MappedResponseSubscriber.java Wed Feb 07 21:45:37 2018 +0000 @@ -27,9 +27,9 @@ * @library /lib/testlibrary http2/server * @build jdk.testlibrary.SimpleSSLContext * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * @run testng/othervm MappedResponseSubscriber */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/NoBodyPartOne.java --- a/test/jdk/java/net/httpclient/NoBodyPartOne.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/NoBodyPartOne.java Wed Feb 07 21:45:37 2018 +0000 @@ -28,9 +28,9 @@ * @library /lib/testlibrary http2/server * @build jdk.testlibrary.SimpleSSLContext * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * @run testng/othervm -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=all NoBodyPartOne */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/NoBodyPartTwo.java --- a/test/jdk/java/net/httpclient/NoBodyPartTwo.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/NoBodyPartTwo.java Wed Feb 07 21:45:37 2018 +0000 @@ -28,9 +28,9 @@ * @library /lib/testlibrary http2/server * @build jdk.testlibrary.SimpleSSLContext * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * @run testng/othervm -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=all NoBodyPartTwo */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/ProxyAuthDisabledSchemes.java --- a/test/jdk/java/net/httpclient/ProxyAuthDisabledSchemes.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/ProxyAuthDisabledSchemes.java Wed Feb 07 21:45:37 2018 +0000 @@ -30,9 +30,9 @@ * @bug 8087112 * @library /lib/testlibrary http2/server * @build jdk.testlibrary.SimpleSSLContext DigestEchoServer DigestEchoClient ProxyAuthDisabledSchemes - * @modules java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * @modules java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * java.logging * java.base/sun.net.www.http * java.base/sun.net.www diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/ProxyAuthDisabledSchemesSSL.java --- a/test/jdk/java/net/httpclient/ProxyAuthDisabledSchemesSSL.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/ProxyAuthDisabledSchemesSSL.java Wed Feb 07 21:45:37 2018 +0000 @@ -30,9 +30,9 @@ * net properties. * @library /lib/testlibrary http2/server * @build jdk.testlibrary.SimpleSSLContext DigestEchoServer DigestEchoClient ProxyAuthDisabledSchemesSSL - * @modules java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * @modules java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * java.logging * java.base/sun.net.www.http * java.base/sun.net.www diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/SmallTimeout.java --- a/test/jdk/java/net/httpclient/SmallTimeout.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/SmallTimeout.java Wed Feb 07 21:45:37 2018 +0000 @@ -40,7 +40,7 @@ * @test * @bug 8178147 * @summary Ensures that small timeouts do not cause hangs due to race conditions - * @run main/othervm -Djava.net.http.internal.common.DEBUG=true SmallTimeout + * @run main/othervm -Djdk.internal.net.http.common.DEBUG=true SmallTimeout */ // To enable logging use. Not enabled by default as it changes the dynamics diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/BasicTest.java --- a/test/jdk/java/net/httpclient/http2/BasicTest.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/BasicTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -27,9 +27,9 @@ * @library /lib/testlibrary server * @build jdk.testlibrary.SimpleSSLContext * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors BasicTest */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java --- a/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -25,9 +25,9 @@ * @test * @summary Test for CONTINUATION frame handling * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * @library /lib/testlibrary server * @build Http2TestServer * @build jdk.testlibrary.SimpleSSLContext @@ -47,11 +47,11 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.net.http.internal.common.HttpHeadersImpl; -import java.net.http.internal.frame.ContinuationFrame; -import java.net.http.internal.frame.HeaderFrame; -import java.net.http.internal.frame.HeadersFrame; -import java.net.http.internal.frame.Http2Frame; +import jdk.internal.net.http.common.HttpHeadersImpl; +import jdk.internal.net.http.frame.ContinuationFrame; +import jdk.internal.net.http.frame.HeaderFrame; +import jdk.internal.net.http.frame.HeadersFrame; +import jdk.internal.net.http.frame.Http2Frame; import jdk.testlibrary.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/ErrorTest.java --- a/test/jdk/java/net/httpclient/http2/ErrorTest.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/ErrorTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -27,9 +27,9 @@ * @library /lib/testlibrary server * @build jdk.testlibrary.SimpleSSLContext * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * java.security.jgss * @run testng/othervm/timeout=60 -Djavax.net.debug=ssl -Djdk.httpclient.HttpClient.log=all ErrorTest * @summary check exception thrown when bad TLS parameters selected diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/FixedThreadPoolTest.java --- a/test/jdk/java/net/httpclient/http2/FixedThreadPoolTest.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/FixedThreadPoolTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -27,9 +27,9 @@ * @library /lib/testlibrary server * @build jdk.testlibrary.SimpleSSLContext * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors FixedThreadPoolTest */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/HpackBinaryTestDriver.java --- a/test/jdk/java/net/httpclient/http2/HpackBinaryTestDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/HpackBinaryTestDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -24,11 +24,11 @@ /* * @test * @bug 8153353 - * @modules java.net.http/java.net.http.internal.hpack + * @modules java.net.http/jdk.internal.net.http.hpack * @key randomness - * @compile/module=java.net.http java/net/http/internal/hpack/SpecHelper.java - * @compile/module=java.net.http java/net/http/internal/hpack/TestHelper.java - * @compile/module=java.net.http java/net/http/internal/hpack/BuffersTestingKit.java - * @run testng/othervm java.net.http/java.net.http.internal.hpack.BinaryPrimitivesTest + * @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java + * @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java + * @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java + * @run testng/othervm java.net.http/jdk.internal.net.http.hpack.BinaryPrimitivesTest */ public class HpackBinaryTestDriver { } diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/HpackCircularBufferDriver.java --- a/test/jdk/java/net/httpclient/http2/HpackCircularBufferDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/HpackCircularBufferDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -24,11 +24,11 @@ /* * @test * @bug 8153353 - * @modules java.net.http/java.net.http.internal.hpack + * @modules java.net.http/jdk.internal.net.http.hpack * @key randomness - * @compile/module=java.net.http java/net/http/internal/hpack/SpecHelper.java - * @compile/module=java.net.http java/net/http/internal/hpack/TestHelper.java - * @compile/module=java.net.http java/net/http/internal/hpack/BuffersTestingKit.java - * @run testng/othervm java.net.http/java.net.http.internal.hpack.CircularBufferTest + * @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java + * @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java + * @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java + * @run testng/othervm java.net.http/jdk.internal.net.http.hpack.CircularBufferTest */ public class HpackCircularBufferDriver { } diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/HpackDecoderDriver.java --- a/test/jdk/java/net/httpclient/http2/HpackDecoderDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/HpackDecoderDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -24,11 +24,11 @@ /* * @test * @bug 8153353 - * @modules java.net.http/java.net.http.internal.hpack + * @modules java.net.http/jdk.internal.net.http.hpack * @key randomness - * @compile/module=java.net.http java/net/http/internal/hpack/SpecHelper.java - * @compile/module=java.net.http java/net/http/internal/hpack/TestHelper.java - * @compile/module=java.net.http java/net/http/internal/hpack/BuffersTestingKit.java - * @run testng/othervm java.net.http/java.net.http.internal.hpack.DecoderTest + * @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java + * @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java + * @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java + * @run testng/othervm java.net.http/jdk.internal.net.http.hpack.DecoderTest */ public class HpackDecoderDriver { } diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/HpackEncoderDriver.java --- a/test/jdk/java/net/httpclient/http2/HpackEncoderDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/HpackEncoderDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -24,11 +24,11 @@ /* * @test * @bug 8153353 - * @modules java.net.http/java.net.http.internal.hpack + * @modules java.net.http/jdk.internal.net.http.hpack * @key randomness - * @compile/module=java.net.http java/net/http/internal/hpack/SpecHelper.java - * @compile/module=java.net.http java/net/http/internal/hpack/TestHelper.java - * @compile/module=java.net.http java/net/http/internal/hpack/BuffersTestingKit.java - * @run testng/othervm java.net.http/java.net.http.internal.hpack.EncoderTest + * @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java + * @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java + * @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java + * @run testng/othervm java.net.http/jdk.internal.net.http.hpack.EncoderTest */ public class HpackEncoderDriver { } diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/HpackHeaderTableDriver.java --- a/test/jdk/java/net/httpclient/http2/HpackHeaderTableDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/HpackHeaderTableDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -24,12 +24,12 @@ /* * @test * @bug 8153353 - * @modules java.net.http/java.net.http.internal.hpack + * @modules java.net.http/jdk.internal.net.http.hpack * jdk.localedata * @key randomness - * @compile/module=java.net.http java/net/http/internal/hpack/SpecHelper.java - * @compile/module=java.net.http java/net/http/internal/hpack/TestHelper.java - * @compile/module=java.net.http java/net/http/internal/hpack/BuffersTestingKit.java - * @run testng/othervm java.net.http/java.net.http.internal.hpack.HeaderTableTest + * @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java + * @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java + * @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java + * @run testng/othervm java.net.http/jdk.internal.net.http.hpack.HeaderTableTest */ public class HpackHeaderTableDriver { } diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/HpackHuffmanDriver.java --- a/test/jdk/java/net/httpclient/http2/HpackHuffmanDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/HpackHuffmanDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -24,11 +24,11 @@ /* * @test * @bug 8153353 - * @modules java.net.http/java.net.http.internal.hpack + * @modules java.net.http/jdk.internal.net.http.hpack * @key randomness - * @compile/module=java.net.http java/net/http/internal/hpack/SpecHelper.java - * @compile/module=java.net.http java/net/http/internal/hpack/TestHelper.java - * @compile/module=java.net.http java/net/http/internal/hpack/BuffersTestingKit.java - * @run testng/othervm java.net.http/java.net.http.internal.hpack.HuffmanTest + * @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java + * @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java + * @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java + * @run testng/othervm java.net.http/jdk.internal.net.http.hpack.HuffmanTest */ public class HpackHuffmanDriver { } diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/HpackTestHelper.java --- a/test/jdk/java/net/httpclient/http2/HpackTestHelper.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/HpackTestHelper.java Wed Feb 07 21:45:37 2018 +0000 @@ -24,11 +24,11 @@ /* * @test * @bug 8153353 - * @modules java.net.http/java.net.http.internal.hpack + * @modules java.net.http/jdk.internal.net.http.hpack * @key randomness - * @compile/module=java.net.http java/net/http/internal/hpack/SpecHelper.java - * @compile/module=java.net.http java/net/http/internal/hpack/TestHelper.java - * @compile/module=java.net.http java/net/http/internal/hpack/BuffersTestingKit.java - * @run testng/othervm java.net.http/java.net.http.internal.hpack.TestHelper + * @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java + * @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java + * @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java + * @run testng/othervm java.net.http/jdk.internal.net.http.hpack.TestHelper */ public class HpackTestHelperDriver { } diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/ImplicitPushCancel.java --- a/test/jdk/java/net/httpclient/http2/ImplicitPushCancel.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/ImplicitPushCancel.java Wed Feb 07 21:45:37 2018 +0000 @@ -26,9 +26,9 @@ * @library /lib/testlibrary server * @build jdk.testlibrary.SimpleSSLContext * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * @run testng/othervm -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=errors,requests,responses,trace ImplicitPushCancel */ @@ -48,7 +48,7 @@ import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.PushPromiseHandler; -import java.net.http.internal.common.HttpHeadersImpl; +import jdk.internal.net.http.common.HttpHeadersImpl; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/ProxyTest2.java --- a/test/jdk/java/net/httpclient/http2/ProxyTest2.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/ProxyTest2.java Wed Feb 07 21:45:37 2018 +0000 @@ -63,9 +63,9 @@ * @modules java.net.http * @library /lib/testlibrary server * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * @build jdk.testlibrary.SimpleSSLContext ProxyTest2 * @run main/othervm ProxyTest2 * @author danielfuchs diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/RedirectTest.java --- a/test/jdk/java/net/httpclient/http2/RedirectTest.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/RedirectTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -27,9 +27,9 @@ * @library /lib/testlibrary server * @build jdk.testlibrary.SimpleSSLContext * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * @run testng/othervm -Djdk.httpclient.HttpClient.log=frames,ssl,requests,responses,errors RedirectTest */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/ServerPush.java --- a/test/jdk/java/net/httpclient/http2/ServerPush.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/ServerPush.java Wed Feb 07 21:45:37 2018 +0000 @@ -27,9 +27,9 @@ * @library /lib/testlibrary server * @build jdk.testlibrary.SimpleSSLContext * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors,requests,responses ServerPush */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/ServerPushWithDiffTypes.java --- a/test/jdk/java/net/httpclient/http2/ServerPushWithDiffTypes.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/ServerPushWithDiffTypes.java Wed Feb 07 21:45:37 2018 +0000 @@ -26,9 +26,9 @@ * @library /lib/testlibrary server * @build jdk.testlibrary.SimpleSSLContext * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * @run testng/othervm * -Djdk.internal.httpclient.debug=true * -Djdk.httpclient.HttpClient.log=errors,requests,responses @@ -45,7 +45,7 @@ import java.net.http.HttpResponse.BodySubscriber; import java.util.*; import java.util.concurrent.*; -import java.net.http.internal.common.HttpHeadersImpl; +import jdk.internal.net.http.common.HttpHeadersImpl; import org.testng.annotations.Test; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/TLSConnection.java --- a/test/jdk/java/net/httpclient/http2/TLSConnection.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/TLSConnection.java Wed Feb 07 21:45:37 2018 +0000 @@ -41,9 +41,9 @@ * @library server * @summary Checks that SSL parameters can be set for HTTP/2 connection * @modules java.base/sun.net.www.http - * java.net.http/java.net.http.internal.common - * java.net.http/java.net.http.internal.frame - * java.net.http/java.net.http.internal.hpack + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack * @run main/othervm * -Djdk.internal.httpclient.debug=true * -Djdk.httpclient.HttpClient.log=all @@ -251,5 +251,4 @@ return true; } } - -} \ No newline at end of file +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/BinaryPrimitivesTest.java --- a/test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/BinaryPrimitivesTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,369 +0,0 @@ -/* - * Copyright (c) 2014, 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 java.net.http.internal.hpack; - -import org.testng.annotations.Test; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.fail; -import static java.net.http.internal.hpack.BuffersTestingKit.*; -import static java.net.http.internal.hpack.TestHelper.newRandom; - -// -// Some of the tests below overlap in what they test. This allows to diagnose -// bugs quicker and with less pain by simply ruling out common working bits. -// -public final class BinaryPrimitivesTest { - - private final Random rnd = newRandom(); - - @Test - public void integerRead1() { - verifyRead(bytes(0b00011111, 0b10011010, 0b00001010), 1337, 5); - } - - @Test - public void integerRead2() { - verifyRead(bytes(0b00001010), 10, 5); - } - - @Test - public void integerRead3() { - verifyRead(bytes(0b00101010), 42, 8); - } - - @Test - public void integerWrite1() { - verifyWrite(bytes(0b00011111, 0b10011010, 0b00001010), 1337, 5); - } - - @Test - public void integerWrite2() { - verifyWrite(bytes(0b00001010), 10, 5); - } - - @Test - public void integerWrite3() { - verifyWrite(bytes(0b00101010), 42, 8); - } - - // - // Since readInteger(x) is the inverse of writeInteger(x), thus: - // - // for all x: readInteger(writeInteger(x)) == x - // - @Test - public void integerIdentity() throws IOException { - final int MAX_VALUE = 1 << 22; - int totalCases = 0; - int maxFilling = 0; - IntegerReader r = new IntegerReader(); - IntegerWriter w = new IntegerWriter(); - ByteBuffer buf = ByteBuffer.allocate(8); - for (int N = 1; N < 9; N++) { - for (int expected = 0; expected <= MAX_VALUE; expected++) { - w.reset().configure(expected, N, 1).write(buf); - buf.flip(); - totalCases++; - maxFilling = Math.max(maxFilling, buf.remaining()); - r.reset().configure(N).read(buf); - assertEquals(r.get(), expected); - buf.clear(); - } - } - System.out.printf("totalCases: %,d, maxFilling: %,d, maxValue: %,d%n", - totalCases, maxFilling, MAX_VALUE); - } - - @Test - public void integerReadChunked() { - final int NUM_TESTS = 1024; - IntegerReader r = new IntegerReader(); - ByteBuffer bb = ByteBuffer.allocate(8); - IntegerWriter w = new IntegerWriter(); - for (int i = 0; i < NUM_TESTS; i++) { - final int N = 1 + rnd.nextInt(8); - final int expected = rnd.nextInt(Integer.MAX_VALUE) + 1; - w.reset().configure(expected, N, rnd.nextInt()).write(bb); - bb.flip(); - - forEachSplit(bb, - (buffers) -> { - Iterable buf = relocateBuffers(injectEmptyBuffers(buffers)); - r.configure(N); - for (ByteBuffer b : buf) { - try { - r.read(b); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - assertEquals(r.get(), expected); - r.reset(); - }); - bb.clear(); - } - } - - // FIXME: use maxValue in the test - - @Test - // FIXME: tune values for better coverage - public void integerWriteChunked() { - ByteBuffer bb = ByteBuffer.allocate(6); - IntegerWriter w = new IntegerWriter(); - IntegerReader r = new IntegerReader(); - for (int i = 0; i < 1024; i++) { // number of tests - final int N = 1 + rnd.nextInt(8); - final int payload = rnd.nextInt(255); - final int expected = rnd.nextInt(Integer.MAX_VALUE) + 1; - - forEachSplit(bb, - (buffers) -> { - List buf = new ArrayList<>(); - relocateBuffers(injectEmptyBuffers(buffers)).forEach(buf::add); - boolean written = false; - w.configure(expected, N, payload); // TODO: test for payload it can be read after written - for (ByteBuffer b : buf) { - int pos = b.position(); - written = w.write(b); - b.position(pos); - } - if (!written) { - fail("please increase bb size"); - } - try { - r.configure(N).read(concat(buf)); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - // TODO: check payload here - assertEquals(r.get(), expected); - w.reset(); - r.reset(); - bb.clear(); - }); - } - } - - - // - // Since readString(x) is the inverse of writeString(x), thus: - // - // for all x: readString(writeString(x)) == x - // - @Test - public void stringIdentity() throws IOException { - final int MAX_STRING_LENGTH = 4096; - ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6); // it takes 6 bytes to encode string length of Integer.MAX_VALUE - CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH); - StringReader reader = new StringReader(); - StringWriter writer = new StringWriter(); - for (int len = 0; len <= MAX_STRING_LENGTH; len++) { - for (int i = 0; i < 64; i++) { - // not so much "test in isolation", I know... we're testing .reset() as well - bytes.clear(); - chars.clear(); - - byte[] b = new byte[len]; - rnd.nextBytes(b); - - String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string - - boolean written = writer - .configure(CharBuffer.wrap(expected), 0, expected.length(), false) - .write(bytes); - - if (!written) { - fail("please increase 'bytes' size"); - } - bytes.flip(); - reader.read(bytes, chars); - chars.flip(); - assertEquals(chars.toString(), expected); - reader.reset(); - writer.reset(); - } - } - } - -// @Test -// public void huffmanStringWriteChunked() { -// fail(); -// } -// -// @Test -// public void huffmanStringReadChunked() { -// fail(); -// } - - @Test - public void stringWriteChunked() { - final int MAX_STRING_LENGTH = 8; - final ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6); - final CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH); - final StringReader reader = new StringReader(); - final StringWriter writer = new StringWriter(); - for (int len = 0; len <= MAX_STRING_LENGTH; len++) { - - byte[] b = new byte[len]; - rnd.nextBytes(b); - - String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string - - forEachSplit(bytes, (buffers) -> { - writer.configure(expected, 0, expected.length(), false); - boolean written = false; - for (ByteBuffer buf : buffers) { - int p0 = buf.position(); - written = writer.write(buf); - buf.position(p0); - } - if (!written) { - fail("please increase 'bytes' size"); - } - try { - reader.read(concat(buffers), chars); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - chars.flip(); - assertEquals(chars.toString(), expected); - reader.reset(); - writer.reset(); - chars.clear(); - bytes.clear(); - }); - } - } - - @Test - public void stringReadChunked() { - final int MAX_STRING_LENGTH = 16; - final ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6); - final CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH); - final StringReader reader = new StringReader(); - final StringWriter writer = new StringWriter(); - for (int len = 0; len <= MAX_STRING_LENGTH; len++) { - - byte[] b = new byte[len]; - rnd.nextBytes(b); - - String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string - - boolean written = writer - .configure(CharBuffer.wrap(expected), 0, expected.length(), false) - .write(bytes); - writer.reset(); - - if (!written) { - fail("please increase 'bytes' size"); - } - bytes.flip(); - - forEachSplit(bytes, (buffers) -> { - for (ByteBuffer buf : buffers) { - int p0 = buf.position(); - try { - reader.read(buf, chars); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - buf.position(p0); - } - chars.flip(); - assertEquals(chars.toString(), expected); - reader.reset(); - chars.clear(); - }); - - bytes.clear(); - } - } - -// @Test -// public void test_Huffman_String_Identity() { -// StringWriter writer = new StringWriter(); -// StringReader reader = new StringReader(); -// // 256 * 8 gives 2048 bits in case of plain 8 bit coding -// // 256 * 30 gives you 7680 bits or 960 bytes in case of almost -// // improbable event of 256 30 bits symbols in a row -// ByteBuffer binary = ByteBuffer.allocate(960); -// CharBuffer text = CharBuffer.allocate(960 / 5); // 5 = minimum code length -// for (int len = 0; len < 128; len++) { -// for (int i = 0; i < 256; i++) { -// // not so much "test in isolation", I know... -// binary.clear(); -// -// byte[] bytes = new byte[len]; -// rnd.nextBytes(bytes); -// -// String s = new String(bytes, StandardCharsets.ISO_8859_1); -// -// writer.write(CharBuffer.wrap(s), binary, true); -// binary.flip(); -// reader.read(binary, text); -// text.flip(); -// assertEquals(text.toString(), s); -// } -// } -// } - - // TODO: atomic failures: e.g. readonly/overflow - - private static byte[] bytes(int... data) { - byte[] bytes = new byte[data.length]; - for (int i = 0; i < data.length; i++) { - bytes[i] = (byte) data[i]; - } - return bytes; - } - - private static void verifyRead(byte[] data, int expected, int N) { - ByteBuffer buf = ByteBuffer.wrap(data, 0, data.length); - IntegerReader reader = new IntegerReader(); - try { - reader.configure(N).read(buf); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - assertEquals(expected, reader.get()); - } - - private void verifyWrite(byte[] expected, int data, int N) { - IntegerWriter w = new IntegerWriter(); - ByteBuffer buf = ByteBuffer.allocate(2 * expected.length); - w.configure(data, N, 1).write(buf); - buf.flip(); - assertEquals(ByteBuffer.wrap(expected), buf); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/BuffersTestingKit.java --- a/test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/BuffersTestingKit.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,210 +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 java.net.http.internal.hpack; - -import java.nio.ByteBuffer; -import java.util.*; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import static java.nio.ByteBuffer.allocate; - -public final class BuffersTestingKit { - - /** - * Relocates a {@code [position, limit)} region of the given buffer to - * corresponding region in a new buffer starting with provided {@code - * newPosition}. - * - *

Might be useful to make sure ByteBuffer's users do not rely on any - * absolute positions, but solely on what's reported by position(), limit(). - * - *

The contents between the given buffer and the returned one are not - * shared. - */ - public static ByteBuffer relocate(ByteBuffer buffer, int newPosition, - int newCapacity) { - int oldPosition = buffer.position(); - int oldLimit = buffer.limit(); - - if (newPosition + oldLimit - oldPosition > newCapacity) { - throw new IllegalArgumentException(); - } - - ByteBuffer result; - if (buffer.isDirect()) { - result = ByteBuffer.allocateDirect(newCapacity); - } else { - result = allocate(newCapacity); - } - - result.position(newPosition); - result.put(buffer).limit(result.position()).position(newPosition); - buffer.position(oldPosition); - - if (buffer.isReadOnly()) { - return result.asReadOnlyBuffer(); - } - return result; - } - - public static Iterable relocateBuffers( - Iterable source) { - return () -> - new Iterator() { - - private final Iterator it = source.iterator(); - - @Override - public boolean hasNext() { - return it.hasNext(); - } - - @Override - public ByteBuffer next() { - ByteBuffer buf = it.next(); - int remaining = buf.remaining(); - int newCapacity = remaining + random.nextInt(17); - int newPosition = random.nextInt(newCapacity - remaining + 1); - return relocate(buf, newPosition, newCapacity); - } - }; - } - - // TODO: not always of size 0 (it's fine for buffer to report !b.hasRemaining()) - public static Iterable injectEmptyBuffers( - Iterable source) { - return injectEmptyBuffers(source, () -> allocate(0)); - } - - public static Iterable injectEmptyBuffers( - Iterable source, - Supplier emptyBufferFactory) { - - return () -> - new Iterator() { - - private final Iterator it = source.iterator(); - private ByteBuffer next = calculateNext(); - - private ByteBuffer calculateNext() { - if (random.nextBoolean()) { - return emptyBufferFactory.get(); - } else if (it.hasNext()) { - return it.next(); - } else { - return null; - } - } - - @Override - public boolean hasNext() { - return next != null; - } - - @Override - public ByteBuffer next() { - if (!hasNext()) { - throw new NoSuchElementException(); - } - ByteBuffer next = this.next; - this.next = calculateNext(); - return next; - } - }; - } - - public static ByteBuffer concat(Iterable split) { - return concat(split, ByteBuffer::allocate); - } - - public static ByteBuffer concat(Iterable split, - Function concatBufferFactory) { - int size = 0; - for (ByteBuffer bb : split) { - size += bb.remaining(); - } - - ByteBuffer result = concatBufferFactory.apply(size); - for (ByteBuffer bb : split) { - result.put(bb); - } - - result.flip(); - return result; - } - - public static void forEachSplit(ByteBuffer bb, - Consumer> action) { - forEachSplit(bb.remaining(), - (lengths) -> { - int end = bb.position(); - List buffers = new LinkedList<>(); - for (int len : lengths) { - ByteBuffer d = bb.duplicate(); - d.position(end); - d.limit(end + len); - end += len; - buffers.add(d); - } - action.accept(buffers); - }); - } - - private static void forEachSplit(int n, Consumer> action) { - forEachSplit(n, new Stack<>(), action); - } - - private static void forEachSplit(int n, Stack path, - Consumer> action) { - if (n == 0) { - action.accept(path); - } else { - for (int i = 1; i <= n; i++) { - path.push(i); - forEachSplit(n - i, path, action); - path.pop(); - } - } - } - - private static final Random random = new Random(); - - private BuffersTestingKit() { - throw new InternalError(); - } - -// public static void main(String[] args) { -// -// List buffers = Arrays.asList( -// (ByteBuffer) allocate(3).position(1).limit(2), -// allocate(0), -// allocate(7)); -// -// Iterable buf = relocateBuffers(injectEmptyBuffers(buffers)); -// List result = new ArrayList<>(); -// buf.forEach(result::add); -// System.out.println(result); -// } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/CircularBufferTest.java --- a/test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/CircularBufferTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,125 +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. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package java.net.http.internal.hpack; - -import org.testng.annotations.BeforeClass; -import org.testng.annotations.Test; -import java.net.http.internal.hpack.HeaderTable.CircularBuffer; - -import java.util.Queue; -import java.util.Random; -import java.util.concurrent.ArrayBlockingQueue; - -import static org.testng.Assert.assertEquals; -import static java.net.http.internal.hpack.TestHelper.newRandom; - -public final class CircularBufferTest { - - private final Random r = newRandom(); - - @BeforeClass - public void setUp() { - r.setSeed(System.currentTimeMillis()); - } - - @Test - public void queue() { - for (int capacity = 1; capacity <= 2048; capacity++) { - queueOnce(capacity, 32); - } - } - - @Test - public void resize() { - for (int capacity = 1; capacity <= 4096; capacity++) { - resizeOnce(capacity); - } - } - - @Test - public void downSizeEmptyBuffer() { - CircularBuffer buffer = new CircularBuffer<>(16); - buffer.resize(15); - } - - private void resizeOnce(int capacity) { - - int nextNumberToPut = 0; - - Queue referenceQueue = new ArrayBlockingQueue<>(capacity); - CircularBuffer buffer = new CircularBuffer<>(capacity); - - // Fill full, so the next add will wrap - for (int i = 0; i < capacity; i++, nextNumberToPut++) { - buffer.add(nextNumberToPut); - referenceQueue.add(nextNumberToPut); - } - int gets = r.nextInt(capacity); // [0, capacity) - for (int i = 0; i < gets; i++) { - referenceQueue.poll(); - buffer.remove(); - } - int puts = r.nextInt(gets + 1); // [0, gets] - for (int i = 0; i < puts; i++, nextNumberToPut++) { - buffer.add(nextNumberToPut); - referenceQueue.add(nextNumberToPut); - } - - Integer[] expected = referenceQueue.toArray(new Integer[0]); - buffer.resize(expected.length); - - assertEquals(buffer.elements, expected); - } - - private void queueOnce(int capacity, int numWraps) { - - Queue referenceQueue = new ArrayBlockingQueue<>(capacity); - CircularBuffer buffer = new CircularBuffer<>(capacity); - - int nextNumberToPut = 0; - int totalPuts = 0; - int putsLimit = capacity * numWraps; - int remainingCapacity = capacity; - int size = 0; - - while (totalPuts < putsLimit) { - assert remainingCapacity + size == capacity; - int puts = r.nextInt(remainingCapacity + 1); // [0, remainingCapacity] - remainingCapacity -= puts; - size += puts; - for (int i = 0; i < puts; i++, nextNumberToPut++) { - referenceQueue.add(nextNumberToPut); - buffer.add(nextNumberToPut); - } - totalPuts += puts; - int gets = r.nextInt(size + 1); // [0, size] - size -= gets; - remainingCapacity += gets; - for (int i = 0; i < gets; i++) { - Integer expected = referenceQueue.poll(); - Integer actual = buffer.remove(); - assertEquals(actual, expected); - } - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/DecoderTest.java --- a/test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/DecoderTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,685 +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 java.net.http.internal.hpack; - -import org.testng.annotations.Test; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.ByteBuffer; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotNull; -import static java.net.http.internal.hpack.TestHelper.*; - -// -// Tests whose names start with "testX" are the ones captured from real HPACK -// use cases -// -public final class DecoderTest { - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.2.1 - // - @Test - public void example1() { - // @formatter:off - test("400a 6375 7374 6f6d 2d6b 6579 0d63 7573\n" + - "746f 6d2d 6865 6164 6572", - - "[ 1] (s = 55) custom-key: custom-header\n" + - " Table size: 55", - - "custom-key: custom-header"); - // @formatter:on - } - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.2.2 - // - @Test - public void example2() { - // @formatter:off - test("040c 2f73 616d 706c 652f 7061 7468", - "empty.", - ":path: /sample/path"); - // @formatter:on - } - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.2.3 - // - @Test - public void example3() { - // @formatter:off - test("1008 7061 7373 776f 7264 0673 6563 7265\n" + - "74", - "empty.", - "password: secret"); - // @formatter:on - } - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.2.4 - // - @Test - public void example4() { - // @formatter:off - test("82", - "empty.", - ":method: GET"); - // @formatter:on - } - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.3 - // - @Test - public void example5() { - // @formatter:off - Decoder d = new Decoder(256); - - test(d, "8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" + - "2e63 6f6d", - - "[ 1] (s = 57) :authority: www.example.com\n" + - " Table size: 57", - - ":method: GET\n" + - ":scheme: http\n" + - ":path: /\n" + - ":authority: www.example.com"); - - test(d, "8286 84be 5808 6e6f 2d63 6163 6865", - - "[ 1] (s = 53) cache-control: no-cache\n" + - "[ 2] (s = 57) :authority: www.example.com\n" + - " Table size: 110", - - ":method: GET\n" + - ":scheme: http\n" + - ":path: /\n" + - ":authority: www.example.com\n" + - "cache-control: no-cache"); - - test(d, "8287 85bf 400a 6375 7374 6f6d 2d6b 6579\n" + - "0c63 7573 746f 6d2d 7661 6c75 65", - - "[ 1] (s = 54) custom-key: custom-value\n" + - "[ 2] (s = 53) cache-control: no-cache\n" + - "[ 3] (s = 57) :authority: www.example.com\n" + - " Table size: 164", - - ":method: GET\n" + - ":scheme: https\n" + - ":path: /index.html\n" + - ":authority: www.example.com\n" + - "custom-key: custom-value"); - - // @formatter:on - } - - @Test - public void example5AllSplits() { - // @formatter:off - testAllSplits( - "8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" + - "2e63 6f6d", - - "[ 1] (s = 57) :authority: www.example.com\n" + - " Table size: 57", - - ":method: GET\n" + - ":scheme: http\n" + - ":path: /\n" + - ":authority: www.example.com"); - // @formatter:on - } - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.4 - // - @Test - public void example6() { - // @formatter:off - Decoder d = new Decoder(256); - - test(d, "8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4\n" + - "ff", - - "[ 1] (s = 57) :authority: www.example.com\n" + - " Table size: 57", - - ":method: GET\n" + - ":scheme: http\n" + - ":path: /\n" + - ":authority: www.example.com"); - - test(d, "8286 84be 5886 a8eb 1064 9cbf", - - "[ 1] (s = 53) cache-control: no-cache\n" + - "[ 2] (s = 57) :authority: www.example.com\n" + - " Table size: 110", - - ":method: GET\n" + - ":scheme: http\n" + - ":path: /\n" + - ":authority: www.example.com\n" + - "cache-control: no-cache"); - - test(d, "8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925\n" + - "a849 e95b b8e8 b4bf", - - "[ 1] (s = 54) custom-key: custom-value\n" + - "[ 2] (s = 53) cache-control: no-cache\n" + - "[ 3] (s = 57) :authority: www.example.com\n" + - " Table size: 164", - - ":method: GET\n" + - ":scheme: https\n" + - ":path: /index.html\n" + - ":authority: www.example.com\n" + - "custom-key: custom-value"); - // @formatter:on - } - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.5 - // - @Test - public void example7() { - // @formatter:off - Decoder d = new Decoder(256); - - test(d, "4803 3330 3258 0770 7269 7661 7465 611d\n" + - "4d6f 6e2c 2032 3120 4f63 7420 3230 3133\n" + - "2032 303a 3133 3a32 3120 474d 546e 1768\n" + - "7474 7073 3a2f 2f77 7777 2e65 7861 6d70\n" + - "6c65 2e63 6f6d", - - "[ 1] (s = 63) location: https://www.example.com\n" + - "[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + - "[ 3] (s = 52) cache-control: private\n" + - "[ 4] (s = 42) :status: 302\n" + - " Table size: 222", - - ":status: 302\n" + - "cache-control: private\n" + - "date: Mon, 21 Oct 2013 20:13:21 GMT\n" + - "location: https://www.example.com"); - - test(d, "4803 3330 37c1 c0bf", - - "[ 1] (s = 42) :status: 307\n" + - "[ 2] (s = 63) location: https://www.example.com\n" + - "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + - "[ 4] (s = 52) cache-control: private\n" + - " Table size: 222", - - ":status: 307\n" + - "cache-control: private\n" + - "date: Mon, 21 Oct 2013 20:13:21 GMT\n" + - "location: https://www.example.com"); - - test(d, "88c1 611d 4d6f 6e2c 2032 3120 4f63 7420\n" + - "3230 3133 2032 303a 3133 3a32 3220 474d\n" + - "54c0 5a04 677a 6970 7738 666f 6f3d 4153\n" + - "444a 4b48 514b 425a 584f 5157 454f 5049\n" + - "5541 5851 5745 4f49 553b 206d 6178 2d61\n" + - "6765 3d33 3630 303b 2076 6572 7369 6f6e\n" + - "3d31", - - "[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" + - "[ 2] (s = 52) content-encoding: gzip\n" + - "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" + - " Table size: 215", - - ":status: 200\n" + - "cache-control: private\n" + - "date: Mon, 21 Oct 2013 20:13:22 GMT\n" + - "location: https://www.example.com\n" + - "content-encoding: gzip\n" + - "set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"); - // @formatter:on - } - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.6 - // - @Test - public void example8() { - // @formatter:off - Decoder d = new Decoder(256); - - test(d, "4882 6402 5885 aec3 771a 4b61 96d0 7abe\n" + - "9410 54d4 44a8 2005 9504 0b81 66e0 82a6\n" + - "2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8\n" + - "e9ae 82ae 43d3", - - "[ 1] (s = 63) location: https://www.example.com\n" + - "[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + - "[ 3] (s = 52) cache-control: private\n" + - "[ 4] (s = 42) :status: 302\n" + - " Table size: 222", - - ":status: 302\n" + - "cache-control: private\n" + - "date: Mon, 21 Oct 2013 20:13:21 GMT\n" + - "location: https://www.example.com"); - - test(d, "4883 640e ffc1 c0bf", - - "[ 1] (s = 42) :status: 307\n" + - "[ 2] (s = 63) location: https://www.example.com\n" + - "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + - "[ 4] (s = 52) cache-control: private\n" + - " Table size: 222", - - ":status: 307\n" + - "cache-control: private\n" + - "date: Mon, 21 Oct 2013 20:13:21 GMT\n" + - "location: https://www.example.com"); - - test(d, "88c1 6196 d07a be94 1054 d444 a820 0595\n" + - "040b 8166 e084 a62d 1bff c05a 839b d9ab\n" + - "77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b\n" + - "3960 d5af 2708 7f36 72c1 ab27 0fb5 291f\n" + - "9587 3160 65c0 03ed 4ee5 b106 3d50 07", - - "[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" + - "[ 2] (s = 52) content-encoding: gzip\n" + - "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" + - " Table size: 215", - - ":status: 200\n" + - "cache-control: private\n" + - "date: Mon, 21 Oct 2013 20:13:22 GMT\n" + - "location: https://www.example.com\n" + - "content-encoding: gzip\n" + - "set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"); - // @formatter:on - } - - @Test - // One of responses from Apache Server that helped to catch a bug - public void testX() { - Decoder d = new Decoder(4096); - // @formatter:off - test(d, "3fe1 1f88 6196 d07a be94 03ea 693f 7504\n" + - "00b6 a05c b827 2e32 fa98 b46f 769e 86b1\n" + - "9272 b025 da5c 2ea9 fd70 a8de 7fb5 3556\n" + - "5ab7 6ece c057 02e2 2ad2 17bf 6c96 d07a\n" + - "be94 0854 cb6d 4a08 0075 40bd 71b6 6e05\n" + - "a531 68df 0f13 8efe 4522 cd32 21b6 5686\n" + - "eb23 781f cf52 848f d24a 8f0f 0d02 3435\n" + - "5f87 497c a589 d34d 1f", - - "[ 1] (s = 53) content-type: text/html\n" + - "[ 2] (s = 50) accept-ranges: bytes\n" + - "[ 3] (s = 74) last-modified: Mon, 11 Jun 2007 18:53:14 GMT\n" + - "[ 4] (s = 77) server: Apache/2.4.17 (Unix) OpenSSL/1.0.2e-dev\n" + - "[ 5] (s = 65) date: Mon, 09 Nov 2015 16:26:39 GMT\n" + - " Table size: 319", - - ":status: 200\n" + - "date: Mon, 09 Nov 2015 16:26:39 GMT\n" + - "server: Apache/2.4.17 (Unix) OpenSSL/1.0.2e-dev\n" + - "last-modified: Mon, 11 Jun 2007 18:53:14 GMT\n" + - "etag: \"2d-432a5e4a73a80\"\n" + - "accept-ranges: bytes\n" + - "content-length: 45\n" + - "content-type: text/html"); - // @formatter:on - } - - @Test - public void testX1() { - // Supplier of a decoder with a particular state - Supplier s = () -> { - Decoder d = new Decoder(4096); - // @formatter:off - test(d, "88 76 92 ca 54 a7 d7 f4 fa ec af ed 6d da 61 d7 bb 1e ad ff" + - "df 61 97 c3 61 be 94 13 4a 65 b6 a5 04 00 b8 a0 5a b8 db 77" + - "1b 71 4c 5a 37 ff 0f 0d 84 08 00 00 03", - - "[ 1] (s = 65) date: Fri, 24 Jun 2016 14:55:56 GMT\n" + - "[ 2] (s = 59) server: Jetty(9.3.z-SNAPSHOT)\n" + - " Table size: 124", - - ":status: 200\n" + - "server: Jetty(9.3.z-SNAPSHOT)\n" + - "date: Fri, 24 Jun 2016 14:55:56 GMT\n" + - "content-length: 100000" - ); - // @formatter:on - return d; - }; - // For all splits of the following data fed to the supplied decoder we - // must get what's expected - // @formatter:off - testAllSplits(s, - "88 bf be 0f 0d 84 08 00 00 03", - - "[ 1] (s = 65) date: Fri, 24 Jun 2016 14:55:56 GMT\n" + - "[ 2] (s = 59) server: Jetty(9.3.z-SNAPSHOT)\n" + - " Table size: 124", - - ":status: 200\n" + - "server: Jetty(9.3.z-SNAPSHOT)\n" + - "date: Fri, 24 Jun 2016 14:55:56 GMT\n" + - "content-length: 100000"); - // @formatter:on - } - - // - // This test is missing in the spec - // - @Test - public void sizeUpdate() throws IOException { - Decoder d = new Decoder(4096); - assertEquals(d.getTable().maxSize(), 4096); - d.decode(ByteBuffer.wrap(new byte[]{0b00111110}), true, nopCallback()); // newSize = 30 - assertEquals(d.getTable().maxSize(), 30); - } - - @Test - public void incorrectSizeUpdate() { - ByteBuffer b = ByteBuffer.allocate(8); - Encoder e = new Encoder(8192) { - @Override - protected int calculateCapacity(int maxCapacity) { - return maxCapacity; - } - }; - e.header("a", "b"); - e.encode(b); - b.flip(); - { - Decoder d = new Decoder(4096); - assertVoidThrows(IOException.class, - () -> d.decode(b, true, (name, value) -> { })); - } - b.flip(); - { - Decoder d = new Decoder(4096); - assertVoidThrows(IOException.class, - () -> d.decode(b, false, (name, value) -> { })); - } - } - - @Test - public void corruptedHeaderBlockInteger() { - Decoder d = new Decoder(4096); - ByteBuffer data = ByteBuffer.wrap(new byte[]{ - (byte) 0b11111111, // indexed - (byte) 0b10011010 // 25 + ... - }); - IOException e = assertVoidThrows(IOException.class, - () -> d.decode(data, true, nopCallback())); - assertExceptionMessageContains(e, "Unexpected end of header block"); - } - - // 5.1. Integer Representation - // ... - // Integer encodings that exceed implementation limits -- in value or octet - // length -- MUST be treated as decoding errors. Different limits can - // be set for each of the different uses of integers, based on - // implementation constraints. - @Test - public void headerBlockIntegerNoOverflow() { - Decoder d = new Decoder(4096); - ByteBuffer data = ByteBuffer.wrap(new byte[]{ - (byte) 0b11111111, // indexed + 127 - // Integer.MAX_VALUE - 127 (base 128, little-endian): - (byte) 0b10000000, - (byte) 0b11111111, - (byte) 0b11111111, - (byte) 0b11111111, - (byte) 0b00000111 - }); - - IOException e = assertVoidThrows(IOException.class, - () -> d.decode(data, true, nopCallback())); - - assertExceptionMessageContains(e.getCause(), "index=2147483647"); - } - - @Test - public void headerBlockIntegerOverflow() { - Decoder d = new Decoder(4096); - ByteBuffer data = ByteBuffer.wrap(new byte[]{ - (byte) 0b11111111, // indexed + 127 - // Integer.MAX_VALUE - 127 + 1 (base 128, little endian): - (byte) 0b10000001, - (byte) 0b11111111, - (byte) 0b11111111, - (byte) 0b11111111, - (byte) 0b00000111 - }); - - IOException e = assertVoidThrows(IOException.class, - () -> d.decode(data, true, nopCallback())); - - assertExceptionMessageContains(e, "Integer overflow"); - } - - @Test - public void corruptedHeaderBlockString1() { - Decoder d = new Decoder(4096); - ByteBuffer data = ByteBuffer.wrap(new byte[]{ - 0b00001111, // literal, index=15 - 0b00000000, - 0b00001000, // huffman=false, length=8 - 0b00000000, // \ - 0b00000000, // but only 3 octets available... - 0b00000000 // / - }); - IOException e = assertVoidThrows(IOException.class, - () -> d.decode(data, true, nopCallback())); - assertExceptionMessageContains(e, "Unexpected end of header block"); - } - - @Test - public void corruptedHeaderBlockString2() { - Decoder d = new Decoder(4096); - ByteBuffer data = ByteBuffer.wrap(new byte[]{ - 0b00001111, // literal, index=15 - 0b00000000, - (byte) 0b10001000, // huffman=true, length=8 - 0b00000000, // \ - 0b00000000, // \ - 0b00000000, // but only 5 octets available... - 0b00000000, // / - 0b00000000 // / - }); - IOException e = assertVoidThrows(IOException.class, - () -> d.decode(data, true, nopCallback())); - assertExceptionMessageContains(e, "Unexpected end of header block"); - } - - // 5.2. String Literal Representation - // ...A Huffman-encoded string literal containing the EOS symbol MUST be - // treated as a decoding error... - @Test - public void corruptedHeaderBlockHuffmanStringEOS() { - Decoder d = new Decoder(4096); - ByteBuffer data = ByteBuffer.wrap(new byte[]{ - 0b00001111, // literal, index=15 - 0b00000000, - (byte) 0b10000110, // huffman=true, length=6 - 0b00011001, 0b01001101, (byte) 0b11111111, - (byte) 0b11111111, (byte) 0b11111111, (byte) 0b11111100 - }); - IOException e = assertVoidThrows(IOException.class, - () -> d.decode(data, true, nopCallback())); - - assertExceptionMessageContains(e, "Encountered EOS"); - } - - // 5.2. String Literal Representation - // ...A padding strictly longer than 7 bits MUST be treated as a decoding - // error... - @Test - public void corruptedHeaderBlockHuffmanStringLongPadding1() { - Decoder d = new Decoder(4096); - ByteBuffer data = ByteBuffer.wrap(new byte[]{ - 0b00001111, // literal, index=15 - 0b00000000, - (byte) 0b10000011, // huffman=true, length=3 - 0b00011001, 0b01001101, (byte) 0b11111111 - // len("aei") + len(padding) = (5 + 5 + 5) + (9) - }); - IOException e = assertVoidThrows(IOException.class, - () -> d.decode(data, true, nopCallback())); - - assertExceptionMessageContains(e, "Padding is too long", "len=9"); - } - - @Test - public void corruptedHeaderBlockHuffmanStringLongPadding2() { - Decoder d = new Decoder(4096); - ByteBuffer data = ByteBuffer.wrap(new byte[]{ - 0b00001111, // literal, index=15 - 0b00000000, - (byte) 0b10000011, // huffman=true, length=3 - 0b00011001, 0b01111010, (byte) 0b11111111 - // len("aek") + len(padding) = (5 + 5 + 7) + (7) - }); - assertVoidDoesNotThrow(() -> d.decode(data, true, nopCallback())); - } - - // 5.2. String Literal Representation - // ...A padding not corresponding to the most significant bits of the code - // for the EOS symbol MUST be treated as a decoding error... - @Test - public void corruptedHeaderBlockHuffmanStringNotEOSPadding() { - Decoder d = new Decoder(4096); - ByteBuffer data = ByteBuffer.wrap(new byte[]{ - 0b00001111, // literal, index=15 - 0b00000000, - (byte) 0b10000011, // huffman=true, length=3 - 0b00011001, 0b01111010, (byte) 0b11111110 - }); - IOException e = assertVoidThrows(IOException.class, - () -> d.decode(data, true, nopCallback())); - - assertExceptionMessageContains(e, "Not a EOS prefix"); - } - - @Test - public void argsTestBiConsumerIsNull() { - Decoder decoder = new Decoder(4096); - assertVoidThrows(NullPointerException.class, - () -> decoder.decode(ByteBuffer.allocate(16), true, null)); - } - - @Test - public void argsTestByteBufferIsNull() { - Decoder decoder = new Decoder(4096); - assertVoidThrows(NullPointerException.class, - () -> decoder.decode(null, true, nopCallback())); - } - - @Test - public void argsTestBothAreNull() { - Decoder decoder = new Decoder(4096); - assertVoidThrows(NullPointerException.class, - () -> decoder.decode(null, true, null)); - } - - private static void test(String hexdump, - String headerTable, String headerList) { - test(new Decoder(4096), hexdump, headerTable, headerList); - } - - private static void testAllSplits(String hexdump, - String expectedHeaderTable, - String expectedHeaderList) { - testAllSplits(() -> new Decoder(256), hexdump, expectedHeaderTable, expectedHeaderList); - } - - private static void testAllSplits(Supplier supplier, String hexdump, - String expectedHeaderTable, String expectedHeaderList) { - ByteBuffer source = SpecHelper.toBytes(hexdump); - - BuffersTestingKit.forEachSplit(source, iterable -> { - List actual = new LinkedList<>(); - Iterator i = iterable.iterator(); - if (!i.hasNext()) { - return; - } - Decoder d = supplier.get(); - do { - ByteBuffer n = i.next(); - try { - d.decode(n, !i.hasNext(), (name, value) -> { - if (value == null) { - actual.add(name.toString()); - } else { - actual.add(name + ": " + value); - } - }); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } while (i.hasNext()); - assertEquals(d.getTable().getStateString(), expectedHeaderTable); - assertEquals(actual.stream().collect(Collectors.joining("\n")), expectedHeaderList); - }); - } - - // - // Sometimes we need to keep the same decoder along several runs, - // as it models the same connection - // - private static void test(Decoder d, String hexdump, - String expectedHeaderTable, String expectedHeaderList) { - - ByteBuffer source = SpecHelper.toBytes(hexdump); - - List actual = new LinkedList<>(); - try { - d.decode(source, true, (name, value) -> { - if (value == null) { - actual.add(name.toString()); - } else { - actual.add(name + ": " + value); - } - }); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - - assertEquals(d.getTable().getStateString(), expectedHeaderTable); - assertEquals(actual.stream().collect(Collectors.joining("\n")), expectedHeaderList); - } - - private static DecodingCallback nopCallback() { - return (t, u) -> { }; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/EncoderTest.java --- a/test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/EncoderTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,693 +0,0 @@ -/* - * Copyright (c) 2014, 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 java.net.http.internal.hpack; - -import org.testng.annotations.Test; - -import java.io.IOException; -import java.nio.Buffer; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; - -import static java.net.http.internal.hpack.BuffersTestingKit.concat; -import static java.net.http.internal.hpack.BuffersTestingKit.forEachSplit; -import static java.net.http.internal.hpack.SpecHelper.toHexdump; -import static java.net.http.internal.hpack.TestHelper.assertVoidThrows; -import static java.util.Arrays.asList; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; - -// TODO: map textual representation of commands from the spec to actual -// calls to encoder (actually, this is a good idea for decoder as well) -public final class EncoderTest { - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.2.1 - // - @Test - public void example1() { - - Encoder e = newCustomEncoder(256); - drainInitialUpdate(e); - - e.literalWithIndexing("custom-key", false, "custom-header", false); - // @formatter:off - test(e, - - "400a 6375 7374 6f6d 2d6b 6579 0d63 7573\n" + - "746f 6d2d 6865 6164 6572", - - "[ 1] (s = 55) custom-key: custom-header\n" + - " Table size: 55"); - // @formatter:on - } - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.2.2 - // - @Test - public void example2() { - - Encoder e = newCustomEncoder(256); - drainInitialUpdate(e); - - e.literal(4, "/sample/path", false); - // @formatter:off - test(e, - - "040c 2f73 616d 706c 652f 7061 7468", - - "empty."); - // @formatter:on - } - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.2.3 - // - @Test - public void example3() { - - Encoder e = newCustomEncoder(256); - drainInitialUpdate(e); - - e.literalNeverIndexed("password", false, "secret", false); - // @formatter:off - test(e, - - "1008 7061 7373 776f 7264 0673 6563 7265\n" + - "74", - - "empty."); - // @formatter:on - } - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.2.4 - // - @Test - public void example4() { - - Encoder e = newCustomEncoder(256); - drainInitialUpdate(e); - - e.indexed(2); - // @formatter:off - test(e, - - "82", - - "empty."); - // @formatter:on - } - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.3 - // - @Test - public void example5() { - Encoder e = newCustomEncoder(256); - drainInitialUpdate(e); - - ByteBuffer output = ByteBuffer.allocate(64); - e.indexed(2); - e.encode(output); - e.indexed(6); - e.encode(output); - e.indexed(4); - e.encode(output); - e.literalWithIndexing(1, "www.example.com", false); - e.encode(output); - - output.flip(); - - // @formatter:off - test(e, output, - - "8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" + - "2e63 6f6d", - - "[ 1] (s = 57) :authority: www.example.com\n" + - " Table size: 57"); - - output.clear(); - - e.indexed( 2); - e.encode(output); - e.indexed( 6); - e.encode(output); - e.indexed( 4); - e.encode(output); - e.indexed(62); - e.encode(output); - e.literalWithIndexing(24, "no-cache", false); - e.encode(output); - - output.flip(); - - test(e, output, - - "8286 84be 5808 6e6f 2d63 6163 6865", - - "[ 1] (s = 53) cache-control: no-cache\n" + - "[ 2] (s = 57) :authority: www.example.com\n" + - " Table size: 110"); - - output.clear(); - - e.indexed( 2); - e.encode(output); - e.indexed( 7); - e.encode(output); - e.indexed( 5); - e.encode(output); - e.indexed(63); - e.encode(output); - e.literalWithIndexing("custom-key", false, "custom-value", false); - e.encode(output); - - output.flip(); - - test(e, output, - - "8287 85bf 400a 6375 7374 6f6d 2d6b 6579\n" + - "0c63 7573 746f 6d2d 7661 6c75 65", - - "[ 1] (s = 54) custom-key: custom-value\n" + - "[ 2] (s = 53) cache-control: no-cache\n" + - "[ 3] (s = 57) :authority: www.example.com\n" + - " Table size: 164"); - // @formatter:on - } - - @Test - public void example5AllSplits() { - - List> actions = new LinkedList<>(); - actions.add(e -> e.indexed(2)); - actions.add(e -> e.indexed(6)); - actions.add(e -> e.indexed(4)); - actions.add(e -> e.literalWithIndexing(1, "www.example.com", false)); - - encodeAllSplits( - actions, - - "8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" + - "2e63 6f6d", - - "[ 1] (s = 57) :authority: www.example.com\n" + - " Table size: 57"); - } - - private static void encodeAllSplits(Iterable> consumers, - String expectedHexdump, - String expectedTableState) { - ByteBuffer buffer = SpecHelper.toBytes(expectedHexdump); - erase(buffer); // Zeroed buffer of size needed to hold the encoding - forEachSplit(buffer, iterable -> { - List copy = new LinkedList<>(); - iterable.forEach(b -> copy.add(ByteBuffer.allocate(b.remaining()))); - Iterator output = copy.iterator(); - if (!output.hasNext()) { - throw new IllegalStateException("No buffers to encode to"); - } - Encoder e = newCustomEncoder(256); // FIXME: pull up (as a parameter) - drainInitialUpdate(e); - boolean encoded; - ByteBuffer b = output.next(); - for (Consumer c : consumers) { - c.accept(e); - do { - encoded = e.encode(b); - if (!encoded) { - if (output.hasNext()) { - b = output.next(); - } else { - throw new IllegalStateException("No room for encoding"); - } - } - } - while (!encoded); - } - copy.forEach(Buffer::flip); - ByteBuffer data = concat(copy); - test(e, data, expectedHexdump, expectedTableState); - }); - } - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.4 - // - @Test - public void example6() { - Encoder e = newCustomEncoder(256); - drainInitialUpdate(e); - - ByteBuffer output = ByteBuffer.allocate(64); - e.indexed(2); - e.encode(output); - e.indexed(6); - e.encode(output); - e.indexed(4); - e.encode(output); - e.literalWithIndexing(1, "www.example.com", true); - e.encode(output); - - output.flip(); - - // @formatter:off - test(e, output, - - "8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4\n" + - "ff", - - "[ 1] (s = 57) :authority: www.example.com\n" + - " Table size: 57"); - - output.clear(); - - e.indexed( 2); - e.encode(output); - e.indexed( 6); - e.encode(output); - e.indexed( 4); - e.encode(output); - e.indexed(62); - e.encode(output); - e.literalWithIndexing(24, "no-cache", true); - e.encode(output); - - output.flip(); - - test(e, output, - - "8286 84be 5886 a8eb 1064 9cbf", - - "[ 1] (s = 53) cache-control: no-cache\n" + - "[ 2] (s = 57) :authority: www.example.com\n" + - " Table size: 110"); - - output.clear(); - - e.indexed( 2); - e.encode(output); - e.indexed( 7); - e.encode(output); - e.indexed( 5); - e.encode(output); - e.indexed(63); - e.encode(output); - e.literalWithIndexing("custom-key", true, "custom-value", true); - e.encode(output); - - output.flip(); - - test(e, output, - - "8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925\n" + - "a849 e95b b8e8 b4bf", - - "[ 1] (s = 54) custom-key: custom-value\n" + - "[ 2] (s = 53) cache-control: no-cache\n" + - "[ 3] (s = 57) :authority: www.example.com\n" + - " Table size: 164"); - // @formatter:on - } - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.5 - // - @Test - public void example7() { - Encoder e = newCustomEncoder(256); - drainInitialUpdate(e); - - ByteBuffer output = ByteBuffer.allocate(128); - // @formatter:off - e.literalWithIndexing( 8, "302", false); - e.encode(output); - e.literalWithIndexing(24, "private", false); - e.encode(output); - e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:21 GMT", false); - e.encode(output); - e.literalWithIndexing(46, "https://www.example.com", false); - e.encode(output); - - output.flip(); - - test(e, output, - - "4803 3330 3258 0770 7269 7661 7465 611d\n" + - "4d6f 6e2c 2032 3120 4f63 7420 3230 3133\n" + - "2032 303a 3133 3a32 3120 474d 546e 1768\n" + - "7474 7073 3a2f 2f77 7777 2e65 7861 6d70\n" + - "6c65 2e63 6f6d", - - "[ 1] (s = 63) location: https://www.example.com\n" + - "[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + - "[ 3] (s = 52) cache-control: private\n" + - "[ 4] (s = 42) :status: 302\n" + - " Table size: 222"); - - output.clear(); - - e.literalWithIndexing( 8, "307", false); - e.encode(output); - e.indexed(65); - e.encode(output); - e.indexed(64); - e.encode(output); - e.indexed(63); - e.encode(output); - - output.flip(); - - test(e, output, - - "4803 3330 37c1 c0bf", - - "[ 1] (s = 42) :status: 307\n" + - "[ 2] (s = 63) location: https://www.example.com\n" + - "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + - "[ 4] (s = 52) cache-control: private\n" + - " Table size: 222"); - - output.clear(); - - e.indexed( 8); - e.encode(output); - e.indexed(65); - e.encode(output); - e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:22 GMT", false); - e.encode(output); - e.indexed(64); - e.encode(output); - e.literalWithIndexing(26, "gzip", false); - e.encode(output); - e.literalWithIndexing(55, "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", false); - e.encode(output); - - output.flip(); - - test(e, output, - - "88c1 611d 4d6f 6e2c 2032 3120 4f63 7420\n" + - "3230 3133 2032 303a 3133 3a32 3220 474d\n" + - "54c0 5a04 677a 6970 7738 666f 6f3d 4153\n" + - "444a 4b48 514b 425a 584f 5157 454f 5049\n" + - "5541 5851 5745 4f49 553b 206d 6178 2d61\n" + - "6765 3d33 3630 303b 2076 6572 7369 6f6e\n" + - "3d31", - - "[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" + - "[ 2] (s = 52) content-encoding: gzip\n" + - "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" + - " Table size: 215"); - // @formatter:on - } - - // - // http://tools.ietf.org/html/rfc7541#appendix-C.6 - // - @Test - public void example8() { - Encoder e = newCustomEncoder(256); - drainInitialUpdate(e); - - ByteBuffer output = ByteBuffer.allocate(128); - // @formatter:off - e.literalWithIndexing( 8, "302", true); - e.encode(output); - e.literalWithIndexing(24, "private", true); - e.encode(output); - e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:21 GMT", true); - e.encode(output); - e.literalWithIndexing(46, "https://www.example.com", true); - e.encode(output); - - output.flip(); - - test(e, output, - - "4882 6402 5885 aec3 771a 4b61 96d0 7abe\n" + - "9410 54d4 44a8 2005 9504 0b81 66e0 82a6\n" + - "2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8\n" + - "e9ae 82ae 43d3", - - "[ 1] (s = 63) location: https://www.example.com\n" + - "[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + - "[ 3] (s = 52) cache-control: private\n" + - "[ 4] (s = 42) :status: 302\n" + - " Table size: 222"); - - output.clear(); - - e.literalWithIndexing( 8, "307", true); - e.encode(output); - e.indexed(65); - e.encode(output); - e.indexed(64); - e.encode(output); - e.indexed(63); - e.encode(output); - - output.flip(); - - test(e, output, - - "4883 640e ffc1 c0bf", - - "[ 1] (s = 42) :status: 307\n" + - "[ 2] (s = 63) location: https://www.example.com\n" + - "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + - "[ 4] (s = 52) cache-control: private\n" + - " Table size: 222"); - - output.clear(); - - e.indexed( 8); - e.encode(output); - e.indexed(65); - e.encode(output); - e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:22 GMT", true); - e.encode(output); - e.indexed(64); - e.encode(output); - e.literalWithIndexing(26, "gzip", true); - e.encode(output); - e.literalWithIndexing(55, "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", true); - e.encode(output); - - output.flip(); - - test(e, output, - - "88c1 6196 d07a be94 1054 d444 a820 0595\n" + - "040b 8166 e084 a62d 1bff c05a 839b d9ab\n" + - "77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b\n" + - "3960 d5af 2708 7f36 72c1 ab27 0fb5 291f\n" + - "9587 3160 65c0 03ed 4ee5 b106 3d50 07", - - "[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" + - "[ 2] (s = 52) content-encoding: gzip\n" + - "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" + - " Table size: 215"); - // @formatter:on - } - - @Test - public void initialSizeUpdateDefaultEncoder() throws IOException { - Function e = Encoder::new; - testSizeUpdate(e, 1024, asList(), asList(0)); - testSizeUpdate(e, 1024, asList(1024), asList(0)); - testSizeUpdate(e, 1024, asList(1024, 1024), asList(0)); - testSizeUpdate(e, 1024, asList(1024, 512), asList(0)); - testSizeUpdate(e, 1024, asList(512, 1024), asList(0)); - testSizeUpdate(e, 1024, asList(512, 2048), asList(0)); - } - - @Test - public void initialSizeUpdateCustomEncoder() throws IOException { - Function e = EncoderTest::newCustomEncoder; - testSizeUpdate(e, 1024, asList(), asList(1024)); - testSizeUpdate(e, 1024, asList(1024), asList(1024)); - testSizeUpdate(e, 1024, asList(1024, 1024), asList(1024)); - testSizeUpdate(e, 1024, asList(1024, 512), asList(512)); - testSizeUpdate(e, 1024, asList(512, 1024), asList(1024)); - testSizeUpdate(e, 1024, asList(512, 2048), asList(2048)); - } - - @Test - public void seriesOfSizeUpdatesDefaultEncoder() throws IOException { - Function e = c -> { - Encoder encoder = new Encoder(c); - drainInitialUpdate(encoder); - return encoder; - }; - testSizeUpdate(e, 0, asList(0), asList()); - testSizeUpdate(e, 1024, asList(1024), asList()); - testSizeUpdate(e, 1024, asList(2048), asList()); - testSizeUpdate(e, 1024, asList(512), asList()); - testSizeUpdate(e, 1024, asList(1024, 1024), asList()); - testSizeUpdate(e, 1024, asList(1024, 2048), asList()); - testSizeUpdate(e, 1024, asList(2048, 1024), asList()); - testSizeUpdate(e, 1024, asList(1024, 512), asList()); - testSizeUpdate(e, 1024, asList(512, 1024), asList()); - } - - // - // https://tools.ietf.org/html/rfc7541#section-4.2 - // - @Test - public void seriesOfSizeUpdatesCustomEncoder() throws IOException { - Function e = c -> { - Encoder encoder = newCustomEncoder(c); - drainInitialUpdate(encoder); - return encoder; - }; - testSizeUpdate(e, 0, asList(0), asList()); - testSizeUpdate(e, 1024, asList(1024), asList()); - testSizeUpdate(e, 1024, asList(2048), asList(2048)); - testSizeUpdate(e, 1024, asList(512), asList(512)); - testSizeUpdate(e, 1024, asList(1024, 1024), asList()); - testSizeUpdate(e, 1024, asList(1024, 2048), asList(2048)); - testSizeUpdate(e, 1024, asList(2048, 1024), asList()); - testSizeUpdate(e, 1024, asList(1024, 512), asList(512)); - testSizeUpdate(e, 1024, asList(512, 1024), asList(512, 1024)); - } - - @Test - public void callSequenceViolations() { - { // Hasn't set up a header - Encoder e = new Encoder(0); - assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16))); - } - { // Can't set up header while there's an unfinished encoding - Encoder e = new Encoder(0); - e.indexed(32); - assertVoidThrows(IllegalStateException.class, () -> e.indexed(32)); - } - { // Can't setMaxCapacity while there's an unfinished encoding - Encoder e = new Encoder(0); - e.indexed(32); - assertVoidThrows(IllegalStateException.class, () -> e.setMaxCapacity(512)); - } - { // Hasn't set up a header - Encoder e = new Encoder(0); - e.setMaxCapacity(256); - assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16))); - } - { // Hasn't set up a header after the previous encoding - Encoder e = new Encoder(0); - e.indexed(0); - boolean encoded = e.encode(ByteBuffer.allocate(16)); - assertTrue(encoded); // assumption - assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16))); - } - } - - private static void test(Encoder encoder, - String expectedTableState, - String expectedHexdump) { - - ByteBuffer b = ByteBuffer.allocate(128); - encoder.encode(b); - b.flip(); - test(encoder, b, expectedTableState, expectedHexdump); - } - - private static void test(Encoder encoder, - ByteBuffer output, - String expectedHexdump, - String expectedTableState) { - - String actualTableState = encoder.getHeaderTable().getStateString(); - assertEquals(actualTableState, expectedTableState); - - String actualHexdump = toHexdump(output); - assertEquals(actualHexdump, expectedHexdump.replaceAll("\\n", " ")); - } - - // initial size - the size encoder is constructed with - // updates - a sequence of values for consecutive calls to encoder.setMaxCapacity - // expected - a sequence of values expected to be decoded by a decoder - private void testSizeUpdate(Function encoder, - int initialSize, - List updates, - List expected) throws IOException { - Encoder e = encoder.apply(initialSize); - updates.forEach(e::setMaxCapacity); - ByteBuffer b = ByteBuffer.allocate(64); - e.header("a", "b"); - e.encode(b); - b.flip(); - Decoder d = new Decoder(updates.isEmpty() ? initialSize : Collections.max(updates)); - List actual = new ArrayList<>(); - d.decode(b, true, new DecodingCallback() { - @Override - public void onDecoded(CharSequence name, CharSequence value) { } - - @Override - public void onSizeUpdate(int capacity) { - actual.add(capacity); - } - }); - assertEquals(actual, expected); - } - - // - // Default encoder does not need any table, therefore a subclass that - // behaves differently is needed - // - private static Encoder newCustomEncoder(int maxCapacity) { - return new Encoder(maxCapacity) { - @Override - protected int calculateCapacity(int maxCapacity) { - return maxCapacity; - } - }; - } - - private static void drainInitialUpdate(Encoder e) { - ByteBuffer b = ByteBuffer.allocate(4); - e.header("a", "b"); - boolean done; - do { - done = e.encode(b); - b.flip(); - } while (!done); - } - - private static void erase(ByteBuffer buffer) { - buffer.clear(); - while (buffer.hasRemaining()) { - buffer.put((byte) 0); - } - buffer.clear(); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/HeaderTableTest.java --- a/test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/HeaderTableTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,409 +0,0 @@ -/* - * Copyright (c) 2014, 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 java.net.http.internal.hpack; - -import org.testng.annotations.Test; -import java.net.http.internal.hpack.HeaderTable.HeaderField; - -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Random; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static java.lang.String.format; -import static org.testng.Assert.assertEquals; -import static java.net.http.internal.hpack.TestHelper.assertExceptionMessageContains; -import static java.net.http.internal.hpack.TestHelper.assertThrows; -import static java.net.http.internal.hpack.TestHelper.assertVoidThrows; -import static java.net.http.internal.hpack.TestHelper.newRandom; - -public class HeaderTableTest { - - // - // https://tools.ietf.org/html/rfc7541#appendix-A - // - // @formatter:off - private static final String SPEC = - " | 1 | :authority | |\n" + - " | 2 | :method | GET |\n" + - " | 3 | :method | POST |\n" + - " | 4 | :path | / |\n" + - " | 5 | :path | /index.html |\n" + - " | 6 | :scheme | http |\n" + - " | 7 | :scheme | https |\n" + - " | 8 | :status | 200 |\n" + - " | 9 | :status | 204 |\n" + - " | 10 | :status | 206 |\n" + - " | 11 | :status | 304 |\n" + - " | 12 | :status | 400 |\n" + - " | 13 | :status | 404 |\n" + - " | 14 | :status | 500 |\n" + - " | 15 | accept-charset | |\n" + - " | 16 | accept-encoding | gzip, deflate |\n" + - " | 17 | accept-language | |\n" + - " | 18 | accept-ranges | |\n" + - " | 19 | accept | |\n" + - " | 20 | access-control-allow-origin | |\n" + - " | 21 | age | |\n" + - " | 22 | allow | |\n" + - " | 23 | authorization | |\n" + - " | 24 | cache-control | |\n" + - " | 25 | content-disposition | |\n" + - " | 26 | content-encoding | |\n" + - " | 27 | content-language | |\n" + - " | 28 | content-length | |\n" + - " | 29 | content-location | |\n" + - " | 30 | content-range | |\n" + - " | 31 | content-type | |\n" + - " | 32 | cookie | |\n" + - " | 33 | date | |\n" + - " | 34 | etag | |\n" + - " | 35 | expect | |\n" + - " | 36 | expires | |\n" + - " | 37 | from | |\n" + - " | 38 | host | |\n" + - " | 39 | if-match | |\n" + - " | 40 | if-modified-since | |\n" + - " | 41 | if-none-match | |\n" + - " | 42 | if-range | |\n" + - " | 43 | if-unmodified-since | |\n" + - " | 44 | last-modified | |\n" + - " | 45 | link | |\n" + - " | 46 | location | |\n" + - " | 47 | max-forwards | |\n" + - " | 48 | proxy-authenticate | |\n" + - " | 49 | proxy-authorization | |\n" + - " | 50 | range | |\n" + - " | 51 | referer | |\n" + - " | 52 | refresh | |\n" + - " | 53 | retry-after | |\n" + - " | 54 | server | |\n" + - " | 55 | set-cookie | |\n" + - " | 56 | strict-transport-security | |\n" + - " | 57 | transfer-encoding | |\n" + - " | 58 | user-agent | |\n" + - " | 59 | vary | |\n" + - " | 60 | via | |\n" + - " | 61 | www-authenticate | |\n"; - // @formatter:on - - private static final int STATIC_TABLE_LENGTH = createStaticEntries().size(); - private final Random rnd = newRandom(); - - @Test - public void staticData() { - HeaderTable table = new HeaderTable(0, HPACK.getLogger()); - Map staticHeaderFields = createStaticEntries(); - - Map minimalIndexes = new HashMap<>(); - - for (Map.Entry e : staticHeaderFields.entrySet()) { - Integer idx = e.getKey(); - String hName = e.getValue().name; - Integer midx = minimalIndexes.get(hName); - if (midx == null) { - minimalIndexes.put(hName, idx); - } else { - minimalIndexes.put(hName, Math.min(idx, midx)); - } - } - - staticHeaderFields.entrySet().forEach( - e -> { - // lookup - HeaderField actualHeaderField = table.get(e.getKey()); - HeaderField expectedHeaderField = e.getValue(); - assertEquals(actualHeaderField, expectedHeaderField); - - // reverse lookup (name, value) - String hName = expectedHeaderField.name; - String hValue = expectedHeaderField.value; - int expectedIndex = e.getKey(); - int actualIndex = table.indexOf(hName, hValue); - - assertEquals(actualIndex, expectedIndex); - - // reverse lookup (name) - int expectedMinimalIndex = minimalIndexes.get(hName); - int actualMinimalIndex = table.indexOf(hName, "blah-blah"); - - assertEquals(-actualMinimalIndex, expectedMinimalIndex); - } - ); - } - - @Test - public void constructorSetsMaxSize() { - int size = rnd.nextInt(64); - HeaderTable t = new HeaderTable(size, HPACK.getLogger()); - assertEquals(t.size(), 0); - assertEquals(t.maxSize(), size); - } - - @Test - public void negativeMaximumSize() { - int maxSize = -(rnd.nextInt(100) + 1); // [-100, -1] - IllegalArgumentException e = - assertVoidThrows(IllegalArgumentException.class, - () -> new HeaderTable(0, HPACK.getLogger()).setMaxSize(maxSize)); - assertExceptionMessageContains(e, "maxSize"); - } - - @Test - public void zeroMaximumSize() { - HeaderTable table = new HeaderTable(0, HPACK.getLogger()); - table.setMaxSize(0); - assertEquals(table.maxSize(), 0); - } - - @Test - public void negativeIndex() { - int idx = -(rnd.nextInt(256) + 1); // [-256, -1] - IndexOutOfBoundsException e = - assertVoidThrows(IndexOutOfBoundsException.class, - () -> new HeaderTable(0, HPACK.getLogger()).get(idx)); - assertExceptionMessageContains(e, "index"); - } - - @Test - public void zeroIndex() { - IndexOutOfBoundsException e = - assertThrows(IndexOutOfBoundsException.class, - () -> new HeaderTable(0, HPACK.getLogger()).get(0)); - assertExceptionMessageContains(e, "index"); - } - - @Test - public void length() { - HeaderTable table = new HeaderTable(0, HPACK.getLogger()); - assertEquals(table.length(), STATIC_TABLE_LENGTH); - } - - @Test - public void indexOutsideStaticRange() { - HeaderTable table = new HeaderTable(0, HPACK.getLogger()); - int idx = table.length() + (rnd.nextInt(256) + 1); - IndexOutOfBoundsException e = - assertThrows(IndexOutOfBoundsException.class, - () -> table.get(idx)); - assertExceptionMessageContains(e, "index"); - } - - @Test - public void entryPutAfterStaticArea() { - HeaderTable table = new HeaderTable(256, HPACK.getLogger()); - int idx = table.length() + 1; - assertThrows(IndexOutOfBoundsException.class, () -> table.get(idx)); - - byte[] bytes = new byte[32]; - rnd.nextBytes(bytes); - String name = new String(bytes, StandardCharsets.ISO_8859_1); - String value = "custom-value"; - - table.put(name, value); - HeaderField f = table.get(idx); - assertEquals(name, f.name); - assertEquals(value, f.value); - } - - @Test - public void staticTableHasZeroSize() { - HeaderTable table = new HeaderTable(0, HPACK.getLogger()); - assertEquals(0, table.size()); - } - - @Test - public void lowerIndexPriority() { - HeaderTable table = new HeaderTable(256, HPACK.getLogger()); - int oldLength = table.length(); - table.put("bender", "rodriguez"); - table.put("bender", "rodriguez"); - table.put("bender", "rodriguez"); - - assertEquals(table.length(), oldLength + 3); // more like an assumption - int i = table.indexOf("bender", "rodriguez"); - assertEquals(oldLength + 1, i); - } - - @Test - public void lowerIndexPriority2() { - HeaderTable table = new HeaderTable(256, HPACK.getLogger()); - int oldLength = table.length(); - int idx = rnd.nextInt(oldLength) + 1; - HeaderField f = table.get(idx); - table.put(f.name, f.value); - assertEquals(table.length(), oldLength + 1); - int i = table.indexOf(f.name, f.value); - assertEquals(idx, i); - } - - // TODO: negative indexes check - // TODO: ensure full table clearance when adding huge header field - // TODO: ensure eviction deletes minimum needed entries, not more - - @Test - public void fifo() { - // Let's add a series of header fields - int NUM_HEADERS = 32; - HeaderTable t = new HeaderTable((32 + 4) * NUM_HEADERS, HPACK.getLogger()); - // ^ ^ - // entry overhead symbols per entry (max 2x2 digits) - for (int i = 1; i <= NUM_HEADERS; i++) { - String s = String.valueOf(i); - t.put(s, s); - } - // They MUST appear in a FIFO order: - // newer entries are at lower indexes - // older entries are at higher indexes - for (int j = 1; j <= NUM_HEADERS; j++) { - HeaderField f = t.get(STATIC_TABLE_LENGTH + j); - int actualName = Integer.parseInt(f.name); - int expectedName = NUM_HEADERS - j + 1; - assertEquals(expectedName, actualName); - } - // Entries MUST be evicted in the order they were added: - // the newer the entry the later it is evicted - for (int k = 1; k <= NUM_HEADERS; k++) { - HeaderField f = t.evictEntry(); - assertEquals(String.valueOf(k), f.name); - } - } - - @Test - public void indexOf() { - // Let's put a series of header fields - int NUM_HEADERS = 32; - HeaderTable t = new HeaderTable((32 + 4) * NUM_HEADERS, HPACK.getLogger()); - // ^ ^ - // entry overhead symbols per entry (max 2x2 digits) - for (int i = 1; i <= NUM_HEADERS; i++) { - String s = String.valueOf(i); - t.put(s, s); - } - // and verify indexOf (reverse lookup) returns correct indexes for - // full lookup - for (int j = 1; j <= NUM_HEADERS; j++) { - String s = String.valueOf(j); - int actualIndex = t.indexOf(s, s); - int expectedIndex = STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1; - assertEquals(expectedIndex, actualIndex); - } - // as well as for just a name lookup - for (int j = 1; j <= NUM_HEADERS; j++) { - String s = String.valueOf(j); - int actualIndex = t.indexOf(s, "blah"); - int expectedIndex = -(STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1); - assertEquals(expectedIndex, actualIndex); - } - // lookup for non-existent name returns 0 - assertEquals(0, t.indexOf("chupacabra", "1")); - } - - @Test - public void testToString() { - testToString0(); - } - - @Test - public void testToStringDifferentLocale() { - Locale locale = Locale.getDefault(); - Locale.setDefault(Locale.FRENCH); - try { - String s = format("%.1f", 3.1); - assertEquals("3,1", s); // assumption of the test, otherwise the test is useless - testToString0(); - } finally { - Locale.setDefault(locale); - } - } - - private void testToString0() { - HeaderTable table = new HeaderTable(0, HPACK.getLogger()); - { - int maxSize = 2048; - table.setMaxSize(maxSize); - String expected = format( - "dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)", - 0, STATIC_TABLE_LENGTH, 0, maxSize, 0.0); - assertEquals(expected, table.toString()); - } - - { - String name = "custom-name"; - String value = "custom-value"; - int size = 512; - - table.setMaxSize(size); - table.put(name, value); - String s = table.toString(); - - int used = name.length() + value.length() + 32; - double ratio = used * 100.0 / size; - - String expected = format( - "dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)", - 1, STATIC_TABLE_LENGTH + 1, used, size, ratio); - assertEquals(expected, s); - } - - { - table.setMaxSize(78); - table.put(":method", ""); - table.put(":status", ""); - String s = table.toString(); - String expected = - format("dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)", - 2, STATIC_TABLE_LENGTH + 2, 78, 78, 100.0); - assertEquals(expected, s); - } - } - - @Test - public void stateString() { - HeaderTable table = new HeaderTable(256, HPACK.getLogger()); - table.put("custom-key", "custom-header"); - // @formatter:off - assertEquals("[ 1] (s = 55) custom-key: custom-header\n" + - " Table size: 55", table.getStateString()); - // @formatter:on - } - - private static Map createStaticEntries() { - Pattern line = Pattern.compile( - "\\|\\s*(?\\d+?)\\s*\\|\\s*(?.+?)\\s*\\|\\s*(?.*?)\\s*\\|"); - Matcher m = line.matcher(SPEC); - Map result = new HashMap<>(); - while (m.find()) { - int index = Integer.parseInt(m.group("index")); - String name = m.group("name"); - String value = m.group("value"); - HeaderField f = new HeaderField(name, value); - result.put(index, f); - } - return Collections.unmodifiableMap(result); // lol - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/HuffmanTest.java --- a/test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/HuffmanTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,629 +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 java.net.http.internal.hpack; - -import org.testng.annotations.Test; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.ByteBuffer; -import java.util.Stack; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static java.lang.Integer.parseInt; -import static org.testng.Assert.*; - -public final class HuffmanTest { - - // - // https://tools.ietf.org/html/rfc7541#appendix-B - // - private static final String SPEC = - // @formatter:off - " code as bits as hex len\n" + - " sym aligned to MSB aligned in\n" + - " to LSB bits\n" + - " ( 0) |11111111|11000 1ff8 [13]\n" + - " ( 1) |11111111|11111111|1011000 7fffd8 [23]\n" + - " ( 2) |11111111|11111111|11111110|0010 fffffe2 [28]\n" + - " ( 3) |11111111|11111111|11111110|0011 fffffe3 [28]\n" + - " ( 4) |11111111|11111111|11111110|0100 fffffe4 [28]\n" + - " ( 5) |11111111|11111111|11111110|0101 fffffe5 [28]\n" + - " ( 6) |11111111|11111111|11111110|0110 fffffe6 [28]\n" + - " ( 7) |11111111|11111111|11111110|0111 fffffe7 [28]\n" + - " ( 8) |11111111|11111111|11111110|1000 fffffe8 [28]\n" + - " ( 9) |11111111|11111111|11101010 ffffea [24]\n" + - " ( 10) |11111111|11111111|11111111|111100 3ffffffc [30]\n" + - " ( 11) |11111111|11111111|11111110|1001 fffffe9 [28]\n" + - " ( 12) |11111111|11111111|11111110|1010 fffffea [28]\n" + - " ( 13) |11111111|11111111|11111111|111101 3ffffffd [30]\n" + - " ( 14) |11111111|11111111|11111110|1011 fffffeb [28]\n" + - " ( 15) |11111111|11111111|11111110|1100 fffffec [28]\n" + - " ( 16) |11111111|11111111|11111110|1101 fffffed [28]\n" + - " ( 17) |11111111|11111111|11111110|1110 fffffee [28]\n" + - " ( 18) |11111111|11111111|11111110|1111 fffffef [28]\n" + - " ( 19) |11111111|11111111|11111111|0000 ffffff0 [28]\n" + - " ( 20) |11111111|11111111|11111111|0001 ffffff1 [28]\n" + - " ( 21) |11111111|11111111|11111111|0010 ffffff2 [28]\n" + - " ( 22) |11111111|11111111|11111111|111110 3ffffffe [30]\n" + - " ( 23) |11111111|11111111|11111111|0011 ffffff3 [28]\n" + - " ( 24) |11111111|11111111|11111111|0100 ffffff4 [28]\n" + - " ( 25) |11111111|11111111|11111111|0101 ffffff5 [28]\n" + - " ( 26) |11111111|11111111|11111111|0110 ffffff6 [28]\n" + - " ( 27) |11111111|11111111|11111111|0111 ffffff7 [28]\n" + - " ( 28) |11111111|11111111|11111111|1000 ffffff8 [28]\n" + - " ( 29) |11111111|11111111|11111111|1001 ffffff9 [28]\n" + - " ( 30) |11111111|11111111|11111111|1010 ffffffa [28]\n" + - " ( 31) |11111111|11111111|11111111|1011 ffffffb [28]\n" + - " ' ' ( 32) |010100 14 [ 6]\n" + - " '!' ( 33) |11111110|00 3f8 [10]\n" + - " '\"' ( 34) |11111110|01 3f9 [10]\n" + - " '#' ( 35) |11111111|1010 ffa [12]\n" + - " '$' ( 36) |11111111|11001 1ff9 [13]\n" + - " '%' ( 37) |010101 15 [ 6]\n" + - " '&' ( 38) |11111000 f8 [ 8]\n" + - " ''' ( 39) |11111111|010 7fa [11]\n" + - " '(' ( 40) |11111110|10 3fa [10]\n" + - " ')' ( 41) |11111110|11 3fb [10]\n" + - " '*' ( 42) |11111001 f9 [ 8]\n" + - " '+' ( 43) |11111111|011 7fb [11]\n" + - " ',' ( 44) |11111010 fa [ 8]\n" + - " '-' ( 45) |010110 16 [ 6]\n" + - " '.' ( 46) |010111 17 [ 6]\n" + - " '/' ( 47) |011000 18 [ 6]\n" + - " '0' ( 48) |00000 0 [ 5]\n" + - " '1' ( 49) |00001 1 [ 5]\n" + - " '2' ( 50) |00010 2 [ 5]\n" + - " '3' ( 51) |011001 19 [ 6]\n" + - " '4' ( 52) |011010 1a [ 6]\n" + - " '5' ( 53) |011011 1b [ 6]\n" + - " '6' ( 54) |011100 1c [ 6]\n" + - " '7' ( 55) |011101 1d [ 6]\n" + - " '8' ( 56) |011110 1e [ 6]\n" + - " '9' ( 57) |011111 1f [ 6]\n" + - " ':' ( 58) |1011100 5c [ 7]\n" + - " ';' ( 59) |11111011 fb [ 8]\n" + - " '<' ( 60) |11111111|1111100 7ffc [15]\n" + - " '=' ( 61) |100000 20 [ 6]\n" + - " '>' ( 62) |11111111|1011 ffb [12]\n" + - " '?' ( 63) |11111111|00 3fc [10]\n" + - " '@' ( 64) |11111111|11010 1ffa [13]\n" + - " 'A' ( 65) |100001 21 [ 6]\n" + - " 'B' ( 66) |1011101 5d [ 7]\n" + - " 'C' ( 67) |1011110 5e [ 7]\n" + - " 'D' ( 68) |1011111 5f [ 7]\n" + - " 'E' ( 69) |1100000 60 [ 7]\n" + - " 'F' ( 70) |1100001 61 [ 7]\n" + - " 'G' ( 71) |1100010 62 [ 7]\n" + - " 'H' ( 72) |1100011 63 [ 7]\n" + - " 'I' ( 73) |1100100 64 [ 7]\n" + - " 'J' ( 74) |1100101 65 [ 7]\n" + - " 'K' ( 75) |1100110 66 [ 7]\n" + - " 'L' ( 76) |1100111 67 [ 7]\n" + - " 'M' ( 77) |1101000 68 [ 7]\n" + - " 'N' ( 78) |1101001 69 [ 7]\n" + - " 'O' ( 79) |1101010 6a [ 7]\n" + - " 'P' ( 80) |1101011 6b [ 7]\n" + - " 'Q' ( 81) |1101100 6c [ 7]\n" + - " 'R' ( 82) |1101101 6d [ 7]\n" + - " 'S' ( 83) |1101110 6e [ 7]\n" + - " 'T' ( 84) |1101111 6f [ 7]\n" + - " 'U' ( 85) |1110000 70 [ 7]\n" + - " 'V' ( 86) |1110001 71 [ 7]\n" + - " 'W' ( 87) |1110010 72 [ 7]\n" + - " 'X' ( 88) |11111100 fc [ 8]\n" + - " 'Y' ( 89) |1110011 73 [ 7]\n" + - " 'Z' ( 90) |11111101 fd [ 8]\n" + - " '[' ( 91) |11111111|11011 1ffb [13]\n" + - " '\\' ( 92) |11111111|11111110|000 7fff0 [19]\n" + - " ']' ( 93) |11111111|11100 1ffc [13]\n" + - " '^' ( 94) |11111111|111100 3ffc [14]\n" + - " '_' ( 95) |100010 22 [ 6]\n" + - " '`' ( 96) |11111111|1111101 7ffd [15]\n" + - " 'a' ( 97) |00011 3 [ 5]\n" + - " 'b' ( 98) |100011 23 [ 6]\n" + - " 'c' ( 99) |00100 4 [ 5]\n" + - " 'd' (100) |100100 24 [ 6]\n" + - " 'e' (101) |00101 5 [ 5]\n" + - " 'f' (102) |100101 25 [ 6]\n" + - " 'g' (103) |100110 26 [ 6]\n" + - " 'h' (104) |100111 27 [ 6]\n" + - " 'i' (105) |00110 6 [ 5]\n" + - " 'j' (106) |1110100 74 [ 7]\n" + - " 'k' (107) |1110101 75 [ 7]\n" + - " 'l' (108) |101000 28 [ 6]\n" + - " 'm' (109) |101001 29 [ 6]\n" + - " 'n' (110) |101010 2a [ 6]\n" + - " 'o' (111) |00111 7 [ 5]\n" + - " 'p' (112) |101011 2b [ 6]\n" + - " 'q' (113) |1110110 76 [ 7]\n" + - " 'r' (114) |101100 2c [ 6]\n" + - " 's' (115) |01000 8 [ 5]\n" + - " 't' (116) |01001 9 [ 5]\n" + - " 'u' (117) |101101 2d [ 6]\n" + - " 'v' (118) |1110111 77 [ 7]\n" + - " 'w' (119) |1111000 78 [ 7]\n" + - " 'x' (120) |1111001 79 [ 7]\n" + - " 'y' (121) |1111010 7a [ 7]\n" + - " 'z' (122) |1111011 7b [ 7]\n" + - " '{' (123) |11111111|1111110 7ffe [15]\n" + - " '|' (124) |11111111|100 7fc [11]\n" + - " '}' (125) |11111111|111101 3ffd [14]\n" + - " '~' (126) |11111111|11101 1ffd [13]\n" + - " (127) |11111111|11111111|11111111|1100 ffffffc [28]\n" + - " (128) |11111111|11111110|0110 fffe6 [20]\n" + - " (129) |11111111|11111111|010010 3fffd2 [22]\n" + - " (130) |11111111|11111110|0111 fffe7 [20]\n" + - " (131) |11111111|11111110|1000 fffe8 [20]\n" + - " (132) |11111111|11111111|010011 3fffd3 [22]\n" + - " (133) |11111111|11111111|010100 3fffd4 [22]\n" + - " (134) |11111111|11111111|010101 3fffd5 [22]\n" + - " (135) |11111111|11111111|1011001 7fffd9 [23]\n" + - " (136) |11111111|11111111|010110 3fffd6 [22]\n" + - " (137) |11111111|11111111|1011010 7fffda [23]\n" + - " (138) |11111111|11111111|1011011 7fffdb [23]\n" + - " (139) |11111111|11111111|1011100 7fffdc [23]\n" + - " (140) |11111111|11111111|1011101 7fffdd [23]\n" + - " (141) |11111111|11111111|1011110 7fffde [23]\n" + - " (142) |11111111|11111111|11101011 ffffeb [24]\n" + - " (143) |11111111|11111111|1011111 7fffdf [23]\n" + - " (144) |11111111|11111111|11101100 ffffec [24]\n" + - " (145) |11111111|11111111|11101101 ffffed [24]\n" + - " (146) |11111111|11111111|010111 3fffd7 [22]\n" + - " (147) |11111111|11111111|1100000 7fffe0 [23]\n" + - " (148) |11111111|11111111|11101110 ffffee [24]\n" + - " (149) |11111111|11111111|1100001 7fffe1 [23]\n" + - " (150) |11111111|11111111|1100010 7fffe2 [23]\n" + - " (151) |11111111|11111111|1100011 7fffe3 [23]\n" + - " (152) |11111111|11111111|1100100 7fffe4 [23]\n" + - " (153) |11111111|11111110|11100 1fffdc [21]\n" + - " (154) |11111111|11111111|011000 3fffd8 [22]\n" + - " (155) |11111111|11111111|1100101 7fffe5 [23]\n" + - " (156) |11111111|11111111|011001 3fffd9 [22]\n" + - " (157) |11111111|11111111|1100110 7fffe6 [23]\n" + - " (158) |11111111|11111111|1100111 7fffe7 [23]\n" + - " (159) |11111111|11111111|11101111 ffffef [24]\n" + - " (160) |11111111|11111111|011010 3fffda [22]\n" + - " (161) |11111111|11111110|11101 1fffdd [21]\n" + - " (162) |11111111|11111110|1001 fffe9 [20]\n" + - " (163) |11111111|11111111|011011 3fffdb [22]\n" + - " (164) |11111111|11111111|011100 3fffdc [22]\n" + - " (165) |11111111|11111111|1101000 7fffe8 [23]\n" + - " (166) |11111111|11111111|1101001 7fffe9 [23]\n" + - " (167) |11111111|11111110|11110 1fffde [21]\n" + - " (168) |11111111|11111111|1101010 7fffea [23]\n" + - " (169) |11111111|11111111|011101 3fffdd [22]\n" + - " (170) |11111111|11111111|011110 3fffde [22]\n" + - " (171) |11111111|11111111|11110000 fffff0 [24]\n" + - " (172) |11111111|11111110|11111 1fffdf [21]\n" + - " (173) |11111111|11111111|011111 3fffdf [22]\n" + - " (174) |11111111|11111111|1101011 7fffeb [23]\n" + - " (175) |11111111|11111111|1101100 7fffec [23]\n" + - " (176) |11111111|11111111|00000 1fffe0 [21]\n" + - " (177) |11111111|11111111|00001 1fffe1 [21]\n" + - " (178) |11111111|11111111|100000 3fffe0 [22]\n" + - " (179) |11111111|11111111|00010 1fffe2 [21]\n" + - " (180) |11111111|11111111|1101101 7fffed [23]\n" + - " (181) |11111111|11111111|100001 3fffe1 [22]\n" + - " (182) |11111111|11111111|1101110 7fffee [23]\n" + - " (183) |11111111|11111111|1101111 7fffef [23]\n" + - " (184) |11111111|11111110|1010 fffea [20]\n" + - " (185) |11111111|11111111|100010 3fffe2 [22]\n" + - " (186) |11111111|11111111|100011 3fffe3 [22]\n" + - " (187) |11111111|11111111|100100 3fffe4 [22]\n" + - " (188) |11111111|11111111|1110000 7ffff0 [23]\n" + - " (189) |11111111|11111111|100101 3fffe5 [22]\n" + - " (190) |11111111|11111111|100110 3fffe6 [22]\n" + - " (191) |11111111|11111111|1110001 7ffff1 [23]\n" + - " (192) |11111111|11111111|11111000|00 3ffffe0 [26]\n" + - " (193) |11111111|11111111|11111000|01 3ffffe1 [26]\n" + - " (194) |11111111|11111110|1011 fffeb [20]\n" + - " (195) |11111111|11111110|001 7fff1 [19]\n" + - " (196) |11111111|11111111|100111 3fffe7 [22]\n" + - " (197) |11111111|11111111|1110010 7ffff2 [23]\n" + - " (198) |11111111|11111111|101000 3fffe8 [22]\n" + - " (199) |11111111|11111111|11110110|0 1ffffec [25]\n" + - " (200) |11111111|11111111|11111000|10 3ffffe2 [26]\n" + - " (201) |11111111|11111111|11111000|11 3ffffe3 [26]\n" + - " (202) |11111111|11111111|11111001|00 3ffffe4 [26]\n" + - " (203) |11111111|11111111|11111011|110 7ffffde [27]\n" + - " (204) |11111111|11111111|11111011|111 7ffffdf [27]\n" + - " (205) |11111111|11111111|11111001|01 3ffffe5 [26]\n" + - " (206) |11111111|11111111|11110001 fffff1 [24]\n" + - " (207) |11111111|11111111|11110110|1 1ffffed [25]\n" + - " (208) |11111111|11111110|010 7fff2 [19]\n" + - " (209) |11111111|11111111|00011 1fffe3 [21]\n" + - " (210) |11111111|11111111|11111001|10 3ffffe6 [26]\n" + - " (211) |11111111|11111111|11111100|000 7ffffe0 [27]\n" + - " (212) |11111111|11111111|11111100|001 7ffffe1 [27]\n" + - " (213) |11111111|11111111|11111001|11 3ffffe7 [26]\n" + - " (214) |11111111|11111111|11111100|010 7ffffe2 [27]\n" + - " (215) |11111111|11111111|11110010 fffff2 [24]\n" + - " (216) |11111111|11111111|00100 1fffe4 [21]\n" + - " (217) |11111111|11111111|00101 1fffe5 [21]\n" + - " (218) |11111111|11111111|11111010|00 3ffffe8 [26]\n" + - " (219) |11111111|11111111|11111010|01 3ffffe9 [26]\n" + - " (220) |11111111|11111111|11111111|1101 ffffffd [28]\n" + - " (221) |11111111|11111111|11111100|011 7ffffe3 [27]\n" + - " (222) |11111111|11111111|11111100|100 7ffffe4 [27]\n" + - " (223) |11111111|11111111|11111100|101 7ffffe5 [27]\n" + - " (224) |11111111|11111110|1100 fffec [20]\n" + - " (225) |11111111|11111111|11110011 fffff3 [24]\n" + - " (226) |11111111|11111110|1101 fffed [20]\n" + - " (227) |11111111|11111111|00110 1fffe6 [21]\n" + - " (228) |11111111|11111111|101001 3fffe9 [22]\n" + - " (229) |11111111|11111111|00111 1fffe7 [21]\n" + - " (230) |11111111|11111111|01000 1fffe8 [21]\n" + - " (231) |11111111|11111111|1110011 7ffff3 [23]\n" + - " (232) |11111111|11111111|101010 3fffea [22]\n" + - " (233) |11111111|11111111|101011 3fffeb [22]\n" + - " (234) |11111111|11111111|11110111|0 1ffffee [25]\n" + - " (235) |11111111|11111111|11110111|1 1ffffef [25]\n" + - " (236) |11111111|11111111|11110100 fffff4 [24]\n" + - " (237) |11111111|11111111|11110101 fffff5 [24]\n" + - " (238) |11111111|11111111|11111010|10 3ffffea [26]\n" + - " (239) |11111111|11111111|1110100 7ffff4 [23]\n" + - " (240) |11111111|11111111|11111010|11 3ffffeb [26]\n" + - " (241) |11111111|11111111|11111100|110 7ffffe6 [27]\n" + - " (242) |11111111|11111111|11111011|00 3ffffec [26]\n" + - " (243) |11111111|11111111|11111011|01 3ffffed [26]\n" + - " (244) |11111111|11111111|11111100|111 7ffffe7 [27]\n" + - " (245) |11111111|11111111|11111101|000 7ffffe8 [27]\n" + - " (246) |11111111|11111111|11111101|001 7ffffe9 [27]\n" + - " (247) |11111111|11111111|11111101|010 7ffffea [27]\n" + - " (248) |11111111|11111111|11111101|011 7ffffeb [27]\n" + - " (249) |11111111|11111111|11111111|1110 ffffffe [28]\n" + - " (250) |11111111|11111111|11111101|100 7ffffec [27]\n" + - " (251) |11111111|11111111|11111101|101 7ffffed [27]\n" + - " (252) |11111111|11111111|11111101|110 7ffffee [27]\n" + - " (253) |11111111|11111111|11111101|111 7ffffef [27]\n" + - " (254) |11111111|11111111|11111110|000 7fffff0 [27]\n" + - " (255) |11111111|11111111|11111011|10 3ffffee [26]\n" + - " EOS (256) |11111111|11111111|11111111|111111 3fffffff [30]"; - // @formatter:on - - @Test - public void read_table() throws IOException { - Pattern line = Pattern.compile( - "\\(\\s*(?\\d+)\\s*\\)\\s*(?(\\|(0|1)+)+)\\s*" + - "(?[0-9a-zA-Z]+)\\s*\\[\\s*(?\\d+)\\s*\\]"); - Matcher m = line.matcher(SPEC); - int i = 0; - while (m.find()) { - String ascii = m.group("ascii"); - String binary = m.group("binary").replaceAll("\\|", ""); - String hex = m.group("hex"); - String len = m.group("len"); - - // Several sanity checks for the data read from the table, just to - // make sure what we read makes sense - assertEquals(parseInt(len), binary.length()); - assertEquals(parseInt(binary, 2), parseInt(hex, 16)); - - int expected = parseInt(ascii); - - // TODO: find actual eos, do not hardcode it! - byte[] bytes = intToBytes(0x3fffffff, 30, - parseInt(hex, 16), parseInt(len)); - - StringBuilder actual = new StringBuilder(); - Huffman.Reader t = new Huffman.Reader(); - t.read(ByteBuffer.wrap(bytes), actual, false, true); - - // What has been read MUST represent a single symbol - assertEquals(actual.length(), 1, "ascii: " + ascii); - - // It's a lot more visual to compare char as codes rather than - // characters (as some of them might not be visible) - assertEquals(actual.charAt(0), expected); - i++; - } - assertEquals(i, 257); // 256 + EOS - } - - // - // https://tools.ietf.org/html/rfc7541#appendix-C.4.1 - // - @Test - public void read_1() { - read("f1e3 c2e5 f23a 6ba0 ab90 f4ff", "www.example.com"); - } - - @Test - public void write_1() { - write("www.example.com", "f1e3 c2e5 f23a 6ba0 ab90 f4ff"); - } - - // - // https://tools.ietf.org/html/rfc7541#appendix-C.4.2 - // - @Test - public void read_2() { - read("a8eb 1064 9cbf", "no-cache"); - } - - @Test - public void write_2() { - write("no-cache", "a8eb 1064 9cbf"); - } - - // - // https://tools.ietf.org/html/rfc7541#appendix-C.4.3 - // - @Test - public void read_3() { - read("25a8 49e9 5ba9 7d7f", "custom-key"); - } - - @Test - public void write_3() { - write("custom-key", "25a8 49e9 5ba9 7d7f"); - } - - // - // https://tools.ietf.org/html/rfc7541#appendix-C.4.3 - // - @Test - public void read_4() { - read("25a8 49e9 5bb8 e8b4 bf", "custom-value"); - } - - @Test - public void write_4() { - write("custom-value", "25a8 49e9 5bb8 e8b4 bf"); - } - - // - // https://tools.ietf.org/html/rfc7541#appendix-C.6.1 - // - @Test - public void read_5() { - read("6402", "302"); - } - - @Test - public void write_5() { - write("302", "6402"); - } - - // - // https://tools.ietf.org/html/rfc7541#appendix-C.6.1 - // - @Test - public void read_6() { - read("aec3 771a 4b", "private"); - } - - @Test - public void write_6() { - write("private", "aec3 771a 4b"); - } - - // - // https://tools.ietf.org/html/rfc7541#appendix-C.6.1 - // - @Test - public void read_7() { - read("d07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff", - "Mon, 21 Oct 2013 20:13:21 GMT"); - } - - @Test - public void write_7() { - write("Mon, 21 Oct 2013 20:13:21 GMT", - "d07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff"); - } - - // - // https://tools.ietf.org/html/rfc7541#appendix-C.6.1 - // - @Test - public void read_8() { - read("9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3", - "https://www.example.com"); - } - - @Test - public void write_8() { - write("https://www.example.com", - "9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3"); - } - - // - // https://tools.ietf.org/html/rfc7541#appendix-C.6.2 - // - @Test - public void read_9() { - read("640e ff", "307"); - } - - @Test - public void write_9() { - write("307", "640e ff"); - } - - // - // https://tools.ietf.org/html/rfc7541#appendix-C.6.3 - // - @Test - public void read_10() { - read("d07a be94 1054 d444 a820 0595 040b 8166 e084 a62d 1bff", - "Mon, 21 Oct 2013 20:13:22 GMT"); - } - - @Test - public void write_10() { - write("Mon, 21 Oct 2013 20:13:22 GMT", - "d07a be94 1054 d444 a820 0595 040b 8166 e084 a62d 1bff"); - } - - // - // https://tools.ietf.org/html/rfc7541#appendix-C.6.3 - // - @Test - public void read_11() { - read("9bd9 ab", "gzip"); - } - - @Test - public void write_11() { - write("gzip", "9bd9 ab"); - } - - // - // https://tools.ietf.org/html/rfc7541#appendix-C.6.3 - // - @Test - public void read_12() { - read("94e7 821d d7f2 e6c7 b335 dfdf cd5b 3960 " + - "d5af 2708 7f36 72c1 ab27 0fb5 291f 9587 " + - "3160 65c0 03ed 4ee5 b106 3d50 07", - "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"); - } - - @Test - public void test_trie_has_no_empty_nodes() { - Huffman.Node root = Huffman.INSTANCE.getRoot(); - Stack backlog = new Stack<>(); - backlog.push(root); - while (!backlog.isEmpty()) { - Huffman.Node n = backlog.pop(); - // The only type of nodes we couldn't possibly catch during - // construction is an empty node: no children and no char - if (n.left != null) { - backlog.push(n.left); - } - if (n.right != null) { - backlog.push(n.right); - } - assertFalse(!n.charIsSet && n.left == null && n.right == null, - "Empty node in the trie"); - } - } - - @Test - public void test_trie_has_257_nodes() { - int count = 0; - Huffman.Node root = Huffman.INSTANCE.getRoot(); - Stack backlog = new Stack<>(); - backlog.push(root); - while (!backlog.isEmpty()) { - Huffman.Node n = backlog.pop(); - if (n.left != null) { - backlog.push(n.left); - } - if (n.right != null) { - backlog.push(n.right); - } - if (n.isLeaf()) { - count++; - } - } - assertEquals(count, 257); - } - - @Test - public void cant_encode_outside_byte() { - TestHelper.Block coding = - () -> new Huffman.Writer() - .from(((char) 256) + "", 0, 1) - .write(ByteBuffer.allocate(1)); - RuntimeException e = - TestHelper.assertVoidThrows(RuntimeException.class, coding); - TestHelper.assertExceptionMessageContains(e, "char"); - } - - private static void read(String hexdump, String decoded) { - ByteBuffer source = SpecHelper.toBytes(hexdump); - Appendable actual = new StringBuilder(); - try { - new Huffman.Reader().read(source, actual, true); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - assertEquals(actual.toString(), decoded); - } - - private static void write(String decoded, String hexdump) { - int n = Huffman.INSTANCE.lengthOf(decoded); - ByteBuffer destination = ByteBuffer.allocate(n); // Extra margin (1) to test having more bytes in the destination than needed is ok - Huffman.Writer writer = new Huffman.Writer(); - BuffersTestingKit.forEachSplit(destination, byteBuffers -> { - writer.from(decoded, 0, decoded.length()); - boolean written = false; - for (ByteBuffer b : byteBuffers) { - int pos = b.position(); - written = writer.write(b); - b.position(pos); - } - assertTrue(written); - ByteBuffer concated = BuffersTestingKit.concat(byteBuffers); - String actual = SpecHelper.toHexdump(concated); - assertEquals(actual, hexdump); - writer.reset(); - }); - } - - // - // It's not very pretty, yes I know that - // - // hex: - // - // |31|30|...|N-1|...|01|00| - // \ / - // codeLength - // - // hex <<= 32 - codeLength; (align to MSB): - // - // |31|30|...|32-N|...|01|00| - // \ / - // codeLength - // - // EOS: - // - // |31|30|...|M-1|...|01|00| - // \ / - // eosLength - // - // eos <<= 32 - eosLength; (align to MSB): - // - // pad with MSBs of EOS: - // - // |31|30|...|32-N|32-N-1|...|01|00| - // | 32|...| - // - // Finally, split into byte[] - // - private byte[] intToBytes(int eos, int eosLength, int hex, int codeLength) { - hex <<= 32 - codeLength; - eos >>= codeLength - (32 - eosLength); - hex |= eos; - int n = (int) Math.ceil(codeLength / 8.0); - byte[] result = new byte[n]; - for (int i = 0; i < n; i++) { - result[i] = (byte) (hex >> (32 - 8 * (i + 1))); - } - return result; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/SpecHelper.java --- a/test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/SpecHelper.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2014, 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 java.net.http.internal.hpack; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -// -// THIS IS NOT A TEST -// -public final class SpecHelper { - - private SpecHelper() { - throw new AssertionError(); - } - - public static ByteBuffer toBytes(String hexdump) { - Pattern hexByte = Pattern.compile("[0-9a-fA-F]{2}"); - List bytes = new ArrayList<>(); - Matcher matcher = hexByte.matcher(hexdump); - while (matcher.find()) { - bytes.add(matcher.group(0)); - } - ByteBuffer result = ByteBuffer.allocate(bytes.size()); - for (String f : bytes) { - result.put((byte) Integer.parseInt(f, 16)); - } - result.flip(); - return result; - } - - public static String toHexdump(ByteBuffer bb) { - List words = new ArrayList<>(); - int i = 0; - while (bb.hasRemaining()) { - if (i % 2 == 0) { - words.add(""); - } - byte b = bb.get(); - String hex = Integer.toHexString(256 + Byte.toUnsignedInt(b)).substring(1); - words.set(i / 2, words.get(i / 2) + hex); - i++; - } - return words.stream().collect(Collectors.joining(" ")); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/TestHelper.java --- a/test/jdk/java/net/httpclient/http2/java.net.http/java/net/http/internal/hpack/TestHelper.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2014, 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 java.net.http.internal.hpack; - -import org.testng.annotations.Test; - -import java.util.Objects; -import java.util.Random; - -public final class TestHelper { - - public static Random newRandom() { - long seed = Long.getLong("jdk.test.lib.random.seed", System.currentTimeMillis()); - System.out.println("new java.util.Random(" + seed + ")"); - return new Random(seed); - } - - public static T assertVoidThrows(Class clazz, Block code) { - return assertThrows(clazz, () -> { - code.run(); - return null; - }); - } - - public static T assertThrows(Class clazz, ReturningBlock code) { - Objects.requireNonNull(clazz, "clazz == null"); - Objects.requireNonNull(code, "code == null"); - try { - code.run(); - } catch (Throwable t) { - if (clazz.isInstance(t)) { - return clazz.cast(t); - } - throw new AssertionError("Expected to catch exception of type " - + clazz.getCanonicalName() + ", instead caught " - + t.getClass().getCanonicalName(), t); - - } - throw new AssertionError( - "Expected to catch exception of type " + clazz.getCanonicalName() - + ", but caught nothing"); - } - - public static T assertDoesNotThrow(ReturningBlock code) { - Objects.requireNonNull(code, "code == null"); - try { - return code.run(); - } catch (Throwable t) { - throw new AssertionError( - "Expected code block to exit normally, instead " + - "caught " + t.getClass().getCanonicalName(), t); - } - } - - public static void assertVoidDoesNotThrow(Block code) { - Objects.requireNonNull(code, "code == null"); - try { - code.run(); - } catch (Throwable t) { - throw new AssertionError( - "Expected code block to exit normally, instead " + - "caught " + t.getClass().getCanonicalName(), t); - } - } - - - public static void assertExceptionMessageContains(Throwable t, - CharSequence firstSubsequence, - CharSequence... others) { - assertCharSequenceContains(t.getMessage(), firstSubsequence, others); - } - - public static void assertCharSequenceContains(CharSequence s, - CharSequence firstSubsequence, - CharSequence... others) { - if (s == null) { - throw new NullPointerException("Exception message is null"); - } - String str = s.toString(); - String missing = null; - if (!str.contains(firstSubsequence.toString())) { - missing = firstSubsequence.toString(); - } else { - for (CharSequence o : others) { - if (!str.contains(o.toString())) { - missing = o.toString(); - break; - } - } - } - if (missing != null) { - throw new AssertionError("CharSequence '" + s + "'" + " does not " - + "contain subsequence '" + missing + "'"); - } - } - - public interface ReturningBlock { - T run() throws Throwable; - } - - public interface Block { - void run() throws Throwable; - } - - // tests - - @Test - public void assertThrows() { - assertThrows(NullPointerException.class, () -> ((Object) null).toString()); - } - - @Test - public void assertThrowsWrongType() { - try { - assertThrows(IllegalArgumentException.class, () -> ((Object) null).toString()); - } catch (AssertionError e) { - Throwable cause = e.getCause(); - String message = e.getMessage(); - if (cause != null - && cause instanceof NullPointerException - && message != null - && message.contains("instead caught")) { - return; - } - } - throw new AssertionError(); - } - - @Test - public void assertThrowsNoneCaught() { - try { - assertThrows(IllegalArgumentException.class, () -> null); - } catch (AssertionError e) { - Throwable cause = e.getCause(); - String message = e.getMessage(); - if (cause == null - && message != null - && message.contains("but caught nothing")) { - return; - } - } - throw new AssertionError(); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/BinaryPrimitivesTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/BinaryPrimitivesTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2014, 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.internal.net.http.hpack; + +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; +import static jdk.internal.net.http.hpack.BuffersTestingKit.*; +import static jdk.internal.net.http.hpack.TestHelper.newRandom; + +// +// Some of the tests below overlap in what they test. This allows to diagnose +// bugs quicker and with less pain by simply ruling out common working bits. +// +public final class BinaryPrimitivesTest { + + private final Random rnd = newRandom(); + + @Test + public void integerRead1() { + verifyRead(bytes(0b00011111, 0b10011010, 0b00001010), 1337, 5); + } + + @Test + public void integerRead2() { + verifyRead(bytes(0b00001010), 10, 5); + } + + @Test + public void integerRead3() { + verifyRead(bytes(0b00101010), 42, 8); + } + + @Test + public void integerWrite1() { + verifyWrite(bytes(0b00011111, 0b10011010, 0b00001010), 1337, 5); + } + + @Test + public void integerWrite2() { + verifyWrite(bytes(0b00001010), 10, 5); + } + + @Test + public void integerWrite3() { + verifyWrite(bytes(0b00101010), 42, 8); + } + + // + // Since readInteger(x) is the inverse of writeInteger(x), thus: + // + // for all x: readInteger(writeInteger(x)) == x + // + @Test + public void integerIdentity() throws IOException { + final int MAX_VALUE = 1 << 22; + int totalCases = 0; + int maxFilling = 0; + IntegerReader r = new IntegerReader(); + IntegerWriter w = new IntegerWriter(); + ByteBuffer buf = ByteBuffer.allocate(8); + for (int N = 1; N < 9; N++) { + for (int expected = 0; expected <= MAX_VALUE; expected++) { + w.reset().configure(expected, N, 1).write(buf); + buf.flip(); + totalCases++; + maxFilling = Math.max(maxFilling, buf.remaining()); + r.reset().configure(N).read(buf); + assertEquals(r.get(), expected); + buf.clear(); + } + } + System.out.printf("totalCases: %,d, maxFilling: %,d, maxValue: %,d%n", + totalCases, maxFilling, MAX_VALUE); + } + + @Test + public void integerReadChunked() { + final int NUM_TESTS = 1024; + IntegerReader r = new IntegerReader(); + ByteBuffer bb = ByteBuffer.allocate(8); + IntegerWriter w = new IntegerWriter(); + for (int i = 0; i < NUM_TESTS; i++) { + final int N = 1 + rnd.nextInt(8); + final int expected = rnd.nextInt(Integer.MAX_VALUE) + 1; + w.reset().configure(expected, N, rnd.nextInt()).write(bb); + bb.flip(); + + forEachSplit(bb, + (buffers) -> { + Iterable buf = relocateBuffers(injectEmptyBuffers(buffers)); + r.configure(N); + for (ByteBuffer b : buf) { + try { + r.read(b); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + assertEquals(r.get(), expected); + r.reset(); + }); + bb.clear(); + } + } + + // FIXME: use maxValue in the test + + @Test + // FIXME: tune values for better coverage + public void integerWriteChunked() { + ByteBuffer bb = ByteBuffer.allocate(6); + IntegerWriter w = new IntegerWriter(); + IntegerReader r = new IntegerReader(); + for (int i = 0; i < 1024; i++) { // number of tests + final int N = 1 + rnd.nextInt(8); + final int payload = rnd.nextInt(255); + final int expected = rnd.nextInt(Integer.MAX_VALUE) + 1; + + forEachSplit(bb, + (buffers) -> { + List buf = new ArrayList<>(); + relocateBuffers(injectEmptyBuffers(buffers)).forEach(buf::add); + boolean written = false; + w.configure(expected, N, payload); // TODO: test for payload it can be read after written + for (ByteBuffer b : buf) { + int pos = b.position(); + written = w.write(b); + b.position(pos); + } + if (!written) { + fail("please increase bb size"); + } + try { + r.configure(N).read(concat(buf)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + // TODO: check payload here + assertEquals(r.get(), expected); + w.reset(); + r.reset(); + bb.clear(); + }); + } + } + + + // + // Since readString(x) is the inverse of writeString(x), thus: + // + // for all x: readString(writeString(x)) == x + // + @Test + public void stringIdentity() throws IOException { + final int MAX_STRING_LENGTH = 4096; + ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6); // it takes 6 bytes to encode string length of Integer.MAX_VALUE + CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH); + StringReader reader = new StringReader(); + StringWriter writer = new StringWriter(); + for (int len = 0; len <= MAX_STRING_LENGTH; len++) { + for (int i = 0; i < 64; i++) { + // not so much "test in isolation", I know... we're testing .reset() as well + bytes.clear(); + chars.clear(); + + byte[] b = new byte[len]; + rnd.nextBytes(b); + + String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string + + boolean written = writer + .configure(CharBuffer.wrap(expected), 0, expected.length(), false) + .write(bytes); + + if (!written) { + fail("please increase 'bytes' size"); + } + bytes.flip(); + reader.read(bytes, chars); + chars.flip(); + assertEquals(chars.toString(), expected); + reader.reset(); + writer.reset(); + } + } + } + +// @Test +// public void huffmanStringWriteChunked() { +// fail(); +// } +// +// @Test +// public void huffmanStringReadChunked() { +// fail(); +// } + + @Test + public void stringWriteChunked() { + final int MAX_STRING_LENGTH = 8; + final ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6); + final CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH); + final StringReader reader = new StringReader(); + final StringWriter writer = new StringWriter(); + for (int len = 0; len <= MAX_STRING_LENGTH; len++) { + + byte[] b = new byte[len]; + rnd.nextBytes(b); + + String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string + + forEachSplit(bytes, (buffers) -> { + writer.configure(expected, 0, expected.length(), false); + boolean written = false; + for (ByteBuffer buf : buffers) { + int p0 = buf.position(); + written = writer.write(buf); + buf.position(p0); + } + if (!written) { + fail("please increase 'bytes' size"); + } + try { + reader.read(concat(buffers), chars); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + chars.flip(); + assertEquals(chars.toString(), expected); + reader.reset(); + writer.reset(); + chars.clear(); + bytes.clear(); + }); + } + } + + @Test + public void stringReadChunked() { + final int MAX_STRING_LENGTH = 16; + final ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6); + final CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH); + final StringReader reader = new StringReader(); + final StringWriter writer = new StringWriter(); + for (int len = 0; len <= MAX_STRING_LENGTH; len++) { + + byte[] b = new byte[len]; + rnd.nextBytes(b); + + String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string + + boolean written = writer + .configure(CharBuffer.wrap(expected), 0, expected.length(), false) + .write(bytes); + writer.reset(); + + if (!written) { + fail("please increase 'bytes' size"); + } + bytes.flip(); + + forEachSplit(bytes, (buffers) -> { + for (ByteBuffer buf : buffers) { + int p0 = buf.position(); + try { + reader.read(buf, chars); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + buf.position(p0); + } + chars.flip(); + assertEquals(chars.toString(), expected); + reader.reset(); + chars.clear(); + }); + + bytes.clear(); + } + } + +// @Test +// public void test_Huffman_String_Identity() { +// StringWriter writer = new StringWriter(); +// StringReader reader = new StringReader(); +// // 256 * 8 gives 2048 bits in case of plain 8 bit coding +// // 256 * 30 gives you 7680 bits or 960 bytes in case of almost +// // improbable event of 256 30 bits symbols in a row +// ByteBuffer binary = ByteBuffer.allocate(960); +// CharBuffer text = CharBuffer.allocate(960 / 5); // 5 = minimum code length +// for (int len = 0; len < 128; len++) { +// for (int i = 0; i < 256; i++) { +// // not so much "test in isolation", I know... +// binary.clear(); +// +// byte[] bytes = new byte[len]; +// rnd.nextBytes(bytes); +// +// String s = new String(bytes, StandardCharsets.ISO_8859_1); +// +// writer.write(CharBuffer.wrap(s), binary, true); +// binary.flip(); +// reader.read(binary, text); +// text.flip(); +// assertEquals(text.toString(), s); +// } +// } +// } + + // TODO: atomic failures: e.g. readonly/overflow + + private static byte[] bytes(int... data) { + byte[] bytes = new byte[data.length]; + for (int i = 0; i < data.length; i++) { + bytes[i] = (byte) data[i]; + } + return bytes; + } + + private static void verifyRead(byte[] data, int expected, int N) { + ByteBuffer buf = ByteBuffer.wrap(data, 0, data.length); + IntegerReader reader = new IntegerReader(); + try { + reader.configure(N).read(buf); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + assertEquals(expected, reader.get()); + } + + private void verifyWrite(byte[] expected, int data, int N) { + IntegerWriter w = new IntegerWriter(); + ByteBuffer buf = ByteBuffer.allocate(2 * expected.length); + w.configure(data, N, 1).write(buf); + buf.flip(); + assertEquals(ByteBuffer.wrap(expected), buf); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/BuffersTestingKit.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/BuffersTestingKit.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,210 @@ +/* + * 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.internal.net.http.hpack; + +import java.nio.ByteBuffer; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import static java.nio.ByteBuffer.allocate; + +public final class BuffersTestingKit { + + /** + * Relocates a {@code [position, limit)} region of the given buffer to + * corresponding region in a new buffer starting with provided {@code + * newPosition}. + * + *

Might be useful to make sure ByteBuffer's users do not rely on any + * absolute positions, but solely on what's reported by position(), limit(). + * + *

The contents between the given buffer and the returned one are not + * shared. + */ + public static ByteBuffer relocate(ByteBuffer buffer, int newPosition, + int newCapacity) { + int oldPosition = buffer.position(); + int oldLimit = buffer.limit(); + + if (newPosition + oldLimit - oldPosition > newCapacity) { + throw new IllegalArgumentException(); + } + + ByteBuffer result; + if (buffer.isDirect()) { + result = ByteBuffer.allocateDirect(newCapacity); + } else { + result = allocate(newCapacity); + } + + result.position(newPosition); + result.put(buffer).limit(result.position()).position(newPosition); + buffer.position(oldPosition); + + if (buffer.isReadOnly()) { + return result.asReadOnlyBuffer(); + } + return result; + } + + public static Iterable relocateBuffers( + Iterable source) { + return () -> + new Iterator() { + + private final Iterator it = source.iterator(); + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public ByteBuffer next() { + ByteBuffer buf = it.next(); + int remaining = buf.remaining(); + int newCapacity = remaining + random.nextInt(17); + int newPosition = random.nextInt(newCapacity - remaining + 1); + return relocate(buf, newPosition, newCapacity); + } + }; + } + + // TODO: not always of size 0 (it's fine for buffer to report !b.hasRemaining()) + public static Iterable injectEmptyBuffers( + Iterable source) { + return injectEmptyBuffers(source, () -> allocate(0)); + } + + public static Iterable injectEmptyBuffers( + Iterable source, + Supplier emptyBufferFactory) { + + return () -> + new Iterator() { + + private final Iterator it = source.iterator(); + private ByteBuffer next = calculateNext(); + + private ByteBuffer calculateNext() { + if (random.nextBoolean()) { + return emptyBufferFactory.get(); + } else if (it.hasNext()) { + return it.next(); + } else { + return null; + } + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public ByteBuffer next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + ByteBuffer next = this.next; + this.next = calculateNext(); + return next; + } + }; + } + + public static ByteBuffer concat(Iterable split) { + return concat(split, ByteBuffer::allocate); + } + + public static ByteBuffer concat(Iterable split, + Function concatBufferFactory) { + int size = 0; + for (ByteBuffer bb : split) { + size += bb.remaining(); + } + + ByteBuffer result = concatBufferFactory.apply(size); + for (ByteBuffer bb : split) { + result.put(bb); + } + + result.flip(); + return result; + } + + public static void forEachSplit(ByteBuffer bb, + Consumer> action) { + forEachSplit(bb.remaining(), + (lengths) -> { + int end = bb.position(); + List buffers = new LinkedList<>(); + for (int len : lengths) { + ByteBuffer d = bb.duplicate(); + d.position(end); + d.limit(end + len); + end += len; + buffers.add(d); + } + action.accept(buffers); + }); + } + + private static void forEachSplit(int n, Consumer> action) { + forEachSplit(n, new Stack<>(), action); + } + + private static void forEachSplit(int n, Stack path, + Consumer> action) { + if (n == 0) { + action.accept(path); + } else { + for (int i = 1; i <= n; i++) { + path.push(i); + forEachSplit(n - i, path, action); + path.pop(); + } + } + } + + private static final Random random = new Random(); + + private BuffersTestingKit() { + throw new InternalError(); + } + +// public static void main(String[] args) { +// +// List buffers = Arrays.asList( +// (ByteBuffer) allocate(3).position(1).limit(2), +// allocate(0), +// allocate(7)); +// +// Iterable buf = relocateBuffers(injectEmptyBuffers(buffers)); +// List result = new ArrayList<>(); +// buf.forEach(result::add); +// System.out.println(result); +// } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/CircularBufferTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/CircularBufferTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,125 @@ +/* + * 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. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.hpack; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import jdk.internal.net.http.hpack.HeaderTable.CircularBuffer; + +import java.util.Queue; +import java.util.Random; +import java.util.concurrent.ArrayBlockingQueue; + +import static org.testng.Assert.assertEquals; +import static jdk.internal.net.http.hpack.TestHelper.newRandom; + +public final class CircularBufferTest { + + private final Random r = newRandom(); + + @BeforeClass + public void setUp() { + r.setSeed(System.currentTimeMillis()); + } + + @Test + public void queue() { + for (int capacity = 1; capacity <= 2048; capacity++) { + queueOnce(capacity, 32); + } + } + + @Test + public void resize() { + for (int capacity = 1; capacity <= 4096; capacity++) { + resizeOnce(capacity); + } + } + + @Test + public void downSizeEmptyBuffer() { + CircularBuffer buffer = new CircularBuffer<>(16); + buffer.resize(15); + } + + private void resizeOnce(int capacity) { + + int nextNumberToPut = 0; + + Queue referenceQueue = new ArrayBlockingQueue<>(capacity); + CircularBuffer buffer = new CircularBuffer<>(capacity); + + // Fill full, so the next add will wrap + for (int i = 0; i < capacity; i++, nextNumberToPut++) { + buffer.add(nextNumberToPut); + referenceQueue.add(nextNumberToPut); + } + int gets = r.nextInt(capacity); // [0, capacity) + for (int i = 0; i < gets; i++) { + referenceQueue.poll(); + buffer.remove(); + } + int puts = r.nextInt(gets + 1); // [0, gets] + for (int i = 0; i < puts; i++, nextNumberToPut++) { + buffer.add(nextNumberToPut); + referenceQueue.add(nextNumberToPut); + } + + Integer[] expected = referenceQueue.toArray(new Integer[0]); + buffer.resize(expected.length); + + assertEquals(buffer.elements, expected); + } + + private void queueOnce(int capacity, int numWraps) { + + Queue referenceQueue = new ArrayBlockingQueue<>(capacity); + CircularBuffer buffer = new CircularBuffer<>(capacity); + + int nextNumberToPut = 0; + int totalPuts = 0; + int putsLimit = capacity * numWraps; + int remainingCapacity = capacity; + int size = 0; + + while (totalPuts < putsLimit) { + assert remainingCapacity + size == capacity; + int puts = r.nextInt(remainingCapacity + 1); // [0, remainingCapacity] + remainingCapacity -= puts; + size += puts; + for (int i = 0; i < puts; i++, nextNumberToPut++) { + referenceQueue.add(nextNumberToPut); + buffer.add(nextNumberToPut); + } + totalPuts += puts; + int gets = r.nextInt(size + 1); // [0, size] + size -= gets; + remainingCapacity += gets; + for (int i = 0; i < gets; i++) { + Integer expected = referenceQueue.poll(); + Integer actual = buffer.remove(); + assertEquals(actual, expected); + } + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/DecoderTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/DecoderTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,685 @@ +/* + * 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.internal.net.http.hpack; + +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static jdk.internal.net.http.hpack.TestHelper.*; + +// +// Tests whose names start with "testX" are the ones captured from real HPACK +// use cases +// +public final class DecoderTest { + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.1 + // + @Test + public void example1() { + // @formatter:off + test("400a 6375 7374 6f6d 2d6b 6579 0d63 7573\n" + + "746f 6d2d 6865 6164 6572", + + "[ 1] (s = 55) custom-key: custom-header\n" + + " Table size: 55", + + "custom-key: custom-header"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.2 + // + @Test + public void example2() { + // @formatter:off + test("040c 2f73 616d 706c 652f 7061 7468", + "empty.", + ":path: /sample/path"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.3 + // + @Test + public void example3() { + // @formatter:off + test("1008 7061 7373 776f 7264 0673 6563 7265\n" + + "74", + "empty.", + "password: secret"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.4 + // + @Test + public void example4() { + // @formatter:off + test("82", + "empty.", + ":method: GET"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.3 + // + @Test + public void example5() { + // @formatter:off + Decoder d = new Decoder(256); + + test(d, "8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" + + "2e63 6f6d", + + "[ 1] (s = 57) :authority: www.example.com\n" + + " Table size: 57", + + ":method: GET\n" + + ":scheme: http\n" + + ":path: /\n" + + ":authority: www.example.com"); + + test(d, "8286 84be 5808 6e6f 2d63 6163 6865", + + "[ 1] (s = 53) cache-control: no-cache\n" + + "[ 2] (s = 57) :authority: www.example.com\n" + + " Table size: 110", + + ":method: GET\n" + + ":scheme: http\n" + + ":path: /\n" + + ":authority: www.example.com\n" + + "cache-control: no-cache"); + + test(d, "8287 85bf 400a 6375 7374 6f6d 2d6b 6579\n" + + "0c63 7573 746f 6d2d 7661 6c75 65", + + "[ 1] (s = 54) custom-key: custom-value\n" + + "[ 2] (s = 53) cache-control: no-cache\n" + + "[ 3] (s = 57) :authority: www.example.com\n" + + " Table size: 164", + + ":method: GET\n" + + ":scheme: https\n" + + ":path: /index.html\n" + + ":authority: www.example.com\n" + + "custom-key: custom-value"); + + // @formatter:on + } + + @Test + public void example5AllSplits() { + // @formatter:off + testAllSplits( + "8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" + + "2e63 6f6d", + + "[ 1] (s = 57) :authority: www.example.com\n" + + " Table size: 57", + + ":method: GET\n" + + ":scheme: http\n" + + ":path: /\n" + + ":authority: www.example.com"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.4 + // + @Test + public void example6() { + // @formatter:off + Decoder d = new Decoder(256); + + test(d, "8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4\n" + + "ff", + + "[ 1] (s = 57) :authority: www.example.com\n" + + " Table size: 57", + + ":method: GET\n" + + ":scheme: http\n" + + ":path: /\n" + + ":authority: www.example.com"); + + test(d, "8286 84be 5886 a8eb 1064 9cbf", + + "[ 1] (s = 53) cache-control: no-cache\n" + + "[ 2] (s = 57) :authority: www.example.com\n" + + " Table size: 110", + + ":method: GET\n" + + ":scheme: http\n" + + ":path: /\n" + + ":authority: www.example.com\n" + + "cache-control: no-cache"); + + test(d, "8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925\n" + + "a849 e95b b8e8 b4bf", + + "[ 1] (s = 54) custom-key: custom-value\n" + + "[ 2] (s = 53) cache-control: no-cache\n" + + "[ 3] (s = 57) :authority: www.example.com\n" + + " Table size: 164", + + ":method: GET\n" + + ":scheme: https\n" + + ":path: /index.html\n" + + ":authority: www.example.com\n" + + "custom-key: custom-value"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.5 + // + @Test + public void example7() { + // @formatter:off + Decoder d = new Decoder(256); + + test(d, "4803 3330 3258 0770 7269 7661 7465 611d\n" + + "4d6f 6e2c 2032 3120 4f63 7420 3230 3133\n" + + "2032 303a 3133 3a32 3120 474d 546e 1768\n" + + "7474 7073 3a2f 2f77 7777 2e65 7861 6d70\n" + + "6c65 2e63 6f6d", + + "[ 1] (s = 63) location: https://www.example.com\n" + + "[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 3] (s = 52) cache-control: private\n" + + "[ 4] (s = 42) :status: 302\n" + + " Table size: 222", + + ":status: 302\n" + + "cache-control: private\n" + + "date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "location: https://www.example.com"); + + test(d, "4803 3330 37c1 c0bf", + + "[ 1] (s = 42) :status: 307\n" + + "[ 2] (s = 63) location: https://www.example.com\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 4] (s = 52) cache-control: private\n" + + " Table size: 222", + + ":status: 307\n" + + "cache-control: private\n" + + "date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "location: https://www.example.com"); + + test(d, "88c1 611d 4d6f 6e2c 2032 3120 4f63 7420\n" + + "3230 3133 2032 303a 3133 3a32 3220 474d\n" + + "54c0 5a04 677a 6970 7738 666f 6f3d 4153\n" + + "444a 4b48 514b 425a 584f 5157 454f 5049\n" + + "5541 5851 5745 4f49 553b 206d 6178 2d61\n" + + "6765 3d33 3630 303b 2076 6572 7369 6f6e\n" + + "3d31", + + "[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" + + "[ 2] (s = 52) content-encoding: gzip\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" + + " Table size: 215", + + ":status: 200\n" + + "cache-control: private\n" + + "date: Mon, 21 Oct 2013 20:13:22 GMT\n" + + "location: https://www.example.com\n" + + "content-encoding: gzip\n" + + "set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.6 + // + @Test + public void example8() { + // @formatter:off + Decoder d = new Decoder(256); + + test(d, "4882 6402 5885 aec3 771a 4b61 96d0 7abe\n" + + "9410 54d4 44a8 2005 9504 0b81 66e0 82a6\n" + + "2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8\n" + + "e9ae 82ae 43d3", + + "[ 1] (s = 63) location: https://www.example.com\n" + + "[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 3] (s = 52) cache-control: private\n" + + "[ 4] (s = 42) :status: 302\n" + + " Table size: 222", + + ":status: 302\n" + + "cache-control: private\n" + + "date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "location: https://www.example.com"); + + test(d, "4883 640e ffc1 c0bf", + + "[ 1] (s = 42) :status: 307\n" + + "[ 2] (s = 63) location: https://www.example.com\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 4] (s = 52) cache-control: private\n" + + " Table size: 222", + + ":status: 307\n" + + "cache-control: private\n" + + "date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "location: https://www.example.com"); + + test(d, "88c1 6196 d07a be94 1054 d444 a820 0595\n" + + "040b 8166 e084 a62d 1bff c05a 839b d9ab\n" + + "77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b\n" + + "3960 d5af 2708 7f36 72c1 ab27 0fb5 291f\n" + + "9587 3160 65c0 03ed 4ee5 b106 3d50 07", + + "[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" + + "[ 2] (s = 52) content-encoding: gzip\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" + + " Table size: 215", + + ":status: 200\n" + + "cache-control: private\n" + + "date: Mon, 21 Oct 2013 20:13:22 GMT\n" + + "location: https://www.example.com\n" + + "content-encoding: gzip\n" + + "set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"); + // @formatter:on + } + + @Test + // One of responses from Apache Server that helped to catch a bug + public void testX() { + Decoder d = new Decoder(4096); + // @formatter:off + test(d, "3fe1 1f88 6196 d07a be94 03ea 693f 7504\n" + + "00b6 a05c b827 2e32 fa98 b46f 769e 86b1\n" + + "9272 b025 da5c 2ea9 fd70 a8de 7fb5 3556\n" + + "5ab7 6ece c057 02e2 2ad2 17bf 6c96 d07a\n" + + "be94 0854 cb6d 4a08 0075 40bd 71b6 6e05\n" + + "a531 68df 0f13 8efe 4522 cd32 21b6 5686\n" + + "eb23 781f cf52 848f d24a 8f0f 0d02 3435\n" + + "5f87 497c a589 d34d 1f", + + "[ 1] (s = 53) content-type: text/html\n" + + "[ 2] (s = 50) accept-ranges: bytes\n" + + "[ 3] (s = 74) last-modified: Mon, 11 Jun 2007 18:53:14 GMT\n" + + "[ 4] (s = 77) server: Apache/2.4.17 (Unix) OpenSSL/1.0.2e-dev\n" + + "[ 5] (s = 65) date: Mon, 09 Nov 2015 16:26:39 GMT\n" + + " Table size: 319", + + ":status: 200\n" + + "date: Mon, 09 Nov 2015 16:26:39 GMT\n" + + "server: Apache/2.4.17 (Unix) OpenSSL/1.0.2e-dev\n" + + "last-modified: Mon, 11 Jun 2007 18:53:14 GMT\n" + + "etag: \"2d-432a5e4a73a80\"\n" + + "accept-ranges: bytes\n" + + "content-length: 45\n" + + "content-type: text/html"); + // @formatter:on + } + + @Test + public void testX1() { + // Supplier of a decoder with a particular state + Supplier s = () -> { + Decoder d = new Decoder(4096); + // @formatter:off + test(d, "88 76 92 ca 54 a7 d7 f4 fa ec af ed 6d da 61 d7 bb 1e ad ff" + + "df 61 97 c3 61 be 94 13 4a 65 b6 a5 04 00 b8 a0 5a b8 db 77" + + "1b 71 4c 5a 37 ff 0f 0d 84 08 00 00 03", + + "[ 1] (s = 65) date: Fri, 24 Jun 2016 14:55:56 GMT\n" + + "[ 2] (s = 59) server: Jetty(9.3.z-SNAPSHOT)\n" + + " Table size: 124", + + ":status: 200\n" + + "server: Jetty(9.3.z-SNAPSHOT)\n" + + "date: Fri, 24 Jun 2016 14:55:56 GMT\n" + + "content-length: 100000" + ); + // @formatter:on + return d; + }; + // For all splits of the following data fed to the supplied decoder we + // must get what's expected + // @formatter:off + testAllSplits(s, + "88 bf be 0f 0d 84 08 00 00 03", + + "[ 1] (s = 65) date: Fri, 24 Jun 2016 14:55:56 GMT\n" + + "[ 2] (s = 59) server: Jetty(9.3.z-SNAPSHOT)\n" + + " Table size: 124", + + ":status: 200\n" + + "server: Jetty(9.3.z-SNAPSHOT)\n" + + "date: Fri, 24 Jun 2016 14:55:56 GMT\n" + + "content-length: 100000"); + // @formatter:on + } + + // + // This test is missing in the spec + // + @Test + public void sizeUpdate() throws IOException { + Decoder d = new Decoder(4096); + assertEquals(d.getTable().maxSize(), 4096); + d.decode(ByteBuffer.wrap(new byte[]{0b00111110}), true, nopCallback()); // newSize = 30 + assertEquals(d.getTable().maxSize(), 30); + } + + @Test + public void incorrectSizeUpdate() { + ByteBuffer b = ByteBuffer.allocate(8); + Encoder e = new Encoder(8192) { + @Override + protected int calculateCapacity(int maxCapacity) { + return maxCapacity; + } + }; + e.header("a", "b"); + e.encode(b); + b.flip(); + { + Decoder d = new Decoder(4096); + assertVoidThrows(IOException.class, + () -> d.decode(b, true, (name, value) -> { })); + } + b.flip(); + { + Decoder d = new Decoder(4096); + assertVoidThrows(IOException.class, + () -> d.decode(b, false, (name, value) -> { })); + } + } + + @Test + public void corruptedHeaderBlockInteger() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + (byte) 0b11111111, // indexed + (byte) 0b10011010 // 25 + ... + }); + IOException e = assertVoidThrows(IOException.class, + () -> d.decode(data, true, nopCallback())); + assertExceptionMessageContains(e, "Unexpected end of header block"); + } + + // 5.1. Integer Representation + // ... + // Integer encodings that exceed implementation limits -- in value or octet + // length -- MUST be treated as decoding errors. Different limits can + // be set for each of the different uses of integers, based on + // implementation constraints. + @Test + public void headerBlockIntegerNoOverflow() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + (byte) 0b11111111, // indexed + 127 + // Integer.MAX_VALUE - 127 (base 128, little-endian): + (byte) 0b10000000, + (byte) 0b11111111, + (byte) 0b11111111, + (byte) 0b11111111, + (byte) 0b00000111 + }); + + IOException e = assertVoidThrows(IOException.class, + () -> d.decode(data, true, nopCallback())); + + assertExceptionMessageContains(e.getCause(), "index=2147483647"); + } + + @Test + public void headerBlockIntegerOverflow() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + (byte) 0b11111111, // indexed + 127 + // Integer.MAX_VALUE - 127 + 1 (base 128, little endian): + (byte) 0b10000001, + (byte) 0b11111111, + (byte) 0b11111111, + (byte) 0b11111111, + (byte) 0b00000111 + }); + + IOException e = assertVoidThrows(IOException.class, + () -> d.decode(data, true, nopCallback())); + + assertExceptionMessageContains(e, "Integer overflow"); + } + + @Test + public void corruptedHeaderBlockString1() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + 0b00001111, // literal, index=15 + 0b00000000, + 0b00001000, // huffman=false, length=8 + 0b00000000, // \ + 0b00000000, // but only 3 octets available... + 0b00000000 // / + }); + IOException e = assertVoidThrows(IOException.class, + () -> d.decode(data, true, nopCallback())); + assertExceptionMessageContains(e, "Unexpected end of header block"); + } + + @Test + public void corruptedHeaderBlockString2() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + 0b00001111, // literal, index=15 + 0b00000000, + (byte) 0b10001000, // huffman=true, length=8 + 0b00000000, // \ + 0b00000000, // \ + 0b00000000, // but only 5 octets available... + 0b00000000, // / + 0b00000000 // / + }); + IOException e = assertVoidThrows(IOException.class, + () -> d.decode(data, true, nopCallback())); + assertExceptionMessageContains(e, "Unexpected end of header block"); + } + + // 5.2. String Literal Representation + // ...A Huffman-encoded string literal containing the EOS symbol MUST be + // treated as a decoding error... + @Test + public void corruptedHeaderBlockHuffmanStringEOS() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + 0b00001111, // literal, index=15 + 0b00000000, + (byte) 0b10000110, // huffman=true, length=6 + 0b00011001, 0b01001101, (byte) 0b11111111, + (byte) 0b11111111, (byte) 0b11111111, (byte) 0b11111100 + }); + IOException e = assertVoidThrows(IOException.class, + () -> d.decode(data, true, nopCallback())); + + assertExceptionMessageContains(e, "Encountered EOS"); + } + + // 5.2. String Literal Representation + // ...A padding strictly longer than 7 bits MUST be treated as a decoding + // error... + @Test + public void corruptedHeaderBlockHuffmanStringLongPadding1() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + 0b00001111, // literal, index=15 + 0b00000000, + (byte) 0b10000011, // huffman=true, length=3 + 0b00011001, 0b01001101, (byte) 0b11111111 + // len("aei") + len(padding) = (5 + 5 + 5) + (9) + }); + IOException e = assertVoidThrows(IOException.class, + () -> d.decode(data, true, nopCallback())); + + assertExceptionMessageContains(e, "Padding is too long", "len=9"); + } + + @Test + public void corruptedHeaderBlockHuffmanStringLongPadding2() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + 0b00001111, // literal, index=15 + 0b00000000, + (byte) 0b10000011, // huffman=true, length=3 + 0b00011001, 0b01111010, (byte) 0b11111111 + // len("aek") + len(padding) = (5 + 5 + 7) + (7) + }); + assertVoidDoesNotThrow(() -> d.decode(data, true, nopCallback())); + } + + // 5.2. String Literal Representation + // ...A padding not corresponding to the most significant bits of the code + // for the EOS symbol MUST be treated as a decoding error... + @Test + public void corruptedHeaderBlockHuffmanStringNotEOSPadding() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + 0b00001111, // literal, index=15 + 0b00000000, + (byte) 0b10000011, // huffman=true, length=3 + 0b00011001, 0b01111010, (byte) 0b11111110 + }); + IOException e = assertVoidThrows(IOException.class, + () -> d.decode(data, true, nopCallback())); + + assertExceptionMessageContains(e, "Not a EOS prefix"); + } + + @Test + public void argsTestBiConsumerIsNull() { + Decoder decoder = new Decoder(4096); + assertVoidThrows(NullPointerException.class, + () -> decoder.decode(ByteBuffer.allocate(16), true, null)); + } + + @Test + public void argsTestByteBufferIsNull() { + Decoder decoder = new Decoder(4096); + assertVoidThrows(NullPointerException.class, + () -> decoder.decode(null, true, nopCallback())); + } + + @Test + public void argsTestBothAreNull() { + Decoder decoder = new Decoder(4096); + assertVoidThrows(NullPointerException.class, + () -> decoder.decode(null, true, null)); + } + + private static void test(String hexdump, + String headerTable, String headerList) { + test(new Decoder(4096), hexdump, headerTable, headerList); + } + + private static void testAllSplits(String hexdump, + String expectedHeaderTable, + String expectedHeaderList) { + testAllSplits(() -> new Decoder(256), hexdump, expectedHeaderTable, expectedHeaderList); + } + + private static void testAllSplits(Supplier supplier, String hexdump, + String expectedHeaderTable, String expectedHeaderList) { + ByteBuffer source = SpecHelper.toBytes(hexdump); + + BuffersTestingKit.forEachSplit(source, iterable -> { + List actual = new LinkedList<>(); + Iterator i = iterable.iterator(); + if (!i.hasNext()) { + return; + } + Decoder d = supplier.get(); + do { + ByteBuffer n = i.next(); + try { + d.decode(n, !i.hasNext(), (name, value) -> { + if (value == null) { + actual.add(name.toString()); + } else { + actual.add(name + ": " + value); + } + }); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } while (i.hasNext()); + assertEquals(d.getTable().getStateString(), expectedHeaderTable); + assertEquals(actual.stream().collect(Collectors.joining("\n")), expectedHeaderList); + }); + } + + // + // Sometimes we need to keep the same decoder along several runs, + // as it models the same connection + // + private static void test(Decoder d, String hexdump, + String expectedHeaderTable, String expectedHeaderList) { + + ByteBuffer source = SpecHelper.toBytes(hexdump); + + List actual = new LinkedList<>(); + try { + d.decode(source, true, (name, value) -> { + if (value == null) { + actual.add(name.toString()); + } else { + actual.add(name + ": " + value); + } + }); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + assertEquals(d.getTable().getStateString(), expectedHeaderTable); + assertEquals(actual.stream().collect(Collectors.joining("\n")), expectedHeaderList); + } + + private static DecodingCallback nopCallback() { + return (t, u) -> { }; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/EncoderTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/EncoderTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,693 @@ +/* + * Copyright (c) 2014, 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.internal.net.http.hpack; + +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +import static jdk.internal.net.http.hpack.BuffersTestingKit.concat; +import static jdk.internal.net.http.hpack.BuffersTestingKit.forEachSplit; +import static jdk.internal.net.http.hpack.SpecHelper.toHexdump; +import static jdk.internal.net.http.hpack.TestHelper.assertVoidThrows; +import static java.util.Arrays.asList; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +// TODO: map textual representation of commands from the spec to actual +// calls to encoder (actually, this is a good idea for decoder as well) +public final class EncoderTest { + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.1 + // + @Test + public void example1() { + + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + e.literalWithIndexing("custom-key", false, "custom-header", false); + // @formatter:off + test(e, + + "400a 6375 7374 6f6d 2d6b 6579 0d63 7573\n" + + "746f 6d2d 6865 6164 6572", + + "[ 1] (s = 55) custom-key: custom-header\n" + + " Table size: 55"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.2 + // + @Test + public void example2() { + + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + e.literal(4, "/sample/path", false); + // @formatter:off + test(e, + + "040c 2f73 616d 706c 652f 7061 7468", + + "empty."); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.3 + // + @Test + public void example3() { + + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + e.literalNeverIndexed("password", false, "secret", false); + // @formatter:off + test(e, + + "1008 7061 7373 776f 7264 0673 6563 7265\n" + + "74", + + "empty."); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.4 + // + @Test + public void example4() { + + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + e.indexed(2); + // @formatter:off + test(e, + + "82", + + "empty."); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.3 + // + @Test + public void example5() { + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + ByteBuffer output = ByteBuffer.allocate(64); + e.indexed(2); + e.encode(output); + e.indexed(6); + e.encode(output); + e.indexed(4); + e.encode(output); + e.literalWithIndexing(1, "www.example.com", false); + e.encode(output); + + output.flip(); + + // @formatter:off + test(e, output, + + "8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" + + "2e63 6f6d", + + "[ 1] (s = 57) :authority: www.example.com\n" + + " Table size: 57"); + + output.clear(); + + e.indexed( 2); + e.encode(output); + e.indexed( 6); + e.encode(output); + e.indexed( 4); + e.encode(output); + e.indexed(62); + e.encode(output); + e.literalWithIndexing(24, "no-cache", false); + e.encode(output); + + output.flip(); + + test(e, output, + + "8286 84be 5808 6e6f 2d63 6163 6865", + + "[ 1] (s = 53) cache-control: no-cache\n" + + "[ 2] (s = 57) :authority: www.example.com\n" + + " Table size: 110"); + + output.clear(); + + e.indexed( 2); + e.encode(output); + e.indexed( 7); + e.encode(output); + e.indexed( 5); + e.encode(output); + e.indexed(63); + e.encode(output); + e.literalWithIndexing("custom-key", false, "custom-value", false); + e.encode(output); + + output.flip(); + + test(e, output, + + "8287 85bf 400a 6375 7374 6f6d 2d6b 6579\n" + + "0c63 7573 746f 6d2d 7661 6c75 65", + + "[ 1] (s = 54) custom-key: custom-value\n" + + "[ 2] (s = 53) cache-control: no-cache\n" + + "[ 3] (s = 57) :authority: www.example.com\n" + + " Table size: 164"); + // @formatter:on + } + + @Test + public void example5AllSplits() { + + List> actions = new LinkedList<>(); + actions.add(e -> e.indexed(2)); + actions.add(e -> e.indexed(6)); + actions.add(e -> e.indexed(4)); + actions.add(e -> e.literalWithIndexing(1, "www.example.com", false)); + + encodeAllSplits( + actions, + + "8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" + + "2e63 6f6d", + + "[ 1] (s = 57) :authority: www.example.com\n" + + " Table size: 57"); + } + + private static void encodeAllSplits(Iterable> consumers, + String expectedHexdump, + String expectedTableState) { + ByteBuffer buffer = SpecHelper.toBytes(expectedHexdump); + erase(buffer); // Zeroed buffer of size needed to hold the encoding + forEachSplit(buffer, iterable -> { + List copy = new LinkedList<>(); + iterable.forEach(b -> copy.add(ByteBuffer.allocate(b.remaining()))); + Iterator output = copy.iterator(); + if (!output.hasNext()) { + throw new IllegalStateException("No buffers to encode to"); + } + Encoder e = newCustomEncoder(256); // FIXME: pull up (as a parameter) + drainInitialUpdate(e); + boolean encoded; + ByteBuffer b = output.next(); + for (Consumer c : consumers) { + c.accept(e); + do { + encoded = e.encode(b); + if (!encoded) { + if (output.hasNext()) { + b = output.next(); + } else { + throw new IllegalStateException("No room for encoding"); + } + } + } + while (!encoded); + } + copy.forEach(Buffer::flip); + ByteBuffer data = concat(copy); + test(e, data, expectedHexdump, expectedTableState); + }); + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.4 + // + @Test + public void example6() { + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + ByteBuffer output = ByteBuffer.allocate(64); + e.indexed(2); + e.encode(output); + e.indexed(6); + e.encode(output); + e.indexed(4); + e.encode(output); + e.literalWithIndexing(1, "www.example.com", true); + e.encode(output); + + output.flip(); + + // @formatter:off + test(e, output, + + "8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4\n" + + "ff", + + "[ 1] (s = 57) :authority: www.example.com\n" + + " Table size: 57"); + + output.clear(); + + e.indexed( 2); + e.encode(output); + e.indexed( 6); + e.encode(output); + e.indexed( 4); + e.encode(output); + e.indexed(62); + e.encode(output); + e.literalWithIndexing(24, "no-cache", true); + e.encode(output); + + output.flip(); + + test(e, output, + + "8286 84be 5886 a8eb 1064 9cbf", + + "[ 1] (s = 53) cache-control: no-cache\n" + + "[ 2] (s = 57) :authority: www.example.com\n" + + " Table size: 110"); + + output.clear(); + + e.indexed( 2); + e.encode(output); + e.indexed( 7); + e.encode(output); + e.indexed( 5); + e.encode(output); + e.indexed(63); + e.encode(output); + e.literalWithIndexing("custom-key", true, "custom-value", true); + e.encode(output); + + output.flip(); + + test(e, output, + + "8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925\n" + + "a849 e95b b8e8 b4bf", + + "[ 1] (s = 54) custom-key: custom-value\n" + + "[ 2] (s = 53) cache-control: no-cache\n" + + "[ 3] (s = 57) :authority: www.example.com\n" + + " Table size: 164"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.5 + // + @Test + public void example7() { + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + ByteBuffer output = ByteBuffer.allocate(128); + // @formatter:off + e.literalWithIndexing( 8, "302", false); + e.encode(output); + e.literalWithIndexing(24, "private", false); + e.encode(output); + e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:21 GMT", false); + e.encode(output); + e.literalWithIndexing(46, "https://www.example.com", false); + e.encode(output); + + output.flip(); + + test(e, output, + + "4803 3330 3258 0770 7269 7661 7465 611d\n" + + "4d6f 6e2c 2032 3120 4f63 7420 3230 3133\n" + + "2032 303a 3133 3a32 3120 474d 546e 1768\n" + + "7474 7073 3a2f 2f77 7777 2e65 7861 6d70\n" + + "6c65 2e63 6f6d", + + "[ 1] (s = 63) location: https://www.example.com\n" + + "[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 3] (s = 52) cache-control: private\n" + + "[ 4] (s = 42) :status: 302\n" + + " Table size: 222"); + + output.clear(); + + e.literalWithIndexing( 8, "307", false); + e.encode(output); + e.indexed(65); + e.encode(output); + e.indexed(64); + e.encode(output); + e.indexed(63); + e.encode(output); + + output.flip(); + + test(e, output, + + "4803 3330 37c1 c0bf", + + "[ 1] (s = 42) :status: 307\n" + + "[ 2] (s = 63) location: https://www.example.com\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 4] (s = 52) cache-control: private\n" + + " Table size: 222"); + + output.clear(); + + e.indexed( 8); + e.encode(output); + e.indexed(65); + e.encode(output); + e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:22 GMT", false); + e.encode(output); + e.indexed(64); + e.encode(output); + e.literalWithIndexing(26, "gzip", false); + e.encode(output); + e.literalWithIndexing(55, "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", false); + e.encode(output); + + output.flip(); + + test(e, output, + + "88c1 611d 4d6f 6e2c 2032 3120 4f63 7420\n" + + "3230 3133 2032 303a 3133 3a32 3220 474d\n" + + "54c0 5a04 677a 6970 7738 666f 6f3d 4153\n" + + "444a 4b48 514b 425a 584f 5157 454f 5049\n" + + "5541 5851 5745 4f49 553b 206d 6178 2d61\n" + + "6765 3d33 3630 303b 2076 6572 7369 6f6e\n" + + "3d31", + + "[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" + + "[ 2] (s = 52) content-encoding: gzip\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" + + " Table size: 215"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.6 + // + @Test + public void example8() { + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + ByteBuffer output = ByteBuffer.allocate(128); + // @formatter:off + e.literalWithIndexing( 8, "302", true); + e.encode(output); + e.literalWithIndexing(24, "private", true); + e.encode(output); + e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:21 GMT", true); + e.encode(output); + e.literalWithIndexing(46, "https://www.example.com", true); + e.encode(output); + + output.flip(); + + test(e, output, + + "4882 6402 5885 aec3 771a 4b61 96d0 7abe\n" + + "9410 54d4 44a8 2005 9504 0b81 66e0 82a6\n" + + "2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8\n" + + "e9ae 82ae 43d3", + + "[ 1] (s = 63) location: https://www.example.com\n" + + "[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 3] (s = 52) cache-control: private\n" + + "[ 4] (s = 42) :status: 302\n" + + " Table size: 222"); + + output.clear(); + + e.literalWithIndexing( 8, "307", true); + e.encode(output); + e.indexed(65); + e.encode(output); + e.indexed(64); + e.encode(output); + e.indexed(63); + e.encode(output); + + output.flip(); + + test(e, output, + + "4883 640e ffc1 c0bf", + + "[ 1] (s = 42) :status: 307\n" + + "[ 2] (s = 63) location: https://www.example.com\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 4] (s = 52) cache-control: private\n" + + " Table size: 222"); + + output.clear(); + + e.indexed( 8); + e.encode(output); + e.indexed(65); + e.encode(output); + e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:22 GMT", true); + e.encode(output); + e.indexed(64); + e.encode(output); + e.literalWithIndexing(26, "gzip", true); + e.encode(output); + e.literalWithIndexing(55, "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", true); + e.encode(output); + + output.flip(); + + test(e, output, + + "88c1 6196 d07a be94 1054 d444 a820 0595\n" + + "040b 8166 e084 a62d 1bff c05a 839b d9ab\n" + + "77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b\n" + + "3960 d5af 2708 7f36 72c1 ab27 0fb5 291f\n" + + "9587 3160 65c0 03ed 4ee5 b106 3d50 07", + + "[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" + + "[ 2] (s = 52) content-encoding: gzip\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" + + " Table size: 215"); + // @formatter:on + } + + @Test + public void initialSizeUpdateDefaultEncoder() throws IOException { + Function e = Encoder::new; + testSizeUpdate(e, 1024, asList(), asList(0)); + testSizeUpdate(e, 1024, asList(1024), asList(0)); + testSizeUpdate(e, 1024, asList(1024, 1024), asList(0)); + testSizeUpdate(e, 1024, asList(1024, 512), asList(0)); + testSizeUpdate(e, 1024, asList(512, 1024), asList(0)); + testSizeUpdate(e, 1024, asList(512, 2048), asList(0)); + } + + @Test + public void initialSizeUpdateCustomEncoder() throws IOException { + Function e = EncoderTest::newCustomEncoder; + testSizeUpdate(e, 1024, asList(), asList(1024)); + testSizeUpdate(e, 1024, asList(1024), asList(1024)); + testSizeUpdate(e, 1024, asList(1024, 1024), asList(1024)); + testSizeUpdate(e, 1024, asList(1024, 512), asList(512)); + testSizeUpdate(e, 1024, asList(512, 1024), asList(1024)); + testSizeUpdate(e, 1024, asList(512, 2048), asList(2048)); + } + + @Test + public void seriesOfSizeUpdatesDefaultEncoder() throws IOException { + Function e = c -> { + Encoder encoder = new Encoder(c); + drainInitialUpdate(encoder); + return encoder; + }; + testSizeUpdate(e, 0, asList(0), asList()); + testSizeUpdate(e, 1024, asList(1024), asList()); + testSizeUpdate(e, 1024, asList(2048), asList()); + testSizeUpdate(e, 1024, asList(512), asList()); + testSizeUpdate(e, 1024, asList(1024, 1024), asList()); + testSizeUpdate(e, 1024, asList(1024, 2048), asList()); + testSizeUpdate(e, 1024, asList(2048, 1024), asList()); + testSizeUpdate(e, 1024, asList(1024, 512), asList()); + testSizeUpdate(e, 1024, asList(512, 1024), asList()); + } + + // + // https://tools.ietf.org/html/rfc7541#section-4.2 + // + @Test + public void seriesOfSizeUpdatesCustomEncoder() throws IOException { + Function e = c -> { + Encoder encoder = newCustomEncoder(c); + drainInitialUpdate(encoder); + return encoder; + }; + testSizeUpdate(e, 0, asList(0), asList()); + testSizeUpdate(e, 1024, asList(1024), asList()); + testSizeUpdate(e, 1024, asList(2048), asList(2048)); + testSizeUpdate(e, 1024, asList(512), asList(512)); + testSizeUpdate(e, 1024, asList(1024, 1024), asList()); + testSizeUpdate(e, 1024, asList(1024, 2048), asList(2048)); + testSizeUpdate(e, 1024, asList(2048, 1024), asList()); + testSizeUpdate(e, 1024, asList(1024, 512), asList(512)); + testSizeUpdate(e, 1024, asList(512, 1024), asList(512, 1024)); + } + + @Test + public void callSequenceViolations() { + { // Hasn't set up a header + Encoder e = new Encoder(0); + assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16))); + } + { // Can't set up header while there's an unfinished encoding + Encoder e = new Encoder(0); + e.indexed(32); + assertVoidThrows(IllegalStateException.class, () -> e.indexed(32)); + } + { // Can't setMaxCapacity while there's an unfinished encoding + Encoder e = new Encoder(0); + e.indexed(32); + assertVoidThrows(IllegalStateException.class, () -> e.setMaxCapacity(512)); + } + { // Hasn't set up a header + Encoder e = new Encoder(0); + e.setMaxCapacity(256); + assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16))); + } + { // Hasn't set up a header after the previous encoding + Encoder e = new Encoder(0); + e.indexed(0); + boolean encoded = e.encode(ByteBuffer.allocate(16)); + assertTrue(encoded); // assumption + assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16))); + } + } + + private static void test(Encoder encoder, + String expectedTableState, + String expectedHexdump) { + + ByteBuffer b = ByteBuffer.allocate(128); + encoder.encode(b); + b.flip(); + test(encoder, b, expectedTableState, expectedHexdump); + } + + private static void test(Encoder encoder, + ByteBuffer output, + String expectedHexdump, + String expectedTableState) { + + String actualTableState = encoder.getHeaderTable().getStateString(); + assertEquals(actualTableState, expectedTableState); + + String actualHexdump = toHexdump(output); + assertEquals(actualHexdump, expectedHexdump.replaceAll("\\n", " ")); + } + + // initial size - the size encoder is constructed with + // updates - a sequence of values for consecutive calls to encoder.setMaxCapacity + // expected - a sequence of values expected to be decoded by a decoder + private void testSizeUpdate(Function encoder, + int initialSize, + List updates, + List expected) throws IOException { + Encoder e = encoder.apply(initialSize); + updates.forEach(e::setMaxCapacity); + ByteBuffer b = ByteBuffer.allocate(64); + e.header("a", "b"); + e.encode(b); + b.flip(); + Decoder d = new Decoder(updates.isEmpty() ? initialSize : Collections.max(updates)); + List actual = new ArrayList<>(); + d.decode(b, true, new DecodingCallback() { + @Override + public void onDecoded(CharSequence name, CharSequence value) { } + + @Override + public void onSizeUpdate(int capacity) { + actual.add(capacity); + } + }); + assertEquals(actual, expected); + } + + // + // Default encoder does not need any table, therefore a subclass that + // behaves differently is needed + // + private static Encoder newCustomEncoder(int maxCapacity) { + return new Encoder(maxCapacity) { + @Override + protected int calculateCapacity(int maxCapacity) { + return maxCapacity; + } + }; + } + + private static void drainInitialUpdate(Encoder e) { + ByteBuffer b = ByteBuffer.allocate(4); + e.header("a", "b"); + boolean done; + do { + done = e.encode(b); + b.flip(); + } while (!done); + } + + private static void erase(ByteBuffer buffer) { + buffer.clear(); + while (buffer.hasRemaining()) { + buffer.put((byte) 0); + } + buffer.clear(); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/HeaderTableTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/HeaderTableTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,409 @@ +/* + * Copyright (c) 2014, 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.internal.net.http.hpack; + +import org.testng.annotations.Test; +import jdk.internal.net.http.hpack.HeaderTable.HeaderField; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.lang.String.format; +import static org.testng.Assert.assertEquals; +import static jdk.internal.net.http.hpack.TestHelper.assertExceptionMessageContains; +import static jdk.internal.net.http.hpack.TestHelper.assertThrows; +import static jdk.internal.net.http.hpack.TestHelper.assertVoidThrows; +import static jdk.internal.net.http.hpack.TestHelper.newRandom; + +public class HeaderTableTest { + + // + // https://tools.ietf.org/html/rfc7541#appendix-A + // + // @formatter:off + private static final String SPEC = + " | 1 | :authority | |\n" + + " | 2 | :method | GET |\n" + + " | 3 | :method | POST |\n" + + " | 4 | :path | / |\n" + + " | 5 | :path | /index.html |\n" + + " | 6 | :scheme | http |\n" + + " | 7 | :scheme | https |\n" + + " | 8 | :status | 200 |\n" + + " | 9 | :status | 204 |\n" + + " | 10 | :status | 206 |\n" + + " | 11 | :status | 304 |\n" + + " | 12 | :status | 400 |\n" + + " | 13 | :status | 404 |\n" + + " | 14 | :status | 500 |\n" + + " | 15 | accept-charset | |\n" + + " | 16 | accept-encoding | gzip, deflate |\n" + + " | 17 | accept-language | |\n" + + " | 18 | accept-ranges | |\n" + + " | 19 | accept | |\n" + + " | 20 | access-control-allow-origin | |\n" + + " | 21 | age | |\n" + + " | 22 | allow | |\n" + + " | 23 | authorization | |\n" + + " | 24 | cache-control | |\n" + + " | 25 | content-disposition | |\n" + + " | 26 | content-encoding | |\n" + + " | 27 | content-language | |\n" + + " | 28 | content-length | |\n" + + " | 29 | content-location | |\n" + + " | 30 | content-range | |\n" + + " | 31 | content-type | |\n" + + " | 32 | cookie | |\n" + + " | 33 | date | |\n" + + " | 34 | etag | |\n" + + " | 35 | expect | |\n" + + " | 36 | expires | |\n" + + " | 37 | from | |\n" + + " | 38 | host | |\n" + + " | 39 | if-match | |\n" + + " | 40 | if-modified-since | |\n" + + " | 41 | if-none-match | |\n" + + " | 42 | if-range | |\n" + + " | 43 | if-unmodified-since | |\n" + + " | 44 | last-modified | |\n" + + " | 45 | link | |\n" + + " | 46 | location | |\n" + + " | 47 | max-forwards | |\n" + + " | 48 | proxy-authenticate | |\n" + + " | 49 | proxy-authorization | |\n" + + " | 50 | range | |\n" + + " | 51 | referer | |\n" + + " | 52 | refresh | |\n" + + " | 53 | retry-after | |\n" + + " | 54 | server | |\n" + + " | 55 | set-cookie | |\n" + + " | 56 | strict-transport-security | |\n" + + " | 57 | transfer-encoding | |\n" + + " | 58 | user-agent | |\n" + + " | 59 | vary | |\n" + + " | 60 | via | |\n" + + " | 61 | www-authenticate | |\n"; + // @formatter:on + + private static final int STATIC_TABLE_LENGTH = createStaticEntries().size(); + private final Random rnd = newRandom(); + + @Test + public void staticData() { + HeaderTable table = new HeaderTable(0, HPACK.getLogger()); + Map staticHeaderFields = createStaticEntries(); + + Map minimalIndexes = new HashMap<>(); + + for (Map.Entry e : staticHeaderFields.entrySet()) { + Integer idx = e.getKey(); + String hName = e.getValue().name; + Integer midx = minimalIndexes.get(hName); + if (midx == null) { + minimalIndexes.put(hName, idx); + } else { + minimalIndexes.put(hName, Math.min(idx, midx)); + } + } + + staticHeaderFields.entrySet().forEach( + e -> { + // lookup + HeaderField actualHeaderField = table.get(e.getKey()); + HeaderField expectedHeaderField = e.getValue(); + assertEquals(actualHeaderField, expectedHeaderField); + + // reverse lookup (name, value) + String hName = expectedHeaderField.name; + String hValue = expectedHeaderField.value; + int expectedIndex = e.getKey(); + int actualIndex = table.indexOf(hName, hValue); + + assertEquals(actualIndex, expectedIndex); + + // reverse lookup (name) + int expectedMinimalIndex = minimalIndexes.get(hName); + int actualMinimalIndex = table.indexOf(hName, "blah-blah"); + + assertEquals(-actualMinimalIndex, expectedMinimalIndex); + } + ); + } + + @Test + public void constructorSetsMaxSize() { + int size = rnd.nextInt(64); + HeaderTable t = new HeaderTable(size, HPACK.getLogger()); + assertEquals(t.size(), 0); + assertEquals(t.maxSize(), size); + } + + @Test + public void negativeMaximumSize() { + int maxSize = -(rnd.nextInt(100) + 1); // [-100, -1] + IllegalArgumentException e = + assertVoidThrows(IllegalArgumentException.class, + () -> new HeaderTable(0, HPACK.getLogger()).setMaxSize(maxSize)); + assertExceptionMessageContains(e, "maxSize"); + } + + @Test + public void zeroMaximumSize() { + HeaderTable table = new HeaderTable(0, HPACK.getLogger()); + table.setMaxSize(0); + assertEquals(table.maxSize(), 0); + } + + @Test + public void negativeIndex() { + int idx = -(rnd.nextInt(256) + 1); // [-256, -1] + IndexOutOfBoundsException e = + assertVoidThrows(IndexOutOfBoundsException.class, + () -> new HeaderTable(0, HPACK.getLogger()).get(idx)); + assertExceptionMessageContains(e, "index"); + } + + @Test + public void zeroIndex() { + IndexOutOfBoundsException e = + assertThrows(IndexOutOfBoundsException.class, + () -> new HeaderTable(0, HPACK.getLogger()).get(0)); + assertExceptionMessageContains(e, "index"); + } + + @Test + public void length() { + HeaderTable table = new HeaderTable(0, HPACK.getLogger()); + assertEquals(table.length(), STATIC_TABLE_LENGTH); + } + + @Test + public void indexOutsideStaticRange() { + HeaderTable table = new HeaderTable(0, HPACK.getLogger()); + int idx = table.length() + (rnd.nextInt(256) + 1); + IndexOutOfBoundsException e = + assertThrows(IndexOutOfBoundsException.class, + () -> table.get(idx)); + assertExceptionMessageContains(e, "index"); + } + + @Test + public void entryPutAfterStaticArea() { + HeaderTable table = new HeaderTable(256, HPACK.getLogger()); + int idx = table.length() + 1; + assertThrows(IndexOutOfBoundsException.class, () -> table.get(idx)); + + byte[] bytes = new byte[32]; + rnd.nextBytes(bytes); + String name = new String(bytes, StandardCharsets.ISO_8859_1); + String value = "custom-value"; + + table.put(name, value); + HeaderField f = table.get(idx); + assertEquals(name, f.name); + assertEquals(value, f.value); + } + + @Test + public void staticTableHasZeroSize() { + HeaderTable table = new HeaderTable(0, HPACK.getLogger()); + assertEquals(0, table.size()); + } + + @Test + public void lowerIndexPriority() { + HeaderTable table = new HeaderTable(256, HPACK.getLogger()); + int oldLength = table.length(); + table.put("bender", "rodriguez"); + table.put("bender", "rodriguez"); + table.put("bender", "rodriguez"); + + assertEquals(table.length(), oldLength + 3); // more like an assumption + int i = table.indexOf("bender", "rodriguez"); + assertEquals(oldLength + 1, i); + } + + @Test + public void lowerIndexPriority2() { + HeaderTable table = new HeaderTable(256, HPACK.getLogger()); + int oldLength = table.length(); + int idx = rnd.nextInt(oldLength) + 1; + HeaderField f = table.get(idx); + table.put(f.name, f.value); + assertEquals(table.length(), oldLength + 1); + int i = table.indexOf(f.name, f.value); + assertEquals(idx, i); + } + + // TODO: negative indexes check + // TODO: ensure full table clearance when adding huge header field + // TODO: ensure eviction deletes minimum needed entries, not more + + @Test + public void fifo() { + // Let's add a series of header fields + int NUM_HEADERS = 32; + HeaderTable t = new HeaderTable((32 + 4) * NUM_HEADERS, HPACK.getLogger()); + // ^ ^ + // entry overhead symbols per entry (max 2x2 digits) + for (int i = 1; i <= NUM_HEADERS; i++) { + String s = String.valueOf(i); + t.put(s, s); + } + // They MUST appear in a FIFO order: + // newer entries are at lower indexes + // older entries are at higher indexes + for (int j = 1; j <= NUM_HEADERS; j++) { + HeaderField f = t.get(STATIC_TABLE_LENGTH + j); + int actualName = Integer.parseInt(f.name); + int expectedName = NUM_HEADERS - j + 1; + assertEquals(expectedName, actualName); + } + // Entries MUST be evicted in the order they were added: + // the newer the entry the later it is evicted + for (int k = 1; k <= NUM_HEADERS; k++) { + HeaderField f = t.evictEntry(); + assertEquals(String.valueOf(k), f.name); + } + } + + @Test + public void indexOf() { + // Let's put a series of header fields + int NUM_HEADERS = 32; + HeaderTable t = new HeaderTable((32 + 4) * NUM_HEADERS, HPACK.getLogger()); + // ^ ^ + // entry overhead symbols per entry (max 2x2 digits) + for (int i = 1; i <= NUM_HEADERS; i++) { + String s = String.valueOf(i); + t.put(s, s); + } + // and verify indexOf (reverse lookup) returns correct indexes for + // full lookup + for (int j = 1; j <= NUM_HEADERS; j++) { + String s = String.valueOf(j); + int actualIndex = t.indexOf(s, s); + int expectedIndex = STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1; + assertEquals(expectedIndex, actualIndex); + } + // as well as for just a name lookup + for (int j = 1; j <= NUM_HEADERS; j++) { + String s = String.valueOf(j); + int actualIndex = t.indexOf(s, "blah"); + int expectedIndex = -(STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1); + assertEquals(expectedIndex, actualIndex); + } + // lookup for non-existent name returns 0 + assertEquals(0, t.indexOf("chupacabra", "1")); + } + + @Test + public void testToString() { + testToString0(); + } + + @Test + public void testToStringDifferentLocale() { + Locale locale = Locale.getDefault(); + Locale.setDefault(Locale.FRENCH); + try { + String s = format("%.1f", 3.1); + assertEquals("3,1", s); // assumption of the test, otherwise the test is useless + testToString0(); + } finally { + Locale.setDefault(locale); + } + } + + private void testToString0() { + HeaderTable table = new HeaderTable(0, HPACK.getLogger()); + { + int maxSize = 2048; + table.setMaxSize(maxSize); + String expected = format( + "dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)", + 0, STATIC_TABLE_LENGTH, 0, maxSize, 0.0); + assertEquals(expected, table.toString()); + } + + { + String name = "custom-name"; + String value = "custom-value"; + int size = 512; + + table.setMaxSize(size); + table.put(name, value); + String s = table.toString(); + + int used = name.length() + value.length() + 32; + double ratio = used * 100.0 / size; + + String expected = format( + "dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)", + 1, STATIC_TABLE_LENGTH + 1, used, size, ratio); + assertEquals(expected, s); + } + + { + table.setMaxSize(78); + table.put(":method", ""); + table.put(":status", ""); + String s = table.toString(); + String expected = + format("dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)", + 2, STATIC_TABLE_LENGTH + 2, 78, 78, 100.0); + assertEquals(expected, s); + } + } + + @Test + public void stateString() { + HeaderTable table = new HeaderTable(256, HPACK.getLogger()); + table.put("custom-key", "custom-header"); + // @formatter:off + assertEquals("[ 1] (s = 55) custom-key: custom-header\n" + + " Table size: 55", table.getStateString()); + // @formatter:on + } + + private static Map createStaticEntries() { + Pattern line = Pattern.compile( + "\\|\\s*(?\\d+?)\\s*\\|\\s*(?.+?)\\s*\\|\\s*(?.*?)\\s*\\|"); + Matcher m = line.matcher(SPEC); + Map result = new HashMap<>(); + while (m.find()) { + int index = Integer.parseInt(m.group("index")); + String name = m.group("name"); + String value = m.group("value"); + HeaderField f = new HeaderField(name, value); + result.put(index, f); + } + return Collections.unmodifiableMap(result); // lol + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/HuffmanTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/HuffmanTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,629 @@ +/* + * 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.internal.net.http.hpack; + +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.util.Stack; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.lang.Integer.parseInt; +import static org.testng.Assert.*; + +public final class HuffmanTest { + + // + // https://tools.ietf.org/html/rfc7541#appendix-B + // + private static final String SPEC = + // @formatter:off + " code as bits as hex len\n" + + " sym aligned to MSB aligned in\n" + + " to LSB bits\n" + + " ( 0) |11111111|11000 1ff8 [13]\n" + + " ( 1) |11111111|11111111|1011000 7fffd8 [23]\n" + + " ( 2) |11111111|11111111|11111110|0010 fffffe2 [28]\n" + + " ( 3) |11111111|11111111|11111110|0011 fffffe3 [28]\n" + + " ( 4) |11111111|11111111|11111110|0100 fffffe4 [28]\n" + + " ( 5) |11111111|11111111|11111110|0101 fffffe5 [28]\n" + + " ( 6) |11111111|11111111|11111110|0110 fffffe6 [28]\n" + + " ( 7) |11111111|11111111|11111110|0111 fffffe7 [28]\n" + + " ( 8) |11111111|11111111|11111110|1000 fffffe8 [28]\n" + + " ( 9) |11111111|11111111|11101010 ffffea [24]\n" + + " ( 10) |11111111|11111111|11111111|111100 3ffffffc [30]\n" + + " ( 11) |11111111|11111111|11111110|1001 fffffe9 [28]\n" + + " ( 12) |11111111|11111111|11111110|1010 fffffea [28]\n" + + " ( 13) |11111111|11111111|11111111|111101 3ffffffd [30]\n" + + " ( 14) |11111111|11111111|11111110|1011 fffffeb [28]\n" + + " ( 15) |11111111|11111111|11111110|1100 fffffec [28]\n" + + " ( 16) |11111111|11111111|11111110|1101 fffffed [28]\n" + + " ( 17) |11111111|11111111|11111110|1110 fffffee [28]\n" + + " ( 18) |11111111|11111111|11111110|1111 fffffef [28]\n" + + " ( 19) |11111111|11111111|11111111|0000 ffffff0 [28]\n" + + " ( 20) |11111111|11111111|11111111|0001 ffffff1 [28]\n" + + " ( 21) |11111111|11111111|11111111|0010 ffffff2 [28]\n" + + " ( 22) |11111111|11111111|11111111|111110 3ffffffe [30]\n" + + " ( 23) |11111111|11111111|11111111|0011 ffffff3 [28]\n" + + " ( 24) |11111111|11111111|11111111|0100 ffffff4 [28]\n" + + " ( 25) |11111111|11111111|11111111|0101 ffffff5 [28]\n" + + " ( 26) |11111111|11111111|11111111|0110 ffffff6 [28]\n" + + " ( 27) |11111111|11111111|11111111|0111 ffffff7 [28]\n" + + " ( 28) |11111111|11111111|11111111|1000 ffffff8 [28]\n" + + " ( 29) |11111111|11111111|11111111|1001 ffffff9 [28]\n" + + " ( 30) |11111111|11111111|11111111|1010 ffffffa [28]\n" + + " ( 31) |11111111|11111111|11111111|1011 ffffffb [28]\n" + + " ' ' ( 32) |010100 14 [ 6]\n" + + " '!' ( 33) |11111110|00 3f8 [10]\n" + + " '\"' ( 34) |11111110|01 3f9 [10]\n" + + " '#' ( 35) |11111111|1010 ffa [12]\n" + + " '$' ( 36) |11111111|11001 1ff9 [13]\n" + + " '%' ( 37) |010101 15 [ 6]\n" + + " '&' ( 38) |11111000 f8 [ 8]\n" + + " ''' ( 39) |11111111|010 7fa [11]\n" + + " '(' ( 40) |11111110|10 3fa [10]\n" + + " ')' ( 41) |11111110|11 3fb [10]\n" + + " '*' ( 42) |11111001 f9 [ 8]\n" + + " '+' ( 43) |11111111|011 7fb [11]\n" + + " ',' ( 44) |11111010 fa [ 8]\n" + + " '-' ( 45) |010110 16 [ 6]\n" + + " '.' ( 46) |010111 17 [ 6]\n" + + " '/' ( 47) |011000 18 [ 6]\n" + + " '0' ( 48) |00000 0 [ 5]\n" + + " '1' ( 49) |00001 1 [ 5]\n" + + " '2' ( 50) |00010 2 [ 5]\n" + + " '3' ( 51) |011001 19 [ 6]\n" + + " '4' ( 52) |011010 1a [ 6]\n" + + " '5' ( 53) |011011 1b [ 6]\n" + + " '6' ( 54) |011100 1c [ 6]\n" + + " '7' ( 55) |011101 1d [ 6]\n" + + " '8' ( 56) |011110 1e [ 6]\n" + + " '9' ( 57) |011111 1f [ 6]\n" + + " ':' ( 58) |1011100 5c [ 7]\n" + + " ';' ( 59) |11111011 fb [ 8]\n" + + " '<' ( 60) |11111111|1111100 7ffc [15]\n" + + " '=' ( 61) |100000 20 [ 6]\n" + + " '>' ( 62) |11111111|1011 ffb [12]\n" + + " '?' ( 63) |11111111|00 3fc [10]\n" + + " '@' ( 64) |11111111|11010 1ffa [13]\n" + + " 'A' ( 65) |100001 21 [ 6]\n" + + " 'B' ( 66) |1011101 5d [ 7]\n" + + " 'C' ( 67) |1011110 5e [ 7]\n" + + " 'D' ( 68) |1011111 5f [ 7]\n" + + " 'E' ( 69) |1100000 60 [ 7]\n" + + " 'F' ( 70) |1100001 61 [ 7]\n" + + " 'G' ( 71) |1100010 62 [ 7]\n" + + " 'H' ( 72) |1100011 63 [ 7]\n" + + " 'I' ( 73) |1100100 64 [ 7]\n" + + " 'J' ( 74) |1100101 65 [ 7]\n" + + " 'K' ( 75) |1100110 66 [ 7]\n" + + " 'L' ( 76) |1100111 67 [ 7]\n" + + " 'M' ( 77) |1101000 68 [ 7]\n" + + " 'N' ( 78) |1101001 69 [ 7]\n" + + " 'O' ( 79) |1101010 6a [ 7]\n" + + " 'P' ( 80) |1101011 6b [ 7]\n" + + " 'Q' ( 81) |1101100 6c [ 7]\n" + + " 'R' ( 82) |1101101 6d [ 7]\n" + + " 'S' ( 83) |1101110 6e [ 7]\n" + + " 'T' ( 84) |1101111 6f [ 7]\n" + + " 'U' ( 85) |1110000 70 [ 7]\n" + + " 'V' ( 86) |1110001 71 [ 7]\n" + + " 'W' ( 87) |1110010 72 [ 7]\n" + + " 'X' ( 88) |11111100 fc [ 8]\n" + + " 'Y' ( 89) |1110011 73 [ 7]\n" + + " 'Z' ( 90) |11111101 fd [ 8]\n" + + " '[' ( 91) |11111111|11011 1ffb [13]\n" + + " '\\' ( 92) |11111111|11111110|000 7fff0 [19]\n" + + " ']' ( 93) |11111111|11100 1ffc [13]\n" + + " '^' ( 94) |11111111|111100 3ffc [14]\n" + + " '_' ( 95) |100010 22 [ 6]\n" + + " '`' ( 96) |11111111|1111101 7ffd [15]\n" + + " 'a' ( 97) |00011 3 [ 5]\n" + + " 'b' ( 98) |100011 23 [ 6]\n" + + " 'c' ( 99) |00100 4 [ 5]\n" + + " 'd' (100) |100100 24 [ 6]\n" + + " 'e' (101) |00101 5 [ 5]\n" + + " 'f' (102) |100101 25 [ 6]\n" + + " 'g' (103) |100110 26 [ 6]\n" + + " 'h' (104) |100111 27 [ 6]\n" + + " 'i' (105) |00110 6 [ 5]\n" + + " 'j' (106) |1110100 74 [ 7]\n" + + " 'k' (107) |1110101 75 [ 7]\n" + + " 'l' (108) |101000 28 [ 6]\n" + + " 'm' (109) |101001 29 [ 6]\n" + + " 'n' (110) |101010 2a [ 6]\n" + + " 'o' (111) |00111 7 [ 5]\n" + + " 'p' (112) |101011 2b [ 6]\n" + + " 'q' (113) |1110110 76 [ 7]\n" + + " 'r' (114) |101100 2c [ 6]\n" + + " 's' (115) |01000 8 [ 5]\n" + + " 't' (116) |01001 9 [ 5]\n" + + " 'u' (117) |101101 2d [ 6]\n" + + " 'v' (118) |1110111 77 [ 7]\n" + + " 'w' (119) |1111000 78 [ 7]\n" + + " 'x' (120) |1111001 79 [ 7]\n" + + " 'y' (121) |1111010 7a [ 7]\n" + + " 'z' (122) |1111011 7b [ 7]\n" + + " '{' (123) |11111111|1111110 7ffe [15]\n" + + " '|' (124) |11111111|100 7fc [11]\n" + + " '}' (125) |11111111|111101 3ffd [14]\n" + + " '~' (126) |11111111|11101 1ffd [13]\n" + + " (127) |11111111|11111111|11111111|1100 ffffffc [28]\n" + + " (128) |11111111|11111110|0110 fffe6 [20]\n" + + " (129) |11111111|11111111|010010 3fffd2 [22]\n" + + " (130) |11111111|11111110|0111 fffe7 [20]\n" + + " (131) |11111111|11111110|1000 fffe8 [20]\n" + + " (132) |11111111|11111111|010011 3fffd3 [22]\n" + + " (133) |11111111|11111111|010100 3fffd4 [22]\n" + + " (134) |11111111|11111111|010101 3fffd5 [22]\n" + + " (135) |11111111|11111111|1011001 7fffd9 [23]\n" + + " (136) |11111111|11111111|010110 3fffd6 [22]\n" + + " (137) |11111111|11111111|1011010 7fffda [23]\n" + + " (138) |11111111|11111111|1011011 7fffdb [23]\n" + + " (139) |11111111|11111111|1011100 7fffdc [23]\n" + + " (140) |11111111|11111111|1011101 7fffdd [23]\n" + + " (141) |11111111|11111111|1011110 7fffde [23]\n" + + " (142) |11111111|11111111|11101011 ffffeb [24]\n" + + " (143) |11111111|11111111|1011111 7fffdf [23]\n" + + " (144) |11111111|11111111|11101100 ffffec [24]\n" + + " (145) |11111111|11111111|11101101 ffffed [24]\n" + + " (146) |11111111|11111111|010111 3fffd7 [22]\n" + + " (147) |11111111|11111111|1100000 7fffe0 [23]\n" + + " (148) |11111111|11111111|11101110 ffffee [24]\n" + + " (149) |11111111|11111111|1100001 7fffe1 [23]\n" + + " (150) |11111111|11111111|1100010 7fffe2 [23]\n" + + " (151) |11111111|11111111|1100011 7fffe3 [23]\n" + + " (152) |11111111|11111111|1100100 7fffe4 [23]\n" + + " (153) |11111111|11111110|11100 1fffdc [21]\n" + + " (154) |11111111|11111111|011000 3fffd8 [22]\n" + + " (155) |11111111|11111111|1100101 7fffe5 [23]\n" + + " (156) |11111111|11111111|011001 3fffd9 [22]\n" + + " (157) |11111111|11111111|1100110 7fffe6 [23]\n" + + " (158) |11111111|11111111|1100111 7fffe7 [23]\n" + + " (159) |11111111|11111111|11101111 ffffef [24]\n" + + " (160) |11111111|11111111|011010 3fffda [22]\n" + + " (161) |11111111|11111110|11101 1fffdd [21]\n" + + " (162) |11111111|11111110|1001 fffe9 [20]\n" + + " (163) |11111111|11111111|011011 3fffdb [22]\n" + + " (164) |11111111|11111111|011100 3fffdc [22]\n" + + " (165) |11111111|11111111|1101000 7fffe8 [23]\n" + + " (166) |11111111|11111111|1101001 7fffe9 [23]\n" + + " (167) |11111111|11111110|11110 1fffde [21]\n" + + " (168) |11111111|11111111|1101010 7fffea [23]\n" + + " (169) |11111111|11111111|011101 3fffdd [22]\n" + + " (170) |11111111|11111111|011110 3fffde [22]\n" + + " (171) |11111111|11111111|11110000 fffff0 [24]\n" + + " (172) |11111111|11111110|11111 1fffdf [21]\n" + + " (173) |11111111|11111111|011111 3fffdf [22]\n" + + " (174) |11111111|11111111|1101011 7fffeb [23]\n" + + " (175) |11111111|11111111|1101100 7fffec [23]\n" + + " (176) |11111111|11111111|00000 1fffe0 [21]\n" + + " (177) |11111111|11111111|00001 1fffe1 [21]\n" + + " (178) |11111111|11111111|100000 3fffe0 [22]\n" + + " (179) |11111111|11111111|00010 1fffe2 [21]\n" + + " (180) |11111111|11111111|1101101 7fffed [23]\n" + + " (181) |11111111|11111111|100001 3fffe1 [22]\n" + + " (182) |11111111|11111111|1101110 7fffee [23]\n" + + " (183) |11111111|11111111|1101111 7fffef [23]\n" + + " (184) |11111111|11111110|1010 fffea [20]\n" + + " (185) |11111111|11111111|100010 3fffe2 [22]\n" + + " (186) |11111111|11111111|100011 3fffe3 [22]\n" + + " (187) |11111111|11111111|100100 3fffe4 [22]\n" + + " (188) |11111111|11111111|1110000 7ffff0 [23]\n" + + " (189) |11111111|11111111|100101 3fffe5 [22]\n" + + " (190) |11111111|11111111|100110 3fffe6 [22]\n" + + " (191) |11111111|11111111|1110001 7ffff1 [23]\n" + + " (192) |11111111|11111111|11111000|00 3ffffe0 [26]\n" + + " (193) |11111111|11111111|11111000|01 3ffffe1 [26]\n" + + " (194) |11111111|11111110|1011 fffeb [20]\n" + + " (195) |11111111|11111110|001 7fff1 [19]\n" + + " (196) |11111111|11111111|100111 3fffe7 [22]\n" + + " (197) |11111111|11111111|1110010 7ffff2 [23]\n" + + " (198) |11111111|11111111|101000 3fffe8 [22]\n" + + " (199) |11111111|11111111|11110110|0 1ffffec [25]\n" + + " (200) |11111111|11111111|11111000|10 3ffffe2 [26]\n" + + " (201) |11111111|11111111|11111000|11 3ffffe3 [26]\n" + + " (202) |11111111|11111111|11111001|00 3ffffe4 [26]\n" + + " (203) |11111111|11111111|11111011|110 7ffffde [27]\n" + + " (204) |11111111|11111111|11111011|111 7ffffdf [27]\n" + + " (205) |11111111|11111111|11111001|01 3ffffe5 [26]\n" + + " (206) |11111111|11111111|11110001 fffff1 [24]\n" + + " (207) |11111111|11111111|11110110|1 1ffffed [25]\n" + + " (208) |11111111|11111110|010 7fff2 [19]\n" + + " (209) |11111111|11111111|00011 1fffe3 [21]\n" + + " (210) |11111111|11111111|11111001|10 3ffffe6 [26]\n" + + " (211) |11111111|11111111|11111100|000 7ffffe0 [27]\n" + + " (212) |11111111|11111111|11111100|001 7ffffe1 [27]\n" + + " (213) |11111111|11111111|11111001|11 3ffffe7 [26]\n" + + " (214) |11111111|11111111|11111100|010 7ffffe2 [27]\n" + + " (215) |11111111|11111111|11110010 fffff2 [24]\n" + + " (216) |11111111|11111111|00100 1fffe4 [21]\n" + + " (217) |11111111|11111111|00101 1fffe5 [21]\n" + + " (218) |11111111|11111111|11111010|00 3ffffe8 [26]\n" + + " (219) |11111111|11111111|11111010|01 3ffffe9 [26]\n" + + " (220) |11111111|11111111|11111111|1101 ffffffd [28]\n" + + " (221) |11111111|11111111|11111100|011 7ffffe3 [27]\n" + + " (222) |11111111|11111111|11111100|100 7ffffe4 [27]\n" + + " (223) |11111111|11111111|11111100|101 7ffffe5 [27]\n" + + " (224) |11111111|11111110|1100 fffec [20]\n" + + " (225) |11111111|11111111|11110011 fffff3 [24]\n" + + " (226) |11111111|11111110|1101 fffed [20]\n" + + " (227) |11111111|11111111|00110 1fffe6 [21]\n" + + " (228) |11111111|11111111|101001 3fffe9 [22]\n" + + " (229) |11111111|11111111|00111 1fffe7 [21]\n" + + " (230) |11111111|11111111|01000 1fffe8 [21]\n" + + " (231) |11111111|11111111|1110011 7ffff3 [23]\n" + + " (232) |11111111|11111111|101010 3fffea [22]\n" + + " (233) |11111111|11111111|101011 3fffeb [22]\n" + + " (234) |11111111|11111111|11110111|0 1ffffee [25]\n" + + " (235) |11111111|11111111|11110111|1 1ffffef [25]\n" + + " (236) |11111111|11111111|11110100 fffff4 [24]\n" + + " (237) |11111111|11111111|11110101 fffff5 [24]\n" + + " (238) |11111111|11111111|11111010|10 3ffffea [26]\n" + + " (239) |11111111|11111111|1110100 7ffff4 [23]\n" + + " (240) |11111111|11111111|11111010|11 3ffffeb [26]\n" + + " (241) |11111111|11111111|11111100|110 7ffffe6 [27]\n" + + " (242) |11111111|11111111|11111011|00 3ffffec [26]\n" + + " (243) |11111111|11111111|11111011|01 3ffffed [26]\n" + + " (244) |11111111|11111111|11111100|111 7ffffe7 [27]\n" + + " (245) |11111111|11111111|11111101|000 7ffffe8 [27]\n" + + " (246) |11111111|11111111|11111101|001 7ffffe9 [27]\n" + + " (247) |11111111|11111111|11111101|010 7ffffea [27]\n" + + " (248) |11111111|11111111|11111101|011 7ffffeb [27]\n" + + " (249) |11111111|11111111|11111111|1110 ffffffe [28]\n" + + " (250) |11111111|11111111|11111101|100 7ffffec [27]\n" + + " (251) |11111111|11111111|11111101|101 7ffffed [27]\n" + + " (252) |11111111|11111111|11111101|110 7ffffee [27]\n" + + " (253) |11111111|11111111|11111101|111 7ffffef [27]\n" + + " (254) |11111111|11111111|11111110|000 7fffff0 [27]\n" + + " (255) |11111111|11111111|11111011|10 3ffffee [26]\n" + + " EOS (256) |11111111|11111111|11111111|111111 3fffffff [30]"; + // @formatter:on + + @Test + public void read_table() throws IOException { + Pattern line = Pattern.compile( + "\\(\\s*(?\\d+)\\s*\\)\\s*(?(\\|(0|1)+)+)\\s*" + + "(?[0-9a-zA-Z]+)\\s*\\[\\s*(?\\d+)\\s*\\]"); + Matcher m = line.matcher(SPEC); + int i = 0; + while (m.find()) { + String ascii = m.group("ascii"); + String binary = m.group("binary").replaceAll("\\|", ""); + String hex = m.group("hex"); + String len = m.group("len"); + + // Several sanity checks for the data read from the table, just to + // make sure what we read makes sense + assertEquals(parseInt(len), binary.length()); + assertEquals(parseInt(binary, 2), parseInt(hex, 16)); + + int expected = parseInt(ascii); + + // TODO: find actual eos, do not hardcode it! + byte[] bytes = intToBytes(0x3fffffff, 30, + parseInt(hex, 16), parseInt(len)); + + StringBuilder actual = new StringBuilder(); + Huffman.Reader t = new Huffman.Reader(); + t.read(ByteBuffer.wrap(bytes), actual, false, true); + + // What has been read MUST represent a single symbol + assertEquals(actual.length(), 1, "ascii: " + ascii); + + // It's a lot more visual to compare char as codes rather than + // characters (as some of them might not be visible) + assertEquals(actual.charAt(0), expected); + i++; + } + assertEquals(i, 257); // 256 + EOS + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.4.1 + // + @Test + public void read_1() { + read("f1e3 c2e5 f23a 6ba0 ab90 f4ff", "www.example.com"); + } + + @Test + public void write_1() { + write("www.example.com", "f1e3 c2e5 f23a 6ba0 ab90 f4ff"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.4.2 + // + @Test + public void read_2() { + read("a8eb 1064 9cbf", "no-cache"); + } + + @Test + public void write_2() { + write("no-cache", "a8eb 1064 9cbf"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.4.3 + // + @Test + public void read_3() { + read("25a8 49e9 5ba9 7d7f", "custom-key"); + } + + @Test + public void write_3() { + write("custom-key", "25a8 49e9 5ba9 7d7f"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.4.3 + // + @Test + public void read_4() { + read("25a8 49e9 5bb8 e8b4 bf", "custom-value"); + } + + @Test + public void write_4() { + write("custom-value", "25a8 49e9 5bb8 e8b4 bf"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.1 + // + @Test + public void read_5() { + read("6402", "302"); + } + + @Test + public void write_5() { + write("302", "6402"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.1 + // + @Test + public void read_6() { + read("aec3 771a 4b", "private"); + } + + @Test + public void write_6() { + write("private", "aec3 771a 4b"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.1 + // + @Test + public void read_7() { + read("d07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff", + "Mon, 21 Oct 2013 20:13:21 GMT"); + } + + @Test + public void write_7() { + write("Mon, 21 Oct 2013 20:13:21 GMT", + "d07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.1 + // + @Test + public void read_8() { + read("9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3", + "https://www.example.com"); + } + + @Test + public void write_8() { + write("https://www.example.com", + "9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.2 + // + @Test + public void read_9() { + read("640e ff", "307"); + } + + @Test + public void write_9() { + write("307", "640e ff"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.3 + // + @Test + public void read_10() { + read("d07a be94 1054 d444 a820 0595 040b 8166 e084 a62d 1bff", + "Mon, 21 Oct 2013 20:13:22 GMT"); + } + + @Test + public void write_10() { + write("Mon, 21 Oct 2013 20:13:22 GMT", + "d07a be94 1054 d444 a820 0595 040b 8166 e084 a62d 1bff"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.3 + // + @Test + public void read_11() { + read("9bd9 ab", "gzip"); + } + + @Test + public void write_11() { + write("gzip", "9bd9 ab"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.3 + // + @Test + public void read_12() { + read("94e7 821d d7f2 e6c7 b335 dfdf cd5b 3960 " + + "d5af 2708 7f36 72c1 ab27 0fb5 291f 9587 " + + "3160 65c0 03ed 4ee5 b106 3d50 07", + "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"); + } + + @Test + public void test_trie_has_no_empty_nodes() { + Huffman.Node root = Huffman.INSTANCE.getRoot(); + Stack backlog = new Stack<>(); + backlog.push(root); + while (!backlog.isEmpty()) { + Huffman.Node n = backlog.pop(); + // The only type of nodes we couldn't possibly catch during + // construction is an empty node: no children and no char + if (n.left != null) { + backlog.push(n.left); + } + if (n.right != null) { + backlog.push(n.right); + } + assertFalse(!n.charIsSet && n.left == null && n.right == null, + "Empty node in the trie"); + } + } + + @Test + public void test_trie_has_257_nodes() { + int count = 0; + Huffman.Node root = Huffman.INSTANCE.getRoot(); + Stack backlog = new Stack<>(); + backlog.push(root); + while (!backlog.isEmpty()) { + Huffman.Node n = backlog.pop(); + if (n.left != null) { + backlog.push(n.left); + } + if (n.right != null) { + backlog.push(n.right); + } + if (n.isLeaf()) { + count++; + } + } + assertEquals(count, 257); + } + + @Test + public void cant_encode_outside_byte() { + TestHelper.Block coding = + () -> new Huffman.Writer() + .from(((char) 256) + "", 0, 1) + .write(ByteBuffer.allocate(1)); + RuntimeException e = + TestHelper.assertVoidThrows(RuntimeException.class, coding); + TestHelper.assertExceptionMessageContains(e, "char"); + } + + private static void read(String hexdump, String decoded) { + ByteBuffer source = SpecHelper.toBytes(hexdump); + Appendable actual = new StringBuilder(); + try { + new Huffman.Reader().read(source, actual, true); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + assertEquals(actual.toString(), decoded); + } + + private static void write(String decoded, String hexdump) { + int n = Huffman.INSTANCE.lengthOf(decoded); + ByteBuffer destination = ByteBuffer.allocate(n); // Extra margin (1) to test having more bytes in the destination than needed is ok + Huffman.Writer writer = new Huffman.Writer(); + BuffersTestingKit.forEachSplit(destination, byteBuffers -> { + writer.from(decoded, 0, decoded.length()); + boolean written = false; + for (ByteBuffer b : byteBuffers) { + int pos = b.position(); + written = writer.write(b); + b.position(pos); + } + assertTrue(written); + ByteBuffer concated = BuffersTestingKit.concat(byteBuffers); + String actual = SpecHelper.toHexdump(concated); + assertEquals(actual, hexdump); + writer.reset(); + }); + } + + // + // It's not very pretty, yes I know that + // + // hex: + // + // |31|30|...|N-1|...|01|00| + // \ / + // codeLength + // + // hex <<= 32 - codeLength; (align to MSB): + // + // |31|30|...|32-N|...|01|00| + // \ / + // codeLength + // + // EOS: + // + // |31|30|...|M-1|...|01|00| + // \ / + // eosLength + // + // eos <<= 32 - eosLength; (align to MSB): + // + // pad with MSBs of EOS: + // + // |31|30|...|32-N|32-N-1|...|01|00| + // | 32|...| + // + // Finally, split into byte[] + // + private byte[] intToBytes(int eos, int eosLength, int hex, int codeLength) { + hex <<= 32 - codeLength; + eos >>= codeLength - (32 - eosLength); + hex |= eos; + int n = (int) Math.ceil(codeLength / 8.0); + byte[] result = new byte[n]; + for (int i = 0; i < n; i++) { + result[i] = (byte) (hex >> (32 - 8 * (i + 1))); + } + return result; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/SpecHelper.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/SpecHelper.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2014, 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.internal.net.http.hpack; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +// +// THIS IS NOT A TEST +// +public final class SpecHelper { + + private SpecHelper() { + throw new AssertionError(); + } + + public static ByteBuffer toBytes(String hexdump) { + Pattern hexByte = Pattern.compile("[0-9a-fA-F]{2}"); + List bytes = new ArrayList<>(); + Matcher matcher = hexByte.matcher(hexdump); + while (matcher.find()) { + bytes.add(matcher.group(0)); + } + ByteBuffer result = ByteBuffer.allocate(bytes.size()); + for (String f : bytes) { + result.put((byte) Integer.parseInt(f, 16)); + } + result.flip(); + return result; + } + + public static String toHexdump(ByteBuffer bb) { + List words = new ArrayList<>(); + int i = 0; + while (bb.hasRemaining()) { + if (i % 2 == 0) { + words.add(""); + } + byte b = bb.get(); + String hex = Integer.toHexString(256 + Byte.toUnsignedInt(b)).substring(1); + words.set(i / 2, words.get(i / 2) + hex); + i++; + } + return words.stream().collect(Collectors.joining(" ")); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/TestHelper.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/http2/java.net.http/jdk/internal/net/http/hpack/TestHelper.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2014, 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.internal.net.http.hpack; + +import org.testng.annotations.Test; + +import java.util.Objects; +import java.util.Random; + +public final class TestHelper { + + public static Random newRandom() { + long seed = Long.getLong("jdk.test.lib.random.seed", System.currentTimeMillis()); + System.out.println("new java.util.Random(" + seed + ")"); + return new Random(seed); + } + + public static T assertVoidThrows(Class clazz, Block code) { + return assertThrows(clazz, () -> { + code.run(); + return null; + }); + } + + public static T assertThrows(Class clazz, ReturningBlock code) { + Objects.requireNonNull(clazz, "clazz == null"); + Objects.requireNonNull(code, "code == null"); + try { + code.run(); + } catch (Throwable t) { + if (clazz.isInstance(t)) { + return clazz.cast(t); + } + throw new AssertionError("Expected to catch exception of type " + + clazz.getCanonicalName() + ", instead caught " + + t.getClass().getCanonicalName(), t); + + } + throw new AssertionError( + "Expected to catch exception of type " + clazz.getCanonicalName() + + ", but caught nothing"); + } + + public static T assertDoesNotThrow(ReturningBlock code) { + Objects.requireNonNull(code, "code == null"); + try { + return code.run(); + } catch (Throwable t) { + throw new AssertionError( + "Expected code block to exit normally, instead " + + "caught " + t.getClass().getCanonicalName(), t); + } + } + + public static void assertVoidDoesNotThrow(Block code) { + Objects.requireNonNull(code, "code == null"); + try { + code.run(); + } catch (Throwable t) { + throw new AssertionError( + "Expected code block to exit normally, instead " + + "caught " + t.getClass().getCanonicalName(), t); + } + } + + + public static void assertExceptionMessageContains(Throwable t, + CharSequence firstSubsequence, + CharSequence... others) { + assertCharSequenceContains(t.getMessage(), firstSubsequence, others); + } + + public static void assertCharSequenceContains(CharSequence s, + CharSequence firstSubsequence, + CharSequence... others) { + if (s == null) { + throw new NullPointerException("Exception message is null"); + } + String str = s.toString(); + String missing = null; + if (!str.contains(firstSubsequence.toString())) { + missing = firstSubsequence.toString(); + } else { + for (CharSequence o : others) { + if (!str.contains(o.toString())) { + missing = o.toString(); + break; + } + } + } + if (missing != null) { + throw new AssertionError("CharSequence '" + s + "'" + " does not " + + "contain subsequence '" + missing + "'"); + } + } + + public interface ReturningBlock { + T run() throws Throwable; + } + + public interface Block { + void run() throws Throwable; + } + + // tests + + @Test + public void assertThrows() { + assertThrows(NullPointerException.class, () -> ((Object) null).toString()); + } + + @Test + public void assertThrowsWrongType() { + try { + assertThrows(IllegalArgumentException.class, () -> ((Object) null).toString()); + } catch (AssertionError e) { + Throwable cause = e.getCause(); + String message = e.getMessage(); + if (cause != null + && cause instanceof NullPointerException + && message != null + && message.contains("instead caught")) { + return; + } + } + throw new AssertionError(); + } + + @Test + public void assertThrowsNoneCaught() { + try { + assertThrows(IllegalArgumentException.class, () -> null); + } catch (AssertionError e) { + Throwable cause = e.getCause(); + String message = e.getMessage(); + if (cause == null + && message != null + && message.contains("but caught nothing")) { + return; + } + } + throw new AssertionError(); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/server/BodyInputStream.java --- a/test/jdk/java/net/httpclient/http2/server/BodyInputStream.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/server/BodyInputStream.java Wed Feb 07 21:45:37 2018 +0000 @@ -25,10 +25,10 @@ import java.nio.ByteBuffer; import java.util.List; -import java.net.http.internal.common.Utils; -import java.net.http.internal.frame.DataFrame; -import java.net.http.internal.frame.Http2Frame; -import java.net.http.internal.frame.ResetFrame; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.frame.DataFrame; +import jdk.internal.net.http.frame.Http2Frame; +import jdk.internal.net.http.frame.ResetFrame; /** * InputStream reads frames off stream q and supplies read demand from any diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/server/BodyOutputStream.java --- a/test/jdk/java/net/httpclient/http2/server/BodyOutputStream.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/server/BodyOutputStream.java Wed Feb 07 21:45:37 2018 +0000 @@ -24,7 +24,7 @@ import java.io.*; import java.nio.ByteBuffer; -import java.net.http.internal.frame.DataFrame; +import jdk.internal.net.http.frame.DataFrame; /** * OutputStream. Incoming window updates handled by the main connection diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/server/EchoHandler.java --- a/test/jdk/java/net/httpclient/http2/server/EchoHandler.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/server/EchoHandler.java Wed Feb 07 21:45:37 2018 +0000 @@ -22,7 +22,7 @@ */ import java.io.*; -import java.net.http.internal.common.HttpHeadersImpl; +import jdk.internal.net.http.common.HttpHeadersImpl; public class EchoHandler implements Http2Handler { public EchoHandler() {} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/server/Http2EchoHandler.java --- a/test/jdk/java/net/httpclient/http2/server/Http2EchoHandler.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/server/Http2EchoHandler.java Wed Feb 07 21:45:37 2018 +0000 @@ -22,7 +22,7 @@ */ import java.io.*; -import java.net.http.internal.common.HttpHeadersImpl; +import jdk.internal.net.http.common.HttpHeadersImpl; public class Http2EchoHandler implements Http2Handler { public Http2EchoHandler() {} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/server/Http2RedirectHandler.java --- a/test/jdk/java/net/httpclient/http2/server/Http2RedirectHandler.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/server/Http2RedirectHandler.java Wed Feb 07 21:45:37 2018 +0000 @@ -25,7 +25,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.function.Supplier; -import java.net.http.internal.common.HttpHeadersImpl; +import jdk.internal.net.http.common.HttpHeadersImpl; public class Http2RedirectHandler implements Http2Handler { diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/server/Http2TestExchange.java --- a/test/jdk/java/net/httpclient/http2/server/Http2TestExchange.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/server/Http2TestExchange.java Wed Feb 07 21:45:37 2018 +0000 @@ -29,7 +29,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import javax.net.ssl.SSLSession; -import java.net.http.internal.common.HttpHeadersImpl; +import jdk.internal.net.http.common.HttpHeadersImpl; public interface Http2TestExchange { diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/server/Http2TestExchangeImpl.java --- a/test/jdk/java/net/httpclient/http2/server/Http2TestExchangeImpl.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/server/Http2TestExchangeImpl.java Wed Feb 07 21:45:37 2018 +0000 @@ -29,9 +29,9 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import javax.net.ssl.SSLSession; -import java.net.http.internal.common.HttpHeadersImpl; -import java.net.http.internal.frame.HeaderFrame; -import java.net.http.internal.frame.HeadersFrame; +import jdk.internal.net.http.common.HttpHeadersImpl; +import jdk.internal.net.http.frame.HeaderFrame; +import jdk.internal.net.http.frame.HeadersFrame; public class Http2TestExchangeImpl implements Http2TestExchange { diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/server/Http2TestExchangeSupplier.java --- a/test/jdk/java/net/httpclient/http2/server/Http2TestExchangeSupplier.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/server/Http2TestExchangeSupplier.java Wed Feb 07 21:45:37 2018 +0000 @@ -24,7 +24,7 @@ import javax.net.ssl.SSLSession; import java.io.InputStream; import java.net.URI; -import java.net.http.internal.common.HttpHeadersImpl; +import jdk.internal.net.http.common.HttpHeadersImpl; /** * A supplier of Http2TestExchanges. If the default Http2TestExchange impl is diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/server/Http2TestServer.java --- a/test/jdk/java/net/httpclient/http2/server/Http2TestServer.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/server/Http2TestServer.java Wed Feb 07 21:45:37 2018 +0000 @@ -35,7 +35,7 @@ import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SNIServerName; -import java.net.http.internal.frame.ErrorFrame; +import jdk.internal.net.http.frame.ErrorFrame; /** * Waits for incoming TCP connections from a client and establishes diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java --- a/test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java Wed Feb 07 21:45:37 2018 +0000 @@ -40,14 +40,14 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.Consumer; -import java.net.http.internal.common.HttpHeadersImpl; -import java.net.http.internal.frame.*; -import java.net.http.internal.hpack.Decoder; -import java.net.http.internal.hpack.DecodingCallback; -import java.net.http.internal.hpack.Encoder; +import jdk.internal.net.http.common.HttpHeadersImpl; +import jdk.internal.net.http.frame.*; +import jdk.internal.net.http.hpack.Decoder; +import jdk.internal.net.http.hpack.DecodingCallback; +import jdk.internal.net.http.hpack.Encoder; import sun.net.www.http.ChunkedInputStream; import sun.net.www.http.HttpClient; -import static java.net.http.internal.frame.SettingsFrame.HEADER_TABLE_SIZE; +import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE; /** * Represents one HTTP2 connection, either plaintext upgraded from HTTP/1.1 diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/server/OutgoingPushPromise.java --- a/test/jdk/java/net/httpclient/http2/server/OutgoingPushPromise.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/server/OutgoingPushPromise.java Wed Feb 07 21:45:37 2018 +0000 @@ -23,8 +23,8 @@ import java.io.*; import java.net.*; -import java.net.http.internal.common.HttpHeadersImpl; -import java.net.http.internal.frame.Http2Frame; +import jdk.internal.net.http.common.HttpHeadersImpl; +import jdk.internal.net.http.frame.Http2Frame; // will be converted to a PushPromiseFrame in the writeLoop // a thread is then created to produce the DataFrames from the InputStream diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/http2/server/PushHandler.java --- a/test/jdk/java/net/httpclient/http2/server/PushHandler.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/http2/server/PushHandler.java Wed Feb 07 21:45:37 2018 +0000 @@ -24,7 +24,7 @@ import java.io.*; import java.net.*; import java.nio.file.*; -import java.net.http.internal.common.HttpHeadersImpl; +import jdk.internal.net.http.common.HttpHeadersImpl; public class PushHandler implements Http2Handler { diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/BuildingWebSocketDriver.java --- a/test/jdk/java/net/httpclient/websocket/BuildingWebSocketDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/websocket/BuildingWebSocketDriver.java Wed Feb 07 21:45:37 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,9 @@ /* * @test * @bug 8159053 - * @modules java.net.http/java.net.http.internal.websocket:open - * @run testng/othervm --add-reads java.net.http=ALL-UNNAMED java.net.http/java.net.http.internal.websocket.BuildingWebSocketTest + * @modules java.net.http/jdk.internal.net.http.websocket:open + * @run testng/othervm + * --add-reads java.net.http=ALL-UNNAMED + * java.net.http/jdk.internal.net.http.websocket.BuildingWebSocketTest */ public final class BuildingWebSocketDriver { } diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/HeaderWriterDriver.java --- a/test/jdk/java/net/httpclient/websocket/HeaderWriterDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/websocket/HeaderWriterDriver.java Wed Feb 07 21:45:37 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,9 @@ /* * @test * @bug 8159053 - * @modules java.net.http/java.net.http.internal.websocket:open - * @run testng/othervm --add-reads java.net.http=ALL-UNNAMED java.net.http/java.net.http.internal.websocket.HeaderWriterTest + * @modules java.net.http/jdk.internal.net.http.websocket:open + * @run testng/othervm + * --add-reads java.net.http=ALL-UNNAMED + * java.net.http/jdk.internal.net.http.websocket.HeaderWriterTest */ public final class HeaderWriterDriver { } diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/MaskerDriver.java --- a/test/jdk/java/net/httpclient/websocket/MaskerDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/websocket/MaskerDriver.java Wed Feb 07 21:45:37 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,9 @@ /* * @test * @bug 8159053 - * @modules java.net.http/java.net.http.internal.websocket:open - * @run testng/othervm --add-reads java.net.http=ALL-UNNAMED java.net.http/java.net.http.internal.websocket.MaskerTest + * @modules java.net.http/jdk.internal.net.http.websocket:open + * @run testng/othervm + * --add-reads java.net.http=ALL-UNNAMED + * java.net.http/jdk.internal.net.http.websocket.MaskerTest */ public final class MaskerDriver { } diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/ReaderDriver.java --- a/test/jdk/java/net/httpclient/websocket/ReaderDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/websocket/ReaderDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -24,7 +24,7 @@ /* * @test * @bug 8159053 - * @modules java.net.http/java.net.http.internal.websocket:open - * @run testng/othervm --add-reads java.net.http=ALL-UNNAMED java.net.http/java.net.http.internal.websocket.ReaderTest + * @modules java.net.http/jdk.internal.net.http.websocket:open + * @run testng/othervm --add-reads java.net.http=ALL-UNNAMED java.net.http/jdk.internal.net.http.websocket.ReaderTest */ public final class ReaderDriver { } diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/WebSocketImplDriver.java --- a/test/jdk/java/net/httpclient/websocket/WebSocketImplDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/websocket/WebSocketImplDriver.java Wed Feb 07 21:45:37 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 @@ -23,7 +23,9 @@ /* * @test - * @modules java.net.http/java.net.http.internal.websocket:open - * @run testng/othervm --add-reads java.net.http=ALL-UNNAMED java.net.http/java.net.http.internal.websocket.WebSocketImplTest + * @modules java.net.http/jdk.internal.net.http.websocket:open + * @run testng/othervm + * --add-reads java.net.http=ALL-UNNAMED + * java.net.http/jdk.internal.net.http.websocket.WebSocketImplTest */ public class WebSocketImplDriver { } diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/BuildingWebSocketTest.java --- a/test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/BuildingWebSocketTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,225 +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. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package java.net.http.internal.websocket; - -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.WebSocket; -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CompletionStage; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static java.net.http.internal.websocket.TestSupport.assertCompletesExceptionally; -import static java.net.http.internal.websocket.TestSupport.assertThrows; - -/* - * In some places in this class a new String is created out of a string literal. - * The idea is to make sure the code under test relies on something better than - * the reference equality ( == ) for string equality checks. - */ -public class BuildingWebSocketTest { - - private final static URI VALID_URI = URI.create("ws://websocket.example.com"); - - @Test - public void nullArguments() { - HttpClient c = HttpClient.newHttpClient(); - - assertThrows(NullPointerException.class, - () -> c.newWebSocketBuilder() - .buildAsync(null, listener())); - assertThrows(NullPointerException.class, - () -> c.newWebSocketBuilder() - .buildAsync(VALID_URI, null)); - assertThrows(NullPointerException.class, - () -> c.newWebSocketBuilder() - .buildAsync(null, null)); - assertThrows(NullPointerException.class, - () -> c.newWebSocketBuilder() - .header(null, "value")); - assertThrows(NullPointerException.class, - () -> c.newWebSocketBuilder() - .header("name", null)); - assertThrows(NullPointerException.class, - () -> c.newWebSocketBuilder() - .header(null, null)); - assertThrows(NullPointerException.class, - () -> c.newWebSocketBuilder() - .subprotocols(null)); - assertThrows(NullPointerException.class, - () -> c.newWebSocketBuilder() - .subprotocols(null, "sub2.example.com")); - assertThrows(NullPointerException.class, - () -> c.newWebSocketBuilder() - .subprotocols("sub1.example.com", (String) null)); - assertThrows(NullPointerException.class, - () -> c.newWebSocketBuilder() - .subprotocols("sub1.example.com", (String[]) null)); - assertThrows(NullPointerException.class, - () -> c.newWebSocketBuilder() - .subprotocols("sub1.example.com", "sub2.example.com", null)); - assertThrows(NullPointerException.class, - () -> c.newWebSocketBuilder() - .subprotocols("sub1.example.com", null, "sub3.example.com")); - assertThrows(NullPointerException.class, - () -> c.newWebSocketBuilder() - .connectTimeout(null)); - } - - @Test(dataProvider = "badURIs") - void illegalURI(URI uri) { - WebSocket.Builder b = HttpClient.newHttpClient().newWebSocketBuilder(); - assertCompletesExceptionally(IllegalArgumentException.class, - b.buildAsync(uri, listener())); - } - - @Test - public void illegalHeaders() { - List headers = - List.of("Sec-WebSocket-Accept", - "Sec-WebSocket-Extensions", - "Sec-WebSocket-Key", - "Sec-WebSocket-Protocol", - "Sec-WebSocket-Version") - .stream() - .flatMap(s -> Stream.of(s, new String(s))) // a string and a copy of it - .collect(Collectors.toList()); - - Function> f = - header -> HttpClient.newHttpClient() - .newWebSocketBuilder() - .header(header, "value") - .buildAsync(VALID_URI, listener()); - - headers.forEach(h -> assertCompletesExceptionally(IllegalArgumentException.class, f.apply(h))); - } - - // TODO: test for bad syntax headers - // TODO: test for overwrites (subprotocols) and additions (headers) - - @Test(dataProvider = "badSubprotocols") - public void illegalSubprotocolsSyntax(String s) { - WebSocket.Builder b = HttpClient.newHttpClient() - .newWebSocketBuilder() - .subprotocols(s); - assertCompletesExceptionally(IllegalArgumentException.class, - b.buildAsync(VALID_URI, listener())); - } - - @Test(dataProvider = "duplicatingSubprotocols") - public void illegalSubprotocolsDuplicates(String mostPreferred, - String[] lesserPreferred) { - WebSocket.Builder b = HttpClient.newHttpClient() - .newWebSocketBuilder() - .subprotocols(mostPreferred, lesserPreferred); - assertCompletesExceptionally(IllegalArgumentException.class, - b.buildAsync(VALID_URI, listener())); - } - - @Test(dataProvider = "badConnectTimeouts") - public void illegalConnectTimeout(Duration d) { - WebSocket.Builder b = HttpClient.newHttpClient() - .newWebSocketBuilder() - .connectTimeout(d); - assertCompletesExceptionally(IllegalArgumentException.class, - b.buildAsync(VALID_URI, listener())); - } - - @DataProvider - public Object[][] badURIs() { - return new Object[][]{ - {URI.create("http://example.com")}, - {URI.create("ftp://example.com")}, - {URI.create("wss://websocket.example.com/hello#fragment")}, - {URI.create("ws://websocket.example.com/hello#fragment")}, - }; - } - - @DataProvider - public Object[][] badConnectTimeouts() { - return new Object[][]{ - {Duration.ofDays ( 0)}, - {Duration.ofDays (-1)}, - {Duration.ofHours ( 0)}, - {Duration.ofHours (-1)}, - {Duration.ofMinutes( 0)}, - {Duration.ofMinutes(-1)}, - {Duration.ofSeconds( 0)}, - {Duration.ofSeconds(-1)}, - {Duration.ofMillis ( 0)}, - {Duration.ofMillis (-1)}, - {Duration.ofNanos ( 0)}, - {Duration.ofNanos (-1)}, - {Duration.ZERO}, - }; - } - - // https://tools.ietf.org/html/rfc7230#section-3.2.6 - // https://tools.ietf.org/html/rfc20 - @DataProvider - public static Object[][] badSubprotocols() { - return new Object[][]{ - {""}, - {new String("")}, - {"round-brackets("}, - {"round-brackets)"}, - {"comma,"}, - {"slash/"}, - {"colon:"}, - {"semicolon;"}, - {"angle-brackets<"}, - {"angle-brackets>"}, - {"equals="}, - {"question-mark?"}, - {"at@"}, - {"brackets["}, - {"backslash\\"}, - {"brackets]"}, - {"curly-brackets{"}, - {"curly-brackets}"}, - {"space "}, - {"non-printable-character " + Character.toString((char) 31)}, - {"non-printable-character " + Character.toString((char) 127)}, - }; - } - - @DataProvider - public static Object[][] duplicatingSubprotocols() { - return new Object[][]{ - {"a.b.c", new String[]{"a.b.c"}}, - {"a.b.c", new String[]{"x.y.z", "p.q.r", "x.y.z"}}, - {"a.b.c", new String[]{new String("a.b.c")}}, - }; - } - - private static WebSocket.Listener listener() { - return new WebSocket.Listener() { }; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/HeaderWriterTest.java --- a/test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/HeaderWriterTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,115 +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. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package java.net.http.internal.websocket; - -import org.testng.annotations.Test; -import java.net.http.internal.websocket.Frame.HeaderWriter; -import java.net.http.internal.websocket.Frame.Opcode; - -import java.nio.ByteBuffer; -import java.util.OptionalInt; - -import static java.util.OptionalInt.empty; -import static java.util.OptionalInt.of; -import static org.testng.Assert.assertEquals; -import static java.net.http.internal.websocket.TestSupport.assertThrows; -import static java.net.http.internal.websocket.TestSupport.forEachPermutation; - -public class HeaderWriterTest { - - private long cases, frames; - - @Test - public void negativePayload() { - System.out.println("testing negative payload"); - HeaderWriter w = new HeaderWriter(); - assertThrows(IllegalArgumentException.class, - ".*(?i)negative.*", - () -> w.payloadLen(-1)); - } - - @Test - public void test() { - System.out.println("testing regular payloads"); - final long[] payloads = {0, 126, 65536, Integer.MAX_VALUE + 1L}; - final OptionalInt[] masks = {empty(), of(-1), of(0), of(0xCAFEBABE), - of(Integer.MAX_VALUE), of(Integer.MIN_VALUE)}; - for (boolean fin : new boolean[]{true, false}) { - for (boolean rsv1 : new boolean[]{true, false}) { - for (boolean rsv2 : new boolean[]{true, false}) { - for (boolean rsv3 : new boolean[]{true, false}) { - for (Opcode opcode : Opcode.values()) { - for (long payloadLen : payloads) { - for (OptionalInt mask : masks) { - verify(fin, rsv1, rsv2, rsv3, opcode, payloadLen, mask); - } - } - } - } - } - } - } - System.out.println("Frames: " + frames + ", Total cases: " + cases); - } - - private void verify(boolean fin, - boolean rsv1, - boolean rsv2, - boolean rsv3, - Opcode opcode, - long payloadLen, - OptionalInt mask) { - frames++; - HeaderWriter writer = new HeaderWriter(); - ByteBuffer expected = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES); - writer.fin(fin).rsv1(rsv1).rsv2(rsv2).rsv3(rsv3).opcode(opcode).payloadLen(payloadLen); - mask.ifPresentOrElse(writer::mask, writer::noMask); - writer.write(expected); - expected.flip(); - verifyPermutations(expected, writer, - () -> writer.fin(fin), - () -> writer.rsv1(rsv1), - () -> writer.rsv2(rsv2), - () -> writer.rsv3(rsv3), - () -> writer.opcode(opcode), - () -> writer.payloadLen(payloadLen), - () -> mask.ifPresentOrElse(writer::mask, writer::noMask)); - } - - private void verifyPermutations(ByteBuffer expected, - HeaderWriter writer, - Runnable... actions) { - forEachPermutation(actions.length, - order -> { - cases++; - for (int i : order) { - actions[i].run(); - } - ByteBuffer actual = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES + 2); - writer.write(actual); - actual.flip(); - assertEquals(actual, expected); - }); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/MaskerTest.java --- a/test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/MaskerTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,158 +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. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package java.net.http.internal.websocket; - -import org.testng.annotations.Test; - -import java.nio.ByteBuffer; -import java.security.SecureRandom; -import java.util.stream.IntStream; - -import static org.testng.Assert.assertEquals; -import static java.net.http.internal.websocket.Frame.Masker.transferMasking; -import static java.net.http.internal.websocket.TestSupport.forEachBufferPartition; -import static java.net.http.internal.websocket.TestSupport.fullCopy; - -public class MaskerTest { - - private static final SecureRandom random = new SecureRandom(); - - @Test - public void stateless() { - IntStream.iterate(0, i -> i + 1).limit(125).boxed() - .forEach(r -> { - int m = random.nextInt(); - ByteBuffer src = createSourceBuffer(r); - ByteBuffer dst = createDestinationBuffer(r); - verify(src, dst, maskArray(m), 0, - () -> transferMasking(src, dst, m)); - }); - } - - /* - * Stateful masker to make sure setting a mask resets the state as if a new - * Masker instance is created each time - */ - private final Frame.Masker masker = new Frame.Masker(); - - @Test - public void stateful0() { - // This size (17 = 8 + 8 + 1) should test all the stages - // (galloping/slow) of masking good enough - int N = 17; - ByteBuffer src = createSourceBuffer(N); - ByteBuffer dst = createDestinationBuffer(N); - int mask = random.nextInt(); - forEachBufferPartition(src, - buffers -> { - int offset = 0; - masker.mask(mask); - int[] maskBytes = maskArray(mask); - for (ByteBuffer s : buffers) { - offset = verify(s, dst, maskBytes, offset, - () -> masker.transferMasking(s, dst)); - } - }); - } - - @Test - public void stateful1() { - int m = random.nextInt(); - masker.mask(m); - ByteBuffer src = ByteBuffer.allocate(0); - ByteBuffer dst = ByteBuffer.allocate(16); - verify(src, dst, maskArray(m), 0, - () -> masker.transferMasking(src, dst)); - } - - private static int verify(ByteBuffer src, - ByteBuffer dst, - int[] maskBytes, - int offset, - Runnable masking) { - ByteBuffer srcCopy = fullCopy(src); - ByteBuffer dstCopy = fullCopy(dst); - masking.run(); - int srcRemaining = srcCopy.remaining(); - int dstRemaining = dstCopy.remaining(); - int masked = Math.min(srcRemaining, dstRemaining); - // 1. position check - assertEquals(src.position(), srcCopy.position() + masked); - assertEquals(dst.position(), dstCopy.position() + masked); - // 2. masking check - src.position(srcCopy.position()); - dst.position(dstCopy.position()); - for (; src.hasRemaining() && dst.hasRemaining(); - offset = (offset + 1) & 3) { - assertEquals(dst.get(), src.get() ^ maskBytes[offset]); - } - // 3. corruption check - // 3.1 src contents haven't changed - int srcPosition = src.position(); - int srcLimit = src.limit(); - src.clear(); - srcCopy.clear(); - assertEquals(src, srcCopy); - src.limit(srcLimit).position(srcPosition); // restore src - // 3.2 dst leading and trailing regions' contents haven't changed - int dstPosition = dst.position(); - int dstInitialPosition = dstCopy.position(); - int dstLimit = dst.limit(); - // leading - dst.position(0).limit(dstInitialPosition); - dstCopy.position(0).limit(dstInitialPosition); - assertEquals(dst, dstCopy); - // trailing - dst.limit(dst.capacity()).position(dstLimit); - dstCopy.limit(dst.capacity()).position(dstLimit); - assertEquals(dst, dstCopy); - // restore dst - dst.position(dstPosition).limit(dstLimit); - return offset; - } - - private static ByteBuffer createSourceBuffer(int remaining) { - int leading = random.nextInt(4); - int trailing = random.nextInt(4); - byte[] bytes = new byte[leading + remaining + trailing]; - random.nextBytes(bytes); - return ByteBuffer.wrap(bytes).position(leading).limit(leading + remaining); - } - - private static ByteBuffer createDestinationBuffer(int remaining) { - int leading = random.nextInt(4); - int trailing = random.nextInt(4); - return ByteBuffer.allocate(leading + remaining + trailing) - .position(leading).limit(leading + remaining); - } - - private static int[] maskArray(int mask) { - return new int[]{ - (byte) (mask >>> 24), - (byte) (mask >>> 16), - (byte) (mask >>> 8), - (byte) (mask >>> 0) - }; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/MockListener.java --- a/test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/MockListener.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,402 +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 java.net.http.internal.websocket; - -import java.net.http.WebSocket; -import java.net.http.WebSocket.MessagePart; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -import static java.net.http.internal.websocket.TestSupport.fullCopy; - -public class MockListener implements WebSocket.Listener { - - private final long bufferSize; - private long count; - private final List invocations = new ArrayList<>(); - private final CompletableFuture lastCall = new CompletableFuture<>(); - - /* - * Typical buffer sizes: 1, n, Long.MAX_VALUE - */ - public MockListener(long bufferSize) { - if (bufferSize < 1) { - throw new IllegalArgumentException(); - } - this.bufferSize = bufferSize; - } - - @Override - public void onOpen(WebSocket webSocket) { - System.out.printf("onOpen(%s)%n", webSocket); - invocations.add(new OnOpen(webSocket)); - onOpen0(webSocket); - } - - protected void onOpen0(WebSocket webSocket) { - replenish(webSocket); - } - - @Override - public CompletionStage onText(WebSocket webSocket, - CharSequence message, - MessagePart part) { - System.out.printf("onText(%s, %s, %s)%n", webSocket, message, part); - invocations.add(new OnText(webSocket, message.toString(), part)); - return onText0(webSocket, message, part); - } - - protected CompletionStage onText0(WebSocket webSocket, - CharSequence message, - MessagePart part) { - replenish(webSocket); - return null; - } - - @Override - public CompletionStage onBinary(WebSocket webSocket, - ByteBuffer message, - MessagePart part) { - System.out.printf("onBinary(%s, %s, %s)%n", webSocket, message, part); - invocations.add(new OnBinary(webSocket, fullCopy(message), part)); - return onBinary0(webSocket, message, part); - } - - protected CompletionStage onBinary0(WebSocket webSocket, - ByteBuffer message, - MessagePart part) { - replenish(webSocket); - return null; - } - - @Override - public CompletionStage onPing(WebSocket webSocket, ByteBuffer message) { - System.out.printf("onPing(%s, %s)%n", webSocket, message); - invocations.add(new OnPing(webSocket, fullCopy(message))); - return onPing0(webSocket, message); - } - - protected CompletionStage onPing0(WebSocket webSocket, ByteBuffer message) { - replenish(webSocket); - return null; - } - - @Override - public CompletionStage onPong(WebSocket webSocket, ByteBuffer message) { - System.out.printf("onPong(%s, %s)%n", webSocket, message); - invocations.add(new OnPong(webSocket, fullCopy(message))); - return onPong0(webSocket, message); - } - - protected CompletionStage onPong0(WebSocket webSocket, ByteBuffer message) { - replenish(webSocket); - return null; - } - - @Override - public CompletionStage onClose(WebSocket webSocket, - int statusCode, - String reason) { - System.out.printf("onClose(%s, %s, %s)%n", webSocket, statusCode, reason); - invocations.add(new OnClose(webSocket, statusCode, reason)); - lastCall.complete(null); - return null; - } - - @Override - public void onError(WebSocket webSocket, Throwable error) { - System.out.printf("onError(%s, %s)%n", webSocket, error); - invocations.add(new OnError(webSocket, error == null ? null : error.getClass())); - lastCall.complete(null); - } - - public CompletableFuture onCloseOrOnErrorCalled() { - return lastCall.copy(); - } - - protected void replenish(WebSocket webSocket) { - if (--count <= 0) { - count = bufferSize - bufferSize / 2; - } - webSocket.request(count); - } - - public List invocations() { - return new ArrayList<>(invocations); - } - - public abstract static class Invocation { - - public static OnOpen onOpen(WebSocket webSocket) { - return new OnOpen(webSocket); - } - - public static OnText onText(WebSocket webSocket, - String text, - MessagePart part) { - return new OnText(webSocket, text, part); - } - - public static OnBinary onBinary(WebSocket webSocket, - ByteBuffer data, - MessagePart part) { - return new OnBinary(webSocket, data, part); - } - - public static OnPing onPing(WebSocket webSocket, - ByteBuffer data) { - return new OnPing(webSocket, data); - } - - public static OnPong onPong(WebSocket webSocket, - ByteBuffer data) { - return new OnPong(webSocket, data); - } - - public static OnClose onClose(WebSocket webSocket, - int statusCode, - String reason) { - return new OnClose(webSocket, statusCode, reason); - } - - public static OnError onError(WebSocket webSocket, - Class clazz) { - return new OnError(webSocket, clazz); - } - - final WebSocket webSocket; - - private Invocation(WebSocket webSocket) { - this.webSocket = webSocket; - } - } - - public static final class OnOpen extends Invocation { - - public OnOpen(WebSocket webSocket) { - super(webSocket); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Invocation that = (Invocation) o; - return Objects.equals(webSocket, that.webSocket); - } - - @Override - public int hashCode() { - return Objects.hashCode(webSocket); - } - } - - public static final class OnText extends Invocation { - - final String text; - final MessagePart part; - - public OnText(WebSocket webSocket, String text, MessagePart part) { - super(webSocket); - this.text = text; - this.part = part; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - OnText onText = (OnText) o; - return Objects.equals(text, onText.text) && - part == onText.part && - Objects.equals(webSocket, onText.webSocket); - } - - @Override - public int hashCode() { - return Objects.hash(text, part, webSocket); - } - - @Override - public String toString() { - return String.format("onText(%s, %s, %s)", webSocket, text, part); - } - } - - public static final class OnBinary extends Invocation { - - final ByteBuffer data; - final MessagePart part; - - public OnBinary(WebSocket webSocket, ByteBuffer data, MessagePart part) { - super(webSocket); - this.data = data; - this.part = part; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - OnBinary onBinary = (OnBinary) o; - return Objects.equals(data, onBinary.data) && - part == onBinary.part && - Objects.equals(webSocket, onBinary.webSocket); - } - - @Override - public int hashCode() { - return Objects.hash(data, part, webSocket); - } - - @Override - public String toString() { - return String.format("onBinary(%s, %s, %s)", webSocket, data, part); - } - } - - public static final class OnPing extends Invocation { - - final ByteBuffer data; - - public OnPing(WebSocket webSocket, ByteBuffer data) { - super(webSocket); - this.data = data; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - OnPing onPing = (OnPing) o; - return Objects.equals(data, onPing.data) && - Objects.equals(webSocket, onPing.webSocket); - } - - @Override - public int hashCode() { - return Objects.hash(data, webSocket); - } - - @Override - public String toString() { - return String.format("onPing(%s, %s)", webSocket, data); - } - } - - public static final class OnPong extends Invocation { - - final ByteBuffer data; - - public OnPong(WebSocket webSocket, ByteBuffer data) { - super(webSocket); - this.data = data; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - OnPong onPong = (OnPong) o; - return Objects.equals(data, onPong.data) && - Objects.equals(webSocket, onPong.webSocket); - } - - @Override - public int hashCode() { - return Objects.hash(data, webSocket); - } - - @Override - public String toString() { - return String.format("onPong(%s, %s)", webSocket, data); - } - } - - public static final class OnClose extends Invocation { - - final int statusCode; - final String reason; - - public OnClose(WebSocket webSocket, int statusCode, String reason) { - super(webSocket); - this.statusCode = statusCode; - this.reason = reason; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - OnClose onClose = (OnClose) o; - return statusCode == onClose.statusCode && - Objects.equals(reason, onClose.reason) && - Objects.equals(webSocket, onClose.webSocket); - } - - @Override - public int hashCode() { - return Objects.hash(statusCode, reason, webSocket); - } - - @Override - public String toString() { - return String.format("onClose(%s, %s, %s)", webSocket, statusCode, reason); - } - } - - public static final class OnError extends Invocation { - - final Class clazz; - - public OnError(WebSocket webSocket, Class clazz) { - super(webSocket); - this.clazz = clazz; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - OnError onError = (OnError) o; - return Objects.equals(clazz, onError.clazz) && - Objects.equals(webSocket, onError.webSocket); - } - - @Override - public int hashCode() { - return Objects.hash(clazz, webSocket); - } - - @Override - public String toString() { - return String.format("onError(%s, %s)", webSocket, clazz); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/MockTransport.java --- a/test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/MockTransport.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,436 +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 java.net.http.internal.websocket; - -import java.net.http.WebSocket.MessagePart; -import java.net.http.internal.common.Demand; -import java.net.http.internal.common.SequentialScheduler; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.Queue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import static java.net.http.internal.websocket.TestSupport.fullCopy; - -public class MockTransport implements Transport { - - private final long startTime = System.currentTimeMillis(); - private final Queue output = new ConcurrentLinkedQueue<>(); - private final Queue>> - input = new ConcurrentLinkedQueue<>(); - private final Supplier supplier; - private final MessageStreamConsumer consumer; - private final SequentialScheduler scheduler - = new SequentialScheduler(new ReceiveTask()); - private final Demand demand = new Demand(); - - public MockTransport(Supplier sendResultSupplier, - MessageStreamConsumer consumer) { - this.supplier = sendResultSupplier; - this.consumer = consumer; - input.addAll(receive()); - } - - @Override - public final CompletableFuture sendText(CharSequence message, - boolean isLast) { - output.add(Invocation.sendText(message, isLast)); - return send(String.format("sendText(%s, %s)", message, isLast), - () -> sendText0(message, isLast)); - } - - protected CompletableFuture sendText0(CharSequence message, - boolean isLast) { - return defaultSend(); - } - - protected CompletableFuture defaultSend() { - return CompletableFuture.completedFuture(result()); - } - - @Override - public final CompletableFuture sendBinary(ByteBuffer message, - boolean isLast) { - output.add(Invocation.sendBinary(message, isLast)); - return send(String.format("sendBinary(%s, %s)", message, isLast), - () -> sendBinary0(message, isLast)); - } - - protected CompletableFuture sendBinary0(ByteBuffer message, - boolean isLast) { - return defaultSend(); - } - - @Override - public final CompletableFuture sendPing(ByteBuffer message) { - output.add(Invocation.sendPing(message)); - return send(String.format("sendPing(%s)", message), - () -> sendPing0(message)); - } - - protected CompletableFuture sendPing0(ByteBuffer message) { - return defaultSend(); - } - - @Override - public final CompletableFuture sendPong(ByteBuffer message) { - output.add(Invocation.sendPong(message)); - return send(String.format("sendPong(%s)", message), - () -> sendPong0(message)); - } - - protected CompletableFuture sendPong0(ByteBuffer message) { - return defaultSend(); - } - - @Override - public final CompletableFuture sendClose(int statusCode, String reason) { - output.add(Invocation.sendClose(statusCode, reason)); - return send(String.format("sendClose(%s, %s)", statusCode, reason), - () -> sendClose0(statusCode, reason)); - } - - protected CompletableFuture sendClose0(int statusCode, String reason) { - return defaultSend(); - } - - protected Collection>> receive() { - return List.of(); - } - - public static Consumer onText(CharSequence data, - MessagePart part) { - return c -> c.onText(data.toString(), part); - } - - public static Consumer onBinary(ByteBuffer data, - MessagePart part) { - return c -> c.onBinary(fullCopy(data), part); - } - - public static Consumer onPing(ByteBuffer data) { - return c -> c.onPing(fullCopy(data)); - } - - public static Consumer onPong(ByteBuffer data) { - return c -> c.onPong(fullCopy(data)); - } - - public static Consumer onClose(int statusCode, - String reason) { - return c -> c.onClose(statusCode, reason); - } - - public static Consumer onError(Throwable error) { - return c -> c.onError(error); - } - - public static Consumer onComplete() { - return c -> c.onComplete(); - } - - @Override - public void request(long n) { - demand.increase(n); - scheduler.runOrSchedule(); - } - - @Override - public void acknowledgeReception() { - demand.tryDecrement(); - } - - @Override - public final void closeOutput() throws IOException { - output.add(Invocation.closeOutput()); - begin("closeOutput()"); - closeOutput0(); - end("closeOutput()"); - } - - protected void closeOutput0() throws IOException { - defaultClose(); - } - - protected void defaultClose() throws IOException { - } - - @Override - public final void closeInput() throws IOException { - output.add(Invocation.closeInput()); - begin("closeInput()"); - closeInput0(); - end("closeInput()"); - } - - protected void closeInput0() throws IOException { - defaultClose(); - } - - public abstract static class Invocation { - - static Invocation.SendText sendText(CharSequence message, - boolean isLast) { - return new SendText(message, isLast); - } - - static Invocation.SendBinary sendBinary(ByteBuffer message, - boolean isLast) { - return new SendBinary(message, isLast); - } - - static Invocation.SendPing sendPing(ByteBuffer message) { - return new SendPing(message); - } - - static Invocation.SendPong sendPong(ByteBuffer message) { - return new SendPong(message); - } - - static Invocation.SendClose sendClose(int statusCode, String reason) { - return new SendClose(statusCode, reason); - } - - public static CloseOutput closeOutput() { - return new CloseOutput(); - } - - public static CloseInput closeInput() { - return new CloseInput(); - } - - public static final class SendText extends Invocation { - - final CharSequence message; - final boolean isLast; - - SendText(CharSequence message, boolean isLast) { - this.message = message.toString(); - this.isLast = isLast; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - SendText sendText = (SendText) obj; - return isLast == sendText.isLast && - Objects.equals(message, sendText.message); - } - - @Override - public int hashCode() { - return Objects.hash(isLast, message); - } - } - - public static final class SendBinary extends Invocation { - - final ByteBuffer message; - final boolean isLast; - - SendBinary(ByteBuffer message, boolean isLast) { - this.message = fullCopy(message); - this.isLast = isLast; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - SendBinary that = (SendBinary) obj; - return isLast == that.isLast && - Objects.equals(message, that.message); - } - - @Override - public int hashCode() { - return Objects.hash(message, isLast); - } - } - - private static final class SendPing extends Invocation { - - final ByteBuffer message; - - SendPing(ByteBuffer message) { - this.message = fullCopy(message); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - SendPing sendPing = (SendPing) obj; - return Objects.equals(message, sendPing.message); - } - - @Override - public int hashCode() { - return Objects.hash(message); - } - } - - private static final class SendPong extends Invocation { - - final ByteBuffer message; - - SendPong(ByteBuffer message) { - this.message = fullCopy(message); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - SendPing sendPing = (SendPing) obj; - return Objects.equals(message, sendPing.message); - } - - @Override - public int hashCode() { - return Objects.hash(message); - } - } - - private static final class SendClose extends Invocation { - - final int statusCode; - final String reason; - - SendClose(int statusCode, String reason) { - this.statusCode = statusCode; - this.reason = reason; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - SendClose sendClose = (SendClose) obj; - return statusCode == sendClose.statusCode && - Objects.equals(reason, sendClose.reason); - } - - @Override - public int hashCode() { - return Objects.hash(statusCode, reason); - } - } - - private static final class CloseOutput extends Invocation { - - CloseOutput() { } - - @Override - public int hashCode() { - return 0; - } - - @Override - public boolean equals(Object obj) { - return obj instanceof CloseOutput; - } - } - - private static final class CloseInput extends Invocation { - - CloseInput() { } - - @Override - public int hashCode() { - return 0; - } - - @Override - public boolean equals(Object obj) { - return obj instanceof CloseInput; - } - } - } - - public Queue invocations() { - return new LinkedList<>(output); - } - - protected final T result() { - return supplier.get(); - } - - private CompletableFuture send(String name, - Supplier> supplier) { - begin(name); - CompletableFuture cf = supplier.get().whenComplete((r, e) -> { - System.out.printf("[%6s ms.] complete %s%n", elapsedTime(), name); - }); - end(name); - return cf; - } - - private void begin(String name) { - System.out.printf("[%6s ms.] begin %s%n", elapsedTime(), name); - } - - private void end(String name) { - System.out.printf("[%6s ms.] end %s%n", elapsedTime(), name); - } - - private long elapsedTime() { - return System.currentTimeMillis() - startTime; - } - - private final class ReceiveTask implements SequentialScheduler.RestartableTask { - - @Override - public void run(SequentialScheduler.DeferredCompleter taskCompleter) { - if (!scheduler.isStopped() && !demand.isFulfilled() && !input.isEmpty()) { - CompletableFuture> cf = input.remove(); - if (cf.isDone()) { // Forcing synchronous execution - cf.join().accept(consumer); - repeat(taskCompleter); - } else { - cf.whenCompleteAsync((r, e) -> { - r.accept(consumer); - repeat(taskCompleter); - }); - } - } else { - taskCompleter.complete(); - } - } - - private void repeat(SequentialScheduler.DeferredCompleter taskCompleter) { - taskCompleter.complete(); - scheduler.runOrSchedule(); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/ReaderTest.java --- a/test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/ReaderTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,271 +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. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package java.net.http.internal.websocket; - -import org.testng.annotations.Test; -import java.net.http.internal.websocket.Frame.Opcode; - -import java.nio.ByteBuffer; -import java.util.Optional; -import java.util.OptionalInt; -import java.util.OptionalLong; -import java.util.function.IntPredicate; -import java.util.function.IntUnaryOperator; - -import static java.util.OptionalInt.empty; -import static java.util.OptionalInt.of; -import static org.testng.Assert.assertEquals; -import static java.net.http.internal.websocket.TestSupport.assertThrows; -import static java.net.http.internal.websocket.TestSupport.forEachBufferPartition; - -public class ReaderTest { - - private long cases, frames; - - @Test - void notMinimalEncoding01() { - ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES); - h.put((byte) 0b1000_0000).put((byte) 0b0111_1110).putChar((char) 125).flip(); - assertThrows(FailWebSocketException.class, - ".*(?i)minimally-encoded.*", - () -> new Frame.Reader().readFrame(h, new MockConsumer())); - } - - @Test - void notMinimalEncoding02() { - ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES); - h.put((byte) 0b1000_0000).put((byte) 0b0111_1111).putLong(125).flip(); - assertThrows(FailWebSocketException.class, - ".*(?i)minimally-encoded.*", - () -> new Frame.Reader().readFrame(h, new MockConsumer())); - } - - @Test - void notMinimalEncoding03() { - ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES); - h.put((byte) 0b1000_0000).put((byte) 0b0111_1111).putLong(65535).flip(); - assertThrows(FailWebSocketException.class, - ".*(?i)minimally-encoded.*", - () -> new Frame.Reader().readFrame(h, new MockConsumer())); - } - - @Test - public void negativePayload() { - ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES); - h.put((byte) 0b1000_0000).put((byte) 0b0111_1111).putLong(-2L).flip(); - assertThrows(FailWebSocketException.class, - ".*(?i)negative.*", - () -> new Frame.Reader().readFrame(h, new MockConsumer())); - } - - @Test - public void frameStart() { - final long[] payloads = {0, 126, 65536, Integer.MAX_VALUE + 1L}; - final OptionalInt[] masks = {empty(), of(-1), of(0), of(0xCAFEBABE), - of(Integer.MAX_VALUE), of(Integer.MIN_VALUE)}; - for (boolean fin : new boolean[]{true, false}) { - for (boolean rsv1 : new boolean[]{true, false}) { - for (boolean rsv2 : new boolean[]{true, false}) { - for (boolean rsv3 : new boolean[]{true, false}) { - for (Opcode opcode : Opcode.values()) { - for (long payloadLen : payloads) { - for (OptionalInt mask : masks) { - verifyFrameStart(fin, rsv1, rsv2, rsv3, opcode, payloadLen, mask); - } - } - } - } - } - } - } - System.out.println("Frames: " + frames + ", Total cases: " + cases); - } - - /* - * Tests whether or not the frame starts properly. - * That is, a header and the first invocation of payloadData (if any). - */ - private void verifyFrameStart(boolean fin, - boolean rsv1, - boolean rsv2, - boolean rsv3, - Opcode opcode, - long payloadLen, - OptionalInt mask) { - frames++; - Frame.HeaderWriter w = new Frame.HeaderWriter(); - ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES); - w.fin(fin).rsv1(rsv1).rsv2(rsv2).rsv3(rsv3).opcode(opcode).payloadLen(payloadLen); - mask.ifPresentOrElse(w::mask, w::noMask); - w.write(h); - h.flip(); - forEachBufferPartition(h, - buffers -> { - cases++; - Frame.Reader r = new Frame.Reader(); - MockConsumer c = new MockConsumer(); - for (ByteBuffer b : buffers) { - r.readFrame(b, c); - } - assertEquals(fin, c.fin()); - assertEquals(rsv1, c.rsv1()); - assertEquals(rsv2, c.rsv2()); - assertEquals(rsv3, c.rsv3()); - assertEquals(opcode, c.opcode()); - assertEquals(mask.isPresent(), c.mask()); - assertEquals(payloadLen, c.payloadLen()); - assertEquals(mask, c.maskingKey()); - assertEquals(payloadLen == 0, c.isEndFrame()); - }); - } - - /* - * Used to verify the order, the number of invocations as well as the - * arguments of each individual invocation to Frame.Consumer's methods. - */ - private static class MockConsumer implements Frame.Consumer { - - private int invocationOrder; - - private Optional fin = Optional.empty(); - private Optional rsv1 = Optional.empty(); - private Optional rsv2 = Optional.empty(); - private Optional rsv3 = Optional.empty(); - private Optional opcode = Optional.empty(); - private Optional mask = Optional.empty(); - private OptionalLong payloadLen = OptionalLong.empty(); - private OptionalInt maskingKey = OptionalInt.empty(); - - @Override - public void fin(boolean value) { - checkAndSetOrder(0, 1); - fin = Optional.of(value); - } - - @Override - public void rsv1(boolean value) { - checkAndSetOrder(1, 2); - rsv1 = Optional.of(value); - } - - @Override - public void rsv2(boolean value) { - checkAndSetOrder(2, 3); - rsv2 = Optional.of(value); - } - - @Override - public void rsv3(boolean value) { - checkAndSetOrder(3, 4); - rsv3 = Optional.of(value); - } - - @Override - public void opcode(Opcode value) { - checkAndSetOrder(4, 5); - opcode = Optional.of(value); - } - - @Override - public void mask(boolean value) { - checkAndSetOrder(5, 6); - mask = Optional.of(value); - } - - @Override - public void payloadLen(long value) { - checkAndSetOrder(p -> p == 5 || p == 6, n -> 7); - payloadLen = OptionalLong.of(value); - } - - @Override - public void maskingKey(int value) { - checkAndSetOrder(7, 8); - maskingKey = of(value); - } - - @Override - public void payloadData(ByteBuffer data) { - checkAndSetOrder(p -> p == 7 || p == 8, n -> 9); - assert payloadLen.isPresent(); - if (payloadLen.getAsLong() != 0 && !data.hasRemaining()) { - throw new TestSupport.AssertionFailedException("Artefact of reading"); - } - } - - @Override - public void endFrame() { - checkAndSetOrder(9, 10); - } - - boolean isEndFrame() { - return invocationOrder == 10; - } - - public boolean fin() { - return fin.get(); - } - - public boolean rsv1() { - return rsv1.get(); - } - - public boolean rsv2() { - return rsv2.get(); - } - - public boolean rsv3() { - return rsv3.get(); - } - - public Opcode opcode() { - return opcode.get(); - } - - public boolean mask() { - return mask.get(); - } - - public long payloadLen() { - return payloadLen.getAsLong(); - } - - public OptionalInt maskingKey() { - return maskingKey; - } - - private void checkAndSetOrder(int expectedValue, int newValue) { - checkAndSetOrder(p -> p == expectedValue, n -> newValue); - } - - private void checkAndSetOrder(IntPredicate expectedValue, - IntUnaryOperator newValue) { - if (!expectedValue.test(invocationOrder)) { - throw new TestSupport.AssertionFailedException( - expectedValue + " -> " + newValue); - } - invocationOrder = newValue.applyAsInt(invocationOrder); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/TestSupport.java --- a/test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/TestSupport.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,336 +0,0 @@ -/* - * 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. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package java.net.http.internal.websocket; - -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Stack; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.regex.Pattern; - -import static java.util.List.of; -import static java.util.Objects.requireNonNull; - -/* - * Auxiliary test infrastructure - */ -final class TestSupport { - - private TestSupport() { } - - static Iterator cartesianIterator(List a, - List b, - F2 f2) { - @SuppressWarnings("unchecked") - F t = p -> f2.apply((A) p[0], (B) p[1]); - return cartesianIterator(of(a, b), t); - } - - static Iterator cartesianIterator(List a, - List b, - List c, - F3 f3) { - @SuppressWarnings("unchecked") - F t = p -> f3.apply((A) p[0], (B) p[1], (C) p[2]); - return cartesianIterator(of(a, b, c), t); - } - - static Iterator cartesianIterator(List a, - List b, - List c, - List d, - F4 f4) { - @SuppressWarnings("unchecked") - F t = p -> f4.apply((A) p[0], (B) p[1], (C) p[2], (D) p[3]); - return cartesianIterator(of(a, b, c, d), t); - } - - static Iterator cartesianIterator(List a, - List b, - List c, - List d, - List e, - F5 f5) { - @SuppressWarnings("unchecked") - F t = p -> f5.apply((A) p[0], (B) p[1], (C) p[2], (D) p[3], (E) p[4]); - return cartesianIterator(of(a, b, c, d, e), t); - } - - static Iterator cartesianIterator(List> params, - F function) { - if (params.isEmpty()) { - return Collections.emptyIterator(); - } - for (List l : params) { - if (l.isEmpty()) { - return Collections.emptyIterator(); - } - } - // Assertion: if we are still here, there is at least a single element - // in the product - return new Iterator<>() { - - private final int arity = params.size(); - private final int[] coordinates = new int[arity]; - private boolean hasNext = true; - - @Override - public boolean hasNext() { - return hasNext; - } - - @Override - public R next() { - if (!hasNext) { - throw new NoSuchElementException(); - } - Object[] array = new Object[arity]; - for (int i = 0; i < arity; i++) { - array[i] = params.get(i).get(coordinates[i]); - } - int p = arity - 1; - while (p >= 0 && coordinates[p] == params.get(p).size() - 1) { - p--; - } - if (p < 0) { - hasNext = false; - } else { - coordinates[p]++; - for (int i = p + 1; i < arity; i++) { - coordinates[i] = 0; - } - } - return function.apply(array); - } - }; - } - - @FunctionalInterface - public interface F1 { - R apply(A a); - } - - @FunctionalInterface - public interface F2 { - R apply(A a, B b); - } - - @FunctionalInterface - public interface F3 { - R apply(A a, B b, C c); - } - - @FunctionalInterface - public interface F4 { - R apply(A a, B b, C c, D d); - } - - @FunctionalInterface - public interface F5 { - R apply(A a, B b, C c, D d, E e); - } - - @FunctionalInterface - public interface F { - R apply(Object[] args); - } - - static Iterator iteratorOf1(T element) { - return List.of(element).iterator(); - } - - @SafeVarargs - static Iterator iteratorOf(T... elements) { - return List.of(elements).iterator(); - } - - static Iterator limit(int maxElements, Iterator elements) { - return new Iterator<>() { - - int count = maxElements; - - @Override - public boolean hasNext() { - return count > 0 && elements.hasNext(); - } - - @Override - public T next() { - if (!hasNext()) { - throw new NoSuchElementException(); - } - count--; - return elements.next(); - } - }; - } - - static ByteBuffer fullCopy(ByteBuffer src) { - ByteBuffer copy = ByteBuffer.allocate(src.capacity()); - int p = src.position(); - int l = src.limit(); - src.clear(); - copy.put(src).position(p).limit(l); - src.position(p).limit(l); - return copy; - } - - static void forEachBufferPartition(ByteBuffer src, - Consumer> action) { - forEachPartition(src.remaining(), - (lengths) -> { - int end = src.position(); - List buffers = new LinkedList<>(); - for (int len : lengths) { - ByteBuffer d = src.duplicate(); - d.position(end); - d.limit(end + len); - end += len; - buffers.add(d); - } - action.accept(buffers); - }); - } - - private static void forEachPartition(int n, - Consumer> action) { - forEachPartition(n, new Stack<>(), action); - } - - private static void forEachPartition(int n, - Stack path, - Consumer> action) { - if (n == 0) { - action.accept(path); - } else { - for (int i = 1; i <= n; i++) { - path.push(i); - forEachPartition(n - i, path, action); - path.pop(); - } - } - } - - static void forEachPermutation(int n, Consumer c) { - int[] a = new int[n]; - for (int i = 0; i < n; i++) { - a[i] = i; - } - permutations(0, a, c); - } - - private static void permutations(int i, int[] a, Consumer c) { - if (i == a.length) { - c.accept(Arrays.copyOf(a, a.length)); - return; - } - for (int j = i; j < a.length; j++) { - swap(a, i, j); - permutations(i + 1, a, c); - swap(a, i, j); - } - } - - private static void swap(int[] a, int i, int j) { - int x = a[i]; - a[i] = a[j]; - a[j] = x; - } - - public static T assertThrows(Class clazz, - ThrowingProcedure code) { - @SuppressWarnings("unchecked") - T t = (T) assertThrows(clazz::isInstance, code); - return t; - } - - /* - * The rationale behind asking for a regex is to not pollute variable names - * space in the scope of assertion: if it's something as simple as checking - * a message, we can do it inside - */ - @SuppressWarnings("unchecked") - static T assertThrows(Class clazz, - String messageRegex, - ThrowingProcedure code) { - requireNonNull(messageRegex, "messagePattern"); - Predicate p = e -> clazz.isInstance(e) - && Pattern.matches(messageRegex, e.getMessage()); - return (T) assertThrows(p, code); - } - - static Throwable assertThrows(Predicate predicate, - ThrowingProcedure code) { - requireNonNull(predicate, "predicate"); - requireNonNull(code, "code"); - Throwable caught = null; - try { - code.run(); - } catch (Throwable t) { - caught = t; - } - if (caught == null) { - throw new AssertionFailedException("No exception was thrown"); - } - if (predicate.test(caught)) { - System.out.println("Got expected exception: " + caught); - return caught; - } - throw new AssertionFailedException("Caught exception didn't match the predicate", caught); - } - - /* - * Blocking assertion, waits for completion - */ - static Throwable assertCompletesExceptionally(Class clazz, - CompletionStage stage) { - CompletableFuture cf = - CompletableFuture.completedFuture(null).thenCompose(x -> stage); - return assertThrows(t -> clazz.isInstance(t.getCause()), cf::get); - } - - interface ThrowingProcedure { - void run() throws Throwable; - } - - static final class AssertionFailedException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - AssertionFailedException(String message) { - super(message); - } - - AssertionFailedException(String message, Throwable cause) { - super(message, cause); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/WebSocketImplTest.java --- a/test/jdk/java/net/httpclient/websocket/java.net.http/java/net/http/internal/websocket/WebSocketImplTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,453 +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 java.net.http.internal.websocket; - -import java.net.http.WebSocket; -import org.testng.annotations.Test; - -import java.net.URI; -import java.nio.ByteBuffer; -import java.util.Collection; -import java.util.List; -import java.util.Random; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import static java.net.http.WebSocket.MessagePart.FIRST; -import static java.net.http.WebSocket.MessagePart.LAST; -import static java.net.http.WebSocket.MessagePart.PART; -import static java.net.http.WebSocket.MessagePart.WHOLE; -import static java.net.http.WebSocket.NORMAL_CLOSURE; -import static java.net.http.internal.websocket.MockListener.Invocation.onClose; -import static java.net.http.internal.websocket.MockListener.Invocation.onError; -import static java.net.http.internal.websocket.MockListener.Invocation.onOpen; -import static java.net.http.internal.websocket.MockListener.Invocation.onPing; -import static java.net.http.internal.websocket.MockListener.Invocation.onPong; -import static java.net.http.internal.websocket.MockListener.Invocation.onText; -import static java.net.http.internal.websocket.MockTransport.onClose; -import static java.net.http.internal.websocket.MockTransport.onPing; -import static java.net.http.internal.websocket.MockTransport.onPong; -import static java.net.http.internal.websocket.MockTransport.onText; -import static java.net.http.internal.websocket.TestSupport.assertCompletesExceptionally; -import static org.testng.Assert.assertEquals; - -/* - * Formatting in this file may seem strange: - * - * ( - * ( ...) - * ... - * ) - * ... - * - * However there is a rationale behind it. Sometimes the level of argument - * nesting is high, which makes it hard to manage parentheses. - */ -public class WebSocketImplTest { - - // TODO: request in onClose/onError - // TODO: throw exception in onClose/onError - // TODO: exception is thrown from request() - // TODO: repeated sendClose complete normally - // TODO: default Close message is sent if IAE is thrown from sendClose - - @Test - public void testNonPositiveRequest() throws Exception { - MockListener listener = new MockListener(Long.MAX_VALUE) { - @Override - protected void onOpen0(WebSocket webSocket) { - webSocket.request(0); - } - }; - WebSocket ws = newInstance(listener, List.of(now(onText("1", WHOLE)))); - listener.onCloseOrOnErrorCalled().get(10, TimeUnit.SECONDS); - List invocations = listener.invocations(); - assertEquals( - invocations, - List.of( - onOpen(ws), - onError(ws, IllegalArgumentException.class) - ) - ); - } - - @Test - public void testText1() throws Exception { - MockListener listener = new MockListener(Long.MAX_VALUE); - WebSocket ws = newInstance( - listener, - List.of( - now(onText("1", FIRST)), - now(onText("2", PART)), - now(onText("3", LAST)), - now(onClose(NORMAL_CLOSURE, "no reason")) - ) - ); - listener.onCloseOrOnErrorCalled().get(10, TimeUnit.SECONDS); - List invocations = listener.invocations(); - assertEquals( - invocations, - List.of( - onOpen(ws), - onText(ws, "1", FIRST), - onText(ws, "2", PART), - onText(ws, "3", LAST), - onClose(ws, NORMAL_CLOSURE, "no reason") - ) - ); - } - - @Test - public void testText2() throws Exception { - MockListener listener = new MockListener(Long.MAX_VALUE); - WebSocket ws = newInstance( - listener, - List.of( - now(onText("1", FIRST)), - seconds(1, onText("2", PART)), - now(onText("3", LAST)), - seconds(1, onClose(NORMAL_CLOSURE, "no reason")) - ) - ); - listener.onCloseOrOnErrorCalled().get(10, TimeUnit.SECONDS); - List invocations = listener.invocations(); - assertEquals( - invocations, - List.of( - onOpen(ws), - onText(ws, "1", FIRST), - onText(ws, "2", PART), - onText(ws, "3", LAST), - onClose(ws, NORMAL_CLOSURE, "no reason") - ) - ); - } - - @Test - public void testTextIntermixedWithPongs() throws Exception { - MockListener listener = new MockListener(Long.MAX_VALUE); - WebSocket ws = newInstance( - listener, - List.of( - now(onText("1", FIRST)), - now(onText("2", PART)), - now(onPong(ByteBuffer.allocate(16))), - seconds(1, onPong(ByteBuffer.allocate(32))), - now(onText("3", LAST)), - now(onPong(ByteBuffer.allocate(64))), - now(onClose(NORMAL_CLOSURE, "no reason")) - ) - ); - listener.onCloseOrOnErrorCalled().get(10, TimeUnit.SECONDS); - List invocations = listener.invocations(); - assertEquals( - invocations, - List.of( - onOpen(ws), - onText(ws, "1", FIRST), - onText(ws, "2", PART), - onPong(ws, ByteBuffer.allocate(16)), - onPong(ws, ByteBuffer.allocate(32)), - onText(ws, "3", LAST), - onPong(ws, ByteBuffer.allocate(64)), - onClose(ws, NORMAL_CLOSURE, "no reason") - ) - ); - } - - @Test - public void testTextIntermixedWithPings() throws Exception { - MockListener listener = new MockListener(Long.MAX_VALUE); - WebSocket ws = newInstance( - listener, - List.of( - now(onText("1", FIRST)), - now(onText("2", PART)), - now(onPing(ByteBuffer.allocate(16))), - seconds(1, onPing(ByteBuffer.allocate(32))), - now(onText("3", LAST)), - now(onPing(ByteBuffer.allocate(64))), - now(onClose(NORMAL_CLOSURE, "no reason")) - ) - ); - listener.onCloseOrOnErrorCalled().get(10, TimeUnit.SECONDS); - List invocations = listener.invocations(); - assertEquals( - invocations, - List.of( - onOpen(ws), - onText(ws, "1", FIRST), - onText(ws, "2", PART), - onPing(ws, ByteBuffer.allocate(16)), - onPing(ws, ByteBuffer.allocate(32)), - onText(ws, "3", LAST), - onPing(ws, ByteBuffer.allocate(64)), - onClose(ws, NORMAL_CLOSURE, "no reason")) - ); - } - - // Tease out "java.lang.IllegalStateException: Send pending" due to possible - // race between sending a message and replenishing the permit - @Test - public void testManyTextMessages() { - WebSocketImpl ws = newInstance( - new MockListener(1), - new TransportFactory() { - @Override - public Transport createTransport(Supplier sendResultSupplier, - MessageStreamConsumer consumer) { - - final Random r = new Random(); - - return new MockTransport<>(sendResultSupplier, consumer) { - @Override - protected CompletableFuture defaultSend() { - return millis(r.nextInt(100), result()); - } - }; - } - }); - int NUM_MESSAGES = 512; - CompletableFuture current = CompletableFuture.completedFuture(ws); - for (int i = 0; i < NUM_MESSAGES; i++) { - current = current.thenCompose(w -> w.sendText(" ", true)); - } - current.join(); - MockTransport transport = (MockTransport) ws.transport(); - assertEquals(transport.invocations().size(), NUM_MESSAGES); - } - - @Test - public void testManyBinaryMessages() { - WebSocketImpl ws = newInstance( - new MockListener(1), - new TransportFactory() { - @Override - public Transport createTransport(Supplier sendResultSupplier, - MessageStreamConsumer consumer) { - - final Random r = new Random(); - - return new MockTransport<>(sendResultSupplier, consumer) { - @Override - protected CompletableFuture defaultSend() { - return millis(r.nextInt(150), result()); - } - }; - } - }); - CompletableFuture start = new CompletableFuture<>(); - - int NUM_MESSAGES = 512; - CompletableFuture current = start; - for (int i = 0; i < NUM_MESSAGES; i++) { - current = current.thenComposeAsync(w -> w.sendBinary(ByteBuffer.allocate(1), true)); - } - - start.completeAsync(() -> ws); - current.join(); - - MockTransport transport = (MockTransport) ws.transport(); - assertEquals(transport.invocations().size(), NUM_MESSAGES); - } - - - @Test - public void sendTextImmediately() { - WebSocketImpl ws = newInstance( - new MockListener(1), - new TransportFactory() { - @Override - public Transport createTransport(Supplier sendResultSupplier, - MessageStreamConsumer consumer) { - return new MockTransport<>(sendResultSupplier, consumer); - } - }); - CompletableFuture.completedFuture(ws) - .thenCompose(w -> w.sendText("1", true)) - .thenCompose(w -> w.sendText("2", true)) - .thenCompose(w -> w.sendText("3", true)) - .join(); - MockTransport transport = (MockTransport) ws.transport(); - assertEquals(transport.invocations().size(), 3); - } - - @Test - public void sendTextWithDelay() { - MockListener listener = new MockListener(1); - WebSocketImpl ws = newInstance( - listener, - new TransportFactory() { - @Override - public Transport createTransport(Supplier sendResultSupplier, - MessageStreamConsumer consumer) { - return new MockTransport<>(sendResultSupplier, consumer) { - @Override - protected CompletableFuture defaultSend() { - return seconds(1, result()); - } - }; - } - }); - CompletableFuture.completedFuture(ws) - .thenCompose(w -> w.sendText("1", true)) - .thenCompose(w -> w.sendText("2", true)) - .thenCompose(w -> w.sendText("3", true)) - .join(); - assertEquals(listener.invocations(), List.of(onOpen(ws))); - MockTransport transport = (MockTransport) ws.transport(); - assertEquals(transport.invocations().size(), 3); - } - - @Test - public void sendTextMixedDelay() { - MockListener listener = new MockListener(1); - WebSocketImpl ws = newInstance( - listener, - new TransportFactory() { - - final Random r = new Random(); - - @Override - public Transport createTransport(Supplier sendResultSupplier, - MessageStreamConsumer consumer) { - return new MockTransport<>(sendResultSupplier, consumer) { - @Override - protected CompletableFuture defaultSend() { - return r.nextBoolean() - ? seconds(1, result()) - : now(result()); - } - }; - } - }); - CompletableFuture.completedFuture(ws) - .thenCompose(w -> w.sendText("1", true)) - .thenCompose(w -> w.sendText("2", true)) - .thenCompose(w -> w.sendText("3", true)) - .thenCompose(w -> w.sendText("4", true)) - .thenCompose(w -> w.sendText("5", true)) - .thenCompose(w -> w.sendText("6", true)) - .thenCompose(w -> w.sendText("7", true)) - .thenCompose(w -> w.sendText("8", true)) - .thenCompose(w -> w.sendText("9", true)) - .join(); - assertEquals(listener.invocations(), List.of(onOpen(ws))); - MockTransport transport = (MockTransport) ws.transport(); - assertEquals(transport.invocations().size(), 9); - } - - @Test(enabled = false) // temporarily disabled - public void sendControlMessagesConcurrently() { - MockListener listener = new MockListener(1); - - CompletableFuture first = new CompletableFuture<>(); // barrier - - WebSocketImpl ws = newInstance( - listener, - new TransportFactory() { - - final AtomicInteger i = new AtomicInteger(); - - @Override - public Transport createTransport(Supplier sendResultSupplier, - MessageStreamConsumer consumer) { - return new MockTransport<>(sendResultSupplier, consumer) { - @Override - protected CompletableFuture defaultSend() { - if (i.incrementAndGet() == 1) { - return first.thenApply(o -> result()); - } else { - return now(result()); - } - } - }; - } - }); - - CompletableFuture cf1 = ws.sendPing(ByteBuffer.allocate(0)); - CompletableFuture cf2 = ws.sendPong(ByteBuffer.allocate(0)); - CompletableFuture cf3 = ws.sendClose(NORMAL_CLOSURE, ""); - CompletableFuture cf4 = ws.sendClose(NORMAL_CLOSURE, ""); - CompletableFuture cf5 = ws.sendPing(ByteBuffer.allocate(0)); - CompletableFuture cf6 = ws.sendPong(ByteBuffer.allocate(0)); - - first.complete(null); - // Don't care about exceptional completion, only that all of them have - // completed - CompletableFuture.allOf(cf1, cf2, cf3, cf4, cf5, cf6) - .handle((v, e) -> null).join(); - - cf3.join(); /* Check that sendClose has completed normally */ - cf4.join(); /* Check that repeated sendClose has completed normally */ - assertCompletesExceptionally(IllegalStateException.class, cf5); - assertCompletesExceptionally(IllegalStateException.class, cf6); - - assertEquals(listener.invocations(), List.of(onOpen(ws))); - MockTransport transport = (MockTransport) ws.transport(); - assertEquals(transport.invocations().size(), 3); // 6 minus 3 that were not accepted - } - - private static CompletableFuture seconds(long val, T result) { - return new CompletableFuture() - .completeOnTimeout(result, val, TimeUnit.SECONDS); - } - - private static CompletableFuture millis(long val, T result) { - return new CompletableFuture() - .completeOnTimeout(result, val, TimeUnit.MILLISECONDS); - } - - private static CompletableFuture now(T result) { - return CompletableFuture.completedFuture(result); - } - - private static WebSocketImpl newInstance( - WebSocket.Listener listener, - Collection>> input) { - TransportFactory factory = new TransportFactory() { - @Override - public Transport createTransport(Supplier sendResultSupplier, - MessageStreamConsumer consumer) { - return new MockTransport(sendResultSupplier, consumer) { - @Override - protected Collection>> receive() { - return input; - } - }; - } - }; - return newInstance(listener, factory); - } - - private static WebSocketImpl newInstance(WebSocket.Listener listener, - TransportFactory factory) { - URI uri = URI.create("ws://localhost"); - String subprotocol = ""; - return WebSocketImpl.newInstance(uri, subprotocol, listener, factory); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/BuildingWebSocketTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/BuildingWebSocketTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,225 @@ +/* + * 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. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static jdk.internal.net.http.websocket.TestSupport.assertCompletesExceptionally; +import static jdk.internal.net.http.websocket.TestSupport.assertThrows; + +/* + * In some places in this class a new String is created out of a string literal. + * The idea is to make sure the code under test relies on something better than + * the reference equality ( == ) for string equality checks. + */ +public class BuildingWebSocketTest { + + private final static URI VALID_URI = URI.create("ws://websocket.example.com"); + + @Test + public void nullArguments() { + HttpClient c = HttpClient.newHttpClient(); + + assertThrows(NullPointerException.class, + () -> c.newWebSocketBuilder() + .buildAsync(null, listener())); + assertThrows(NullPointerException.class, + () -> c.newWebSocketBuilder() + .buildAsync(VALID_URI, null)); + assertThrows(NullPointerException.class, + () -> c.newWebSocketBuilder() + .buildAsync(null, null)); + assertThrows(NullPointerException.class, + () -> c.newWebSocketBuilder() + .header(null, "value")); + assertThrows(NullPointerException.class, + () -> c.newWebSocketBuilder() + .header("name", null)); + assertThrows(NullPointerException.class, + () -> c.newWebSocketBuilder() + .header(null, null)); + assertThrows(NullPointerException.class, + () -> c.newWebSocketBuilder() + .subprotocols(null)); + assertThrows(NullPointerException.class, + () -> c.newWebSocketBuilder() + .subprotocols(null, "sub2.example.com")); + assertThrows(NullPointerException.class, + () -> c.newWebSocketBuilder() + .subprotocols("sub1.example.com", (String) null)); + assertThrows(NullPointerException.class, + () -> c.newWebSocketBuilder() + .subprotocols("sub1.example.com", (String[]) null)); + assertThrows(NullPointerException.class, + () -> c.newWebSocketBuilder() + .subprotocols("sub1.example.com", "sub2.example.com", null)); + assertThrows(NullPointerException.class, + () -> c.newWebSocketBuilder() + .subprotocols("sub1.example.com", null, "sub3.example.com")); + assertThrows(NullPointerException.class, + () -> c.newWebSocketBuilder() + .connectTimeout(null)); + } + + @Test(dataProvider = "badURIs") + void illegalURI(URI uri) { + WebSocket.Builder b = HttpClient.newHttpClient().newWebSocketBuilder(); + assertCompletesExceptionally(IllegalArgumentException.class, + b.buildAsync(uri, listener())); + } + + @Test + public void illegalHeaders() { + List headers = + List.of("Sec-WebSocket-Accept", + "Sec-WebSocket-Extensions", + "Sec-WebSocket-Key", + "Sec-WebSocket-Protocol", + "Sec-WebSocket-Version") + .stream() + .flatMap(s -> Stream.of(s, new String(s))) // a string and a copy of it + .collect(Collectors.toList()); + + Function> f = + header -> HttpClient.newHttpClient() + .newWebSocketBuilder() + .header(header, "value") + .buildAsync(VALID_URI, listener()); + + headers.forEach(h -> assertCompletesExceptionally(IllegalArgumentException.class, f.apply(h))); + } + + // TODO: test for bad syntax headers + // TODO: test for overwrites (subprotocols) and additions (headers) + + @Test(dataProvider = "badSubprotocols") + public void illegalSubprotocolsSyntax(String s) { + WebSocket.Builder b = HttpClient.newHttpClient() + .newWebSocketBuilder() + .subprotocols(s); + assertCompletesExceptionally(IllegalArgumentException.class, + b.buildAsync(VALID_URI, listener())); + } + + @Test(dataProvider = "duplicatingSubprotocols") + public void illegalSubprotocolsDuplicates(String mostPreferred, + String[] lesserPreferred) { + WebSocket.Builder b = HttpClient.newHttpClient() + .newWebSocketBuilder() + .subprotocols(mostPreferred, lesserPreferred); + assertCompletesExceptionally(IllegalArgumentException.class, + b.buildAsync(VALID_URI, listener())); + } + + @Test(dataProvider = "badConnectTimeouts") + public void illegalConnectTimeout(Duration d) { + WebSocket.Builder b = HttpClient.newHttpClient() + .newWebSocketBuilder() + .connectTimeout(d); + assertCompletesExceptionally(IllegalArgumentException.class, + b.buildAsync(VALID_URI, listener())); + } + + @DataProvider + public Object[][] badURIs() { + return new Object[][]{ + {URI.create("http://example.com")}, + {URI.create("ftp://example.com")}, + {URI.create("wss://websocket.example.com/hello#fragment")}, + {URI.create("ws://websocket.example.com/hello#fragment")}, + }; + } + + @DataProvider + public Object[][] badConnectTimeouts() { + return new Object[][]{ + {Duration.ofDays ( 0)}, + {Duration.ofDays (-1)}, + {Duration.ofHours ( 0)}, + {Duration.ofHours (-1)}, + {Duration.ofMinutes( 0)}, + {Duration.ofMinutes(-1)}, + {Duration.ofSeconds( 0)}, + {Duration.ofSeconds(-1)}, + {Duration.ofMillis ( 0)}, + {Duration.ofMillis (-1)}, + {Duration.ofNanos ( 0)}, + {Duration.ofNanos (-1)}, + {Duration.ZERO}, + }; + } + + // https://tools.ietf.org/html/rfc7230#section-3.2.6 + // https://tools.ietf.org/html/rfc20 + @DataProvider + public static Object[][] badSubprotocols() { + return new Object[][]{ + {""}, + {new String("")}, + {"round-brackets("}, + {"round-brackets)"}, + {"comma,"}, + {"slash/"}, + {"colon:"}, + {"semicolon;"}, + {"angle-brackets<"}, + {"angle-brackets>"}, + {"equals="}, + {"question-mark?"}, + {"at@"}, + {"brackets["}, + {"backslash\\"}, + {"brackets]"}, + {"curly-brackets{"}, + {"curly-brackets}"}, + {"space "}, + {"non-printable-character " + Character.toString((char) 31)}, + {"non-printable-character " + Character.toString((char) 127)}, + }; + } + + @DataProvider + public static Object[][] duplicatingSubprotocols() { + return new Object[][]{ + {"a.b.c", new String[]{"a.b.c"}}, + {"a.b.c", new String[]{"x.y.z", "p.q.r", "x.y.z"}}, + {"a.b.c", new String[]{new String("a.b.c")}}, + }; + } + + private static WebSocket.Listener listener() { + return new WebSocket.Listener() { }; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/HeaderWriterTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/HeaderWriterTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,115 @@ +/* + * 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. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import org.testng.annotations.Test; +import jdk.internal.net.http.websocket.Frame.HeaderWriter; +import jdk.internal.net.http.websocket.Frame.Opcode; + +import java.nio.ByteBuffer; +import java.util.OptionalInt; + +import static java.util.OptionalInt.empty; +import static java.util.OptionalInt.of; +import static org.testng.Assert.assertEquals; +import static jdk.internal.net.http.websocket.TestSupport.assertThrows; +import static jdk.internal.net.http.websocket.TestSupport.forEachPermutation; + +public class HeaderWriterTest { + + private long cases, frames; + + @Test + public void negativePayload() { + System.out.println("testing negative payload"); + HeaderWriter w = new HeaderWriter(); + assertThrows(IllegalArgumentException.class, + ".*(?i)negative.*", + () -> w.payloadLen(-1)); + } + + @Test + public void test() { + System.out.println("testing regular payloads"); + final long[] payloads = {0, 126, 65536, Integer.MAX_VALUE + 1L}; + final OptionalInt[] masks = {empty(), of(-1), of(0), of(0xCAFEBABE), + of(Integer.MAX_VALUE), of(Integer.MIN_VALUE)}; + for (boolean fin : new boolean[]{true, false}) { + for (boolean rsv1 : new boolean[]{true, false}) { + for (boolean rsv2 : new boolean[]{true, false}) { + for (boolean rsv3 : new boolean[]{true, false}) { + for (Opcode opcode : Opcode.values()) { + for (long payloadLen : payloads) { + for (OptionalInt mask : masks) { + verify(fin, rsv1, rsv2, rsv3, opcode, payloadLen, mask); + } + } + } + } + } + } + } + System.out.println("Frames: " + frames + ", Total cases: " + cases); + } + + private void verify(boolean fin, + boolean rsv1, + boolean rsv2, + boolean rsv3, + Opcode opcode, + long payloadLen, + OptionalInt mask) { + frames++; + HeaderWriter writer = new HeaderWriter(); + ByteBuffer expected = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES); + writer.fin(fin).rsv1(rsv1).rsv2(rsv2).rsv3(rsv3).opcode(opcode).payloadLen(payloadLen); + mask.ifPresentOrElse(writer::mask, writer::noMask); + writer.write(expected); + expected.flip(); + verifyPermutations(expected, writer, + () -> writer.fin(fin), + () -> writer.rsv1(rsv1), + () -> writer.rsv2(rsv2), + () -> writer.rsv3(rsv3), + () -> writer.opcode(opcode), + () -> writer.payloadLen(payloadLen), + () -> mask.ifPresentOrElse(writer::mask, writer::noMask)); + } + + private void verifyPermutations(ByteBuffer expected, + HeaderWriter writer, + Runnable... actions) { + forEachPermutation(actions.length, + order -> { + cases++; + for (int i : order) { + actions[i].run(); + } + ByteBuffer actual = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES + 2); + writer.write(actual); + actual.flip(); + assertEquals(actual, expected); + }); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/MaskerTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/MaskerTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,158 @@ +/* + * 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. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.stream.IntStream; + +import static org.testng.Assert.assertEquals; +import static jdk.internal.net.http.websocket.Frame.Masker.transferMasking; +import static jdk.internal.net.http.websocket.TestSupport.forEachBufferPartition; +import static jdk.internal.net.http.websocket.TestSupport.fullCopy; + +public class MaskerTest { + + private static final SecureRandom random = new SecureRandom(); + + @Test + public void stateless() { + IntStream.iterate(0, i -> i + 1).limit(125).boxed() + .forEach(r -> { + int m = random.nextInt(); + ByteBuffer src = createSourceBuffer(r); + ByteBuffer dst = createDestinationBuffer(r); + verify(src, dst, maskArray(m), 0, + () -> transferMasking(src, dst, m)); + }); + } + + /* + * Stateful masker to make sure setting a mask resets the state as if a new + * Masker instance is created each time + */ + private final Frame.Masker masker = new Frame.Masker(); + + @Test + public void stateful0() { + // This size (17 = 8 + 8 + 1) should test all the stages + // (galloping/slow) of masking good enough + int N = 17; + ByteBuffer src = createSourceBuffer(N); + ByteBuffer dst = createDestinationBuffer(N); + int mask = random.nextInt(); + forEachBufferPartition(src, + buffers -> { + int offset = 0; + masker.mask(mask); + int[] maskBytes = maskArray(mask); + for (ByteBuffer s : buffers) { + offset = verify(s, dst, maskBytes, offset, + () -> masker.transferMasking(s, dst)); + } + }); + } + + @Test + public void stateful1() { + int m = random.nextInt(); + masker.mask(m); + ByteBuffer src = ByteBuffer.allocate(0); + ByteBuffer dst = ByteBuffer.allocate(16); + verify(src, dst, maskArray(m), 0, + () -> masker.transferMasking(src, dst)); + } + + private static int verify(ByteBuffer src, + ByteBuffer dst, + int[] maskBytes, + int offset, + Runnable masking) { + ByteBuffer srcCopy = fullCopy(src); + ByteBuffer dstCopy = fullCopy(dst); + masking.run(); + int srcRemaining = srcCopy.remaining(); + int dstRemaining = dstCopy.remaining(); + int masked = Math.min(srcRemaining, dstRemaining); + // 1. position check + assertEquals(src.position(), srcCopy.position() + masked); + assertEquals(dst.position(), dstCopy.position() + masked); + // 2. masking check + src.position(srcCopy.position()); + dst.position(dstCopy.position()); + for (; src.hasRemaining() && dst.hasRemaining(); + offset = (offset + 1) & 3) { + assertEquals(dst.get(), src.get() ^ maskBytes[offset]); + } + // 3. corruption check + // 3.1 src contents haven't changed + int srcPosition = src.position(); + int srcLimit = src.limit(); + src.clear(); + srcCopy.clear(); + assertEquals(src, srcCopy); + src.limit(srcLimit).position(srcPosition); // restore src + // 3.2 dst leading and trailing regions' contents haven't changed + int dstPosition = dst.position(); + int dstInitialPosition = dstCopy.position(); + int dstLimit = dst.limit(); + // leading + dst.position(0).limit(dstInitialPosition); + dstCopy.position(0).limit(dstInitialPosition); + assertEquals(dst, dstCopy); + // trailing + dst.limit(dst.capacity()).position(dstLimit); + dstCopy.limit(dst.capacity()).position(dstLimit); + assertEquals(dst, dstCopy); + // restore dst + dst.position(dstPosition).limit(dstLimit); + return offset; + } + + private static ByteBuffer createSourceBuffer(int remaining) { + int leading = random.nextInt(4); + int trailing = random.nextInt(4); + byte[] bytes = new byte[leading + remaining + trailing]; + random.nextBytes(bytes); + return ByteBuffer.wrap(bytes).position(leading).limit(leading + remaining); + } + + private static ByteBuffer createDestinationBuffer(int remaining) { + int leading = random.nextInt(4); + int trailing = random.nextInt(4); + return ByteBuffer.allocate(leading + remaining + trailing) + .position(leading).limit(leading + remaining); + } + + private static int[] maskArray(int mask) { + return new int[]{ + (byte) (mask >>> 24), + (byte) (mask >>> 16), + (byte) (mask >>> 8), + (byte) (mask >>> 0) + }; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/MockListener.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/MockListener.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,402 @@ +/* + * 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.internal.net.http.websocket; + +import java.net.http.WebSocket; +import java.net.http.WebSocket.MessagePart; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import static jdk.internal.net.http.websocket.TestSupport.fullCopy; + +public class MockListener implements WebSocket.Listener { + + private final long bufferSize; + private long count; + private final List invocations = new ArrayList<>(); + private final CompletableFuture lastCall = new CompletableFuture<>(); + + /* + * Typical buffer sizes: 1, n, Long.MAX_VALUE + */ + public MockListener(long bufferSize) { + if (bufferSize < 1) { + throw new IllegalArgumentException(); + } + this.bufferSize = bufferSize; + } + + @Override + public void onOpen(WebSocket webSocket) { + System.out.printf("onOpen(%s)%n", webSocket); + invocations.add(new OnOpen(webSocket)); + onOpen0(webSocket); + } + + protected void onOpen0(WebSocket webSocket) { + replenish(webSocket); + } + + @Override + public CompletionStage onText(WebSocket webSocket, + CharSequence message, + MessagePart part) { + System.out.printf("onText(%s, %s, %s)%n", webSocket, message, part); + invocations.add(new OnText(webSocket, message.toString(), part)); + return onText0(webSocket, message, part); + } + + protected CompletionStage onText0(WebSocket webSocket, + CharSequence message, + MessagePart part) { + replenish(webSocket); + return null; + } + + @Override + public CompletionStage onBinary(WebSocket webSocket, + ByteBuffer message, + MessagePart part) { + System.out.printf("onBinary(%s, %s, %s)%n", webSocket, message, part); + invocations.add(new OnBinary(webSocket, fullCopy(message), part)); + return onBinary0(webSocket, message, part); + } + + protected CompletionStage onBinary0(WebSocket webSocket, + ByteBuffer message, + MessagePart part) { + replenish(webSocket); + return null; + } + + @Override + public CompletionStage onPing(WebSocket webSocket, ByteBuffer message) { + System.out.printf("onPing(%s, %s)%n", webSocket, message); + invocations.add(new OnPing(webSocket, fullCopy(message))); + return onPing0(webSocket, message); + } + + protected CompletionStage onPing0(WebSocket webSocket, ByteBuffer message) { + replenish(webSocket); + return null; + } + + @Override + public CompletionStage onPong(WebSocket webSocket, ByteBuffer message) { + System.out.printf("onPong(%s, %s)%n", webSocket, message); + invocations.add(new OnPong(webSocket, fullCopy(message))); + return onPong0(webSocket, message); + } + + protected CompletionStage onPong0(WebSocket webSocket, ByteBuffer message) { + replenish(webSocket); + return null; + } + + @Override + public CompletionStage onClose(WebSocket webSocket, + int statusCode, + String reason) { + System.out.printf("onClose(%s, %s, %s)%n", webSocket, statusCode, reason); + invocations.add(new OnClose(webSocket, statusCode, reason)); + lastCall.complete(null); + return null; + } + + @Override + public void onError(WebSocket webSocket, Throwable error) { + System.out.printf("onError(%s, %s)%n", webSocket, error); + invocations.add(new OnError(webSocket, error == null ? null : error.getClass())); + lastCall.complete(null); + } + + public CompletableFuture onCloseOrOnErrorCalled() { + return lastCall.copy(); + } + + protected void replenish(WebSocket webSocket) { + if (--count <= 0) { + count = bufferSize - bufferSize / 2; + } + webSocket.request(count); + } + + public List invocations() { + return new ArrayList<>(invocations); + } + + public abstract static class Invocation { + + public static OnOpen onOpen(WebSocket webSocket) { + return new OnOpen(webSocket); + } + + public static OnText onText(WebSocket webSocket, + String text, + MessagePart part) { + return new OnText(webSocket, text, part); + } + + public static OnBinary onBinary(WebSocket webSocket, + ByteBuffer data, + MessagePart part) { + return new OnBinary(webSocket, data, part); + } + + public static OnPing onPing(WebSocket webSocket, + ByteBuffer data) { + return new OnPing(webSocket, data); + } + + public static OnPong onPong(WebSocket webSocket, + ByteBuffer data) { + return new OnPong(webSocket, data); + } + + public static OnClose onClose(WebSocket webSocket, + int statusCode, + String reason) { + return new OnClose(webSocket, statusCode, reason); + } + + public static OnError onError(WebSocket webSocket, + Class clazz) { + return new OnError(webSocket, clazz); + } + + final WebSocket webSocket; + + private Invocation(WebSocket webSocket) { + this.webSocket = webSocket; + } + } + + public static final class OnOpen extends Invocation { + + public OnOpen(WebSocket webSocket) { + super(webSocket); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Invocation that = (Invocation) o; + return Objects.equals(webSocket, that.webSocket); + } + + @Override + public int hashCode() { + return Objects.hashCode(webSocket); + } + } + + public static final class OnText extends Invocation { + + final String text; + final MessagePart part; + + public OnText(WebSocket webSocket, String text, MessagePart part) { + super(webSocket); + this.text = text; + this.part = part; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OnText onText = (OnText) o; + return Objects.equals(text, onText.text) && + part == onText.part && + Objects.equals(webSocket, onText.webSocket); + } + + @Override + public int hashCode() { + return Objects.hash(text, part, webSocket); + } + + @Override + public String toString() { + return String.format("onText(%s, %s, %s)", webSocket, text, part); + } + } + + public static final class OnBinary extends Invocation { + + final ByteBuffer data; + final MessagePart part; + + public OnBinary(WebSocket webSocket, ByteBuffer data, MessagePart part) { + super(webSocket); + this.data = data; + this.part = part; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OnBinary onBinary = (OnBinary) o; + return Objects.equals(data, onBinary.data) && + part == onBinary.part && + Objects.equals(webSocket, onBinary.webSocket); + } + + @Override + public int hashCode() { + return Objects.hash(data, part, webSocket); + } + + @Override + public String toString() { + return String.format("onBinary(%s, %s, %s)", webSocket, data, part); + } + } + + public static final class OnPing extends Invocation { + + final ByteBuffer data; + + public OnPing(WebSocket webSocket, ByteBuffer data) { + super(webSocket); + this.data = data; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OnPing onPing = (OnPing) o; + return Objects.equals(data, onPing.data) && + Objects.equals(webSocket, onPing.webSocket); + } + + @Override + public int hashCode() { + return Objects.hash(data, webSocket); + } + + @Override + public String toString() { + return String.format("onPing(%s, %s)", webSocket, data); + } + } + + public static final class OnPong extends Invocation { + + final ByteBuffer data; + + public OnPong(WebSocket webSocket, ByteBuffer data) { + super(webSocket); + this.data = data; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OnPong onPong = (OnPong) o; + return Objects.equals(data, onPong.data) && + Objects.equals(webSocket, onPong.webSocket); + } + + @Override + public int hashCode() { + return Objects.hash(data, webSocket); + } + + @Override + public String toString() { + return String.format("onPong(%s, %s)", webSocket, data); + } + } + + public static final class OnClose extends Invocation { + + final int statusCode; + final String reason; + + public OnClose(WebSocket webSocket, int statusCode, String reason) { + super(webSocket); + this.statusCode = statusCode; + this.reason = reason; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OnClose onClose = (OnClose) o; + return statusCode == onClose.statusCode && + Objects.equals(reason, onClose.reason) && + Objects.equals(webSocket, onClose.webSocket); + } + + @Override + public int hashCode() { + return Objects.hash(statusCode, reason, webSocket); + } + + @Override + public String toString() { + return String.format("onClose(%s, %s, %s)", webSocket, statusCode, reason); + } + } + + public static final class OnError extends Invocation { + + final Class clazz; + + public OnError(WebSocket webSocket, Class clazz) { + super(webSocket); + this.clazz = clazz; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OnError onError = (OnError) o; + return Objects.equals(clazz, onError.clazz) && + Objects.equals(webSocket, onError.webSocket); + } + + @Override + public int hashCode() { + return Objects.hash(clazz, webSocket); + } + + @Override + public String toString() { + return String.format("onError(%s, %s)", webSocket, clazz); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/MockTransport.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/MockTransport.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,436 @@ +/* + * 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.internal.net.http.websocket; + +import java.net.http.WebSocket.MessagePart; +import jdk.internal.net.http.common.Demand; +import jdk.internal.net.http.common.SequentialScheduler; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static jdk.internal.net.http.websocket.TestSupport.fullCopy; + +public class MockTransport implements Transport { + + private final long startTime = System.currentTimeMillis(); + private final Queue output = new ConcurrentLinkedQueue<>(); + private final Queue>> + input = new ConcurrentLinkedQueue<>(); + private final Supplier supplier; + private final MessageStreamConsumer consumer; + private final SequentialScheduler scheduler + = new SequentialScheduler(new ReceiveTask()); + private final Demand demand = new Demand(); + + public MockTransport(Supplier sendResultSupplier, + MessageStreamConsumer consumer) { + this.supplier = sendResultSupplier; + this.consumer = consumer; + input.addAll(receive()); + } + + @Override + public final CompletableFuture sendText(CharSequence message, + boolean isLast) { + output.add(Invocation.sendText(message, isLast)); + return send(String.format("sendText(%s, %s)", message, isLast), + () -> sendText0(message, isLast)); + } + + protected CompletableFuture sendText0(CharSequence message, + boolean isLast) { + return defaultSend(); + } + + protected CompletableFuture defaultSend() { + return CompletableFuture.completedFuture(result()); + } + + @Override + public final CompletableFuture sendBinary(ByteBuffer message, + boolean isLast) { + output.add(Invocation.sendBinary(message, isLast)); + return send(String.format("sendBinary(%s, %s)", message, isLast), + () -> sendBinary0(message, isLast)); + } + + protected CompletableFuture sendBinary0(ByteBuffer message, + boolean isLast) { + return defaultSend(); + } + + @Override + public final CompletableFuture sendPing(ByteBuffer message) { + output.add(Invocation.sendPing(message)); + return send(String.format("sendPing(%s)", message), + () -> sendPing0(message)); + } + + protected CompletableFuture sendPing0(ByteBuffer message) { + return defaultSend(); + } + + @Override + public final CompletableFuture sendPong(ByteBuffer message) { + output.add(Invocation.sendPong(message)); + return send(String.format("sendPong(%s)", message), + () -> sendPong0(message)); + } + + protected CompletableFuture sendPong0(ByteBuffer message) { + return defaultSend(); + } + + @Override + public final CompletableFuture sendClose(int statusCode, String reason) { + output.add(Invocation.sendClose(statusCode, reason)); + return send(String.format("sendClose(%s, %s)", statusCode, reason), + () -> sendClose0(statusCode, reason)); + } + + protected CompletableFuture sendClose0(int statusCode, String reason) { + return defaultSend(); + } + + protected Collection>> receive() { + return List.of(); + } + + public static Consumer onText(CharSequence data, + MessagePart part) { + return c -> c.onText(data.toString(), part); + } + + public static Consumer onBinary(ByteBuffer data, + MessagePart part) { + return c -> c.onBinary(fullCopy(data), part); + } + + public static Consumer onPing(ByteBuffer data) { + return c -> c.onPing(fullCopy(data)); + } + + public static Consumer onPong(ByteBuffer data) { + return c -> c.onPong(fullCopy(data)); + } + + public static Consumer onClose(int statusCode, + String reason) { + return c -> c.onClose(statusCode, reason); + } + + public static Consumer onError(Throwable error) { + return c -> c.onError(error); + } + + public static Consumer onComplete() { + return c -> c.onComplete(); + } + + @Override + public void request(long n) { + demand.increase(n); + scheduler.runOrSchedule(); + } + + @Override + public void acknowledgeReception() { + demand.tryDecrement(); + } + + @Override + public final void closeOutput() throws IOException { + output.add(Invocation.closeOutput()); + begin("closeOutput()"); + closeOutput0(); + end("closeOutput()"); + } + + protected void closeOutput0() throws IOException { + defaultClose(); + } + + protected void defaultClose() throws IOException { + } + + @Override + public final void closeInput() throws IOException { + output.add(Invocation.closeInput()); + begin("closeInput()"); + closeInput0(); + end("closeInput()"); + } + + protected void closeInput0() throws IOException { + defaultClose(); + } + + public abstract static class Invocation { + + static Invocation.SendText sendText(CharSequence message, + boolean isLast) { + return new SendText(message, isLast); + } + + static Invocation.SendBinary sendBinary(ByteBuffer message, + boolean isLast) { + return new SendBinary(message, isLast); + } + + static Invocation.SendPing sendPing(ByteBuffer message) { + return new SendPing(message); + } + + static Invocation.SendPong sendPong(ByteBuffer message) { + return new SendPong(message); + } + + static Invocation.SendClose sendClose(int statusCode, String reason) { + return new SendClose(statusCode, reason); + } + + public static CloseOutput closeOutput() { + return new CloseOutput(); + } + + public static CloseInput closeInput() { + return new CloseInput(); + } + + public static final class SendText extends Invocation { + + final CharSequence message; + final boolean isLast; + + SendText(CharSequence message, boolean isLast) { + this.message = message.toString(); + this.isLast = isLast; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + SendText sendText = (SendText) obj; + return isLast == sendText.isLast && + Objects.equals(message, sendText.message); + } + + @Override + public int hashCode() { + return Objects.hash(isLast, message); + } + } + + public static final class SendBinary extends Invocation { + + final ByteBuffer message; + final boolean isLast; + + SendBinary(ByteBuffer message, boolean isLast) { + this.message = fullCopy(message); + this.isLast = isLast; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + SendBinary that = (SendBinary) obj; + return isLast == that.isLast && + Objects.equals(message, that.message); + } + + @Override + public int hashCode() { + return Objects.hash(message, isLast); + } + } + + private static final class SendPing extends Invocation { + + final ByteBuffer message; + + SendPing(ByteBuffer message) { + this.message = fullCopy(message); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + SendPing sendPing = (SendPing) obj; + return Objects.equals(message, sendPing.message); + } + + @Override + public int hashCode() { + return Objects.hash(message); + } + } + + private static final class SendPong extends Invocation { + + final ByteBuffer message; + + SendPong(ByteBuffer message) { + this.message = fullCopy(message); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + SendPing sendPing = (SendPing) obj; + return Objects.equals(message, sendPing.message); + } + + @Override + public int hashCode() { + return Objects.hash(message); + } + } + + private static final class SendClose extends Invocation { + + final int statusCode; + final String reason; + + SendClose(int statusCode, String reason) { + this.statusCode = statusCode; + this.reason = reason; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + SendClose sendClose = (SendClose) obj; + return statusCode == sendClose.statusCode && + Objects.equals(reason, sendClose.reason); + } + + @Override + public int hashCode() { + return Objects.hash(statusCode, reason); + } + } + + private static final class CloseOutput extends Invocation { + + CloseOutput() { } + + @Override + public int hashCode() { + return 0; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof CloseOutput; + } + } + + private static final class CloseInput extends Invocation { + + CloseInput() { } + + @Override + public int hashCode() { + return 0; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof CloseInput; + } + } + } + + public Queue invocations() { + return new LinkedList<>(output); + } + + protected final T result() { + return supplier.get(); + } + + private CompletableFuture send(String name, + Supplier> supplier) { + begin(name); + CompletableFuture cf = supplier.get().whenComplete((r, e) -> { + System.out.printf("[%6s ms.] complete %s%n", elapsedTime(), name); + }); + end(name); + return cf; + } + + private void begin(String name) { + System.out.printf("[%6s ms.] begin %s%n", elapsedTime(), name); + } + + private void end(String name) { + System.out.printf("[%6s ms.] end %s%n", elapsedTime(), name); + } + + private long elapsedTime() { + return System.currentTimeMillis() - startTime; + } + + private final class ReceiveTask implements SequentialScheduler.RestartableTask { + + @Override + public void run(SequentialScheduler.DeferredCompleter taskCompleter) { + if (!scheduler.isStopped() && !demand.isFulfilled() && !input.isEmpty()) { + CompletableFuture> cf = input.remove(); + if (cf.isDone()) { // Forcing synchronous execution + cf.join().accept(consumer); + repeat(taskCompleter); + } else { + cf.whenCompleteAsync((r, e) -> { + r.accept(consumer); + repeat(taskCompleter); + }); + } + } else { + taskCompleter.complete(); + } + } + + private void repeat(SequentialScheduler.DeferredCompleter taskCompleter) { + taskCompleter.complete(); + scheduler.runOrSchedule(); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/ReaderTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/ReaderTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,271 @@ +/* + * 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. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.websocket; + +import org.testng.annotations.Test; +import jdk.internal.net.http.websocket.Frame.Opcode; + +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.function.IntPredicate; +import java.util.function.IntUnaryOperator; + +import static java.util.OptionalInt.empty; +import static java.util.OptionalInt.of; +import static org.testng.Assert.assertEquals; +import static jdk.internal.net.http.websocket.TestSupport.assertThrows; +import static jdk.internal.net.http.websocket.TestSupport.forEachBufferPartition; + +public class ReaderTest { + + private long cases, frames; + + @Test + void notMinimalEncoding01() { + ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES); + h.put((byte) 0b1000_0000).put((byte) 0b0111_1110).putChar((char) 125).flip(); + assertThrows(FailWebSocketException.class, + ".*(?i)minimally-encoded.*", + () -> new Frame.Reader().readFrame(h, new MockConsumer())); + } + + @Test + void notMinimalEncoding02() { + ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES); + h.put((byte) 0b1000_0000).put((byte) 0b0111_1111).putLong(125).flip(); + assertThrows(FailWebSocketException.class, + ".*(?i)minimally-encoded.*", + () -> new Frame.Reader().readFrame(h, new MockConsumer())); + } + + @Test + void notMinimalEncoding03() { + ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES); + h.put((byte) 0b1000_0000).put((byte) 0b0111_1111).putLong(65535).flip(); + assertThrows(FailWebSocketException.class, + ".*(?i)minimally-encoded.*", + () -> new Frame.Reader().readFrame(h, new MockConsumer())); + } + + @Test + public void negativePayload() { + ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES); + h.put((byte) 0b1000_0000).put((byte) 0b0111_1111).putLong(-2L).flip(); + assertThrows(FailWebSocketException.class, + ".*(?i)negative.*", + () -> new Frame.Reader().readFrame(h, new MockConsumer())); + } + + @Test + public void frameStart() { + final long[] payloads = {0, 126, 65536, Integer.MAX_VALUE + 1L}; + final OptionalInt[] masks = {empty(), of(-1), of(0), of(0xCAFEBABE), + of(Integer.MAX_VALUE), of(Integer.MIN_VALUE)}; + for (boolean fin : new boolean[]{true, false}) { + for (boolean rsv1 : new boolean[]{true, false}) { + for (boolean rsv2 : new boolean[]{true, false}) { + for (boolean rsv3 : new boolean[]{true, false}) { + for (Opcode opcode : Opcode.values()) { + for (long payloadLen : payloads) { + for (OptionalInt mask : masks) { + verifyFrameStart(fin, rsv1, rsv2, rsv3, opcode, payloadLen, mask); + } + } + } + } + } + } + } + System.out.println("Frames: " + frames + ", Total cases: " + cases); + } + + /* + * Tests whether or not the frame starts properly. + * That is, a header and the first invocation of payloadData (if any). + */ + private void verifyFrameStart(boolean fin, + boolean rsv1, + boolean rsv2, + boolean rsv3, + Opcode opcode, + long payloadLen, + OptionalInt mask) { + frames++; + Frame.HeaderWriter w = new Frame.HeaderWriter(); + ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES); + w.fin(fin).rsv1(rsv1).rsv2(rsv2).rsv3(rsv3).opcode(opcode).payloadLen(payloadLen); + mask.ifPresentOrElse(w::mask, w::noMask); + w.write(h); + h.flip(); + forEachBufferPartition(h, + buffers -> { + cases++; + Frame.Reader r = new Frame.Reader(); + MockConsumer c = new MockConsumer(); + for (ByteBuffer b : buffers) { + r.readFrame(b, c); + } + assertEquals(fin, c.fin()); + assertEquals(rsv1, c.rsv1()); + assertEquals(rsv2, c.rsv2()); + assertEquals(rsv3, c.rsv3()); + assertEquals(opcode, c.opcode()); + assertEquals(mask.isPresent(), c.mask()); + assertEquals(payloadLen, c.payloadLen()); + assertEquals(mask, c.maskingKey()); + assertEquals(payloadLen == 0, c.isEndFrame()); + }); + } + + /* + * Used to verify the order, the number of invocations as well as the + * arguments of each individual invocation to Frame.Consumer's methods. + */ + private static class MockConsumer implements Frame.Consumer { + + private int invocationOrder; + + private Optional fin = Optional.empty(); + private Optional rsv1 = Optional.empty(); + private Optional rsv2 = Optional.empty(); + private Optional rsv3 = Optional.empty(); + private Optional opcode = Optional.empty(); + private Optional mask = Optional.empty(); + private OptionalLong payloadLen = OptionalLong.empty(); + private OptionalInt maskingKey = OptionalInt.empty(); + + @Override + public void fin(boolean value) { + checkAndSetOrder(0, 1); + fin = Optional.of(value); + } + + @Override + public void rsv1(boolean value) { + checkAndSetOrder(1, 2); + rsv1 = Optional.of(value); + } + + @Override + public void rsv2(boolean value) { + checkAndSetOrder(2, 3); + rsv2 = Optional.of(value); + } + + @Override + public void rsv3(boolean value) { + checkAndSetOrder(3, 4); + rsv3 = Optional.of(value); + } + + @Override + public void opcode(Opcode value) { + checkAndSetOrder(4, 5); + opcode = Optional.of(value); + } + + @Override + public void mask(boolean value) { + checkAndSetOrder(5, 6); + mask = Optional.of(value); + } + + @Override + public void payloadLen(long value) { + checkAndSetOrder(p -> p == 5 || p == 6, n -> 7); + payloadLen = OptionalLong.of(value); + } + + @Override + public void maskingKey(int value) { + checkAndSetOrder(7, 8); + maskingKey = of(value); + } + + @Override + public void payloadData(ByteBuffer data) { + checkAndSetOrder(p -> p == 7 || p == 8, n -> 9); + assert payloadLen.isPresent(); + if (payloadLen.getAsLong() != 0 && !data.hasRemaining()) { + throw new TestSupport.AssertionFailedException("Artefact of reading"); + } + } + + @Override + public void endFrame() { + checkAndSetOrder(9, 10); + } + + boolean isEndFrame() { + return invocationOrder == 10; + } + + public boolean fin() { + return fin.get(); + } + + public boolean rsv1() { + return rsv1.get(); + } + + public boolean rsv2() { + return rsv2.get(); + } + + public boolean rsv3() { + return rsv3.get(); + } + + public Opcode opcode() { + return opcode.get(); + } + + public boolean mask() { + return mask.get(); + } + + public long payloadLen() { + return payloadLen.getAsLong(); + } + + public OptionalInt maskingKey() { + return maskingKey; + } + + private void checkAndSetOrder(int expectedValue, int newValue) { + checkAndSetOrder(p -> p == expectedValue, n -> newValue); + } + + private void checkAndSetOrder(IntPredicate expectedValue, + IntUnaryOperator newValue) { + if (!expectedValue.test(invocationOrder)) { + throw new TestSupport.AssertionFailedException( + expectedValue + " -> " + newValue); + } + invocationOrder = newValue.applyAsInt(invocationOrder); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/TestSupport.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/TestSupport.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,336 @@ +/* + * 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. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.websocket; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Stack; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import static java.util.List.of; +import static java.util.Objects.requireNonNull; + +/* + * Auxiliary test infrastructure + */ +final class TestSupport { + + private TestSupport() { } + + static Iterator cartesianIterator(List a, + List b, + F2 f2) { + @SuppressWarnings("unchecked") + F t = p -> f2.apply((A) p[0], (B) p[1]); + return cartesianIterator(of(a, b), t); + } + + static Iterator cartesianIterator(List a, + List b, + List c, + F3 f3) { + @SuppressWarnings("unchecked") + F t = p -> f3.apply((A) p[0], (B) p[1], (C) p[2]); + return cartesianIterator(of(a, b, c), t); + } + + static Iterator cartesianIterator(List a, + List b, + List c, + List d, + F4 f4) { + @SuppressWarnings("unchecked") + F t = p -> f4.apply((A) p[0], (B) p[1], (C) p[2], (D) p[3]); + return cartesianIterator(of(a, b, c, d), t); + } + + static Iterator cartesianIterator(List a, + List b, + List c, + List d, + List e, + F5 f5) { + @SuppressWarnings("unchecked") + F t = p -> f5.apply((A) p[0], (B) p[1], (C) p[2], (D) p[3], (E) p[4]); + return cartesianIterator(of(a, b, c, d, e), t); + } + + static Iterator cartesianIterator(List> params, + F function) { + if (params.isEmpty()) { + return Collections.emptyIterator(); + } + for (List l : params) { + if (l.isEmpty()) { + return Collections.emptyIterator(); + } + } + // Assertion: if we are still here, there is at least a single element + // in the product + return new Iterator<>() { + + private final int arity = params.size(); + private final int[] coordinates = new int[arity]; + private boolean hasNext = true; + + @Override + public boolean hasNext() { + return hasNext; + } + + @Override + public R next() { + if (!hasNext) { + throw new NoSuchElementException(); + } + Object[] array = new Object[arity]; + for (int i = 0; i < arity; i++) { + array[i] = params.get(i).get(coordinates[i]); + } + int p = arity - 1; + while (p >= 0 && coordinates[p] == params.get(p).size() - 1) { + p--; + } + if (p < 0) { + hasNext = false; + } else { + coordinates[p]++; + for (int i = p + 1; i < arity; i++) { + coordinates[i] = 0; + } + } + return function.apply(array); + } + }; + } + + @FunctionalInterface + public interface F1 { + R apply(A a); + } + + @FunctionalInterface + public interface F2 { + R apply(A a, B b); + } + + @FunctionalInterface + public interface F3 { + R apply(A a, B b, C c); + } + + @FunctionalInterface + public interface F4 { + R apply(A a, B b, C c, D d); + } + + @FunctionalInterface + public interface F5 { + R apply(A a, B b, C c, D d, E e); + } + + @FunctionalInterface + public interface F { + R apply(Object[] args); + } + + static Iterator iteratorOf1(T element) { + return List.of(element).iterator(); + } + + @SafeVarargs + static Iterator iteratorOf(T... elements) { + return List.of(elements).iterator(); + } + + static Iterator limit(int maxElements, Iterator elements) { + return new Iterator<>() { + + int count = maxElements; + + @Override + public boolean hasNext() { + return count > 0 && elements.hasNext(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + count--; + return elements.next(); + } + }; + } + + static ByteBuffer fullCopy(ByteBuffer src) { + ByteBuffer copy = ByteBuffer.allocate(src.capacity()); + int p = src.position(); + int l = src.limit(); + src.clear(); + copy.put(src).position(p).limit(l); + src.position(p).limit(l); + return copy; + } + + static void forEachBufferPartition(ByteBuffer src, + Consumer> action) { + forEachPartition(src.remaining(), + (lengths) -> { + int end = src.position(); + List buffers = new LinkedList<>(); + for (int len : lengths) { + ByteBuffer d = src.duplicate(); + d.position(end); + d.limit(end + len); + end += len; + buffers.add(d); + } + action.accept(buffers); + }); + } + + private static void forEachPartition(int n, + Consumer> action) { + forEachPartition(n, new Stack<>(), action); + } + + private static void forEachPartition(int n, + Stack path, + Consumer> action) { + if (n == 0) { + action.accept(path); + } else { + for (int i = 1; i <= n; i++) { + path.push(i); + forEachPartition(n - i, path, action); + path.pop(); + } + } + } + + static void forEachPermutation(int n, Consumer c) { + int[] a = new int[n]; + for (int i = 0; i < n; i++) { + a[i] = i; + } + permutations(0, a, c); + } + + private static void permutations(int i, int[] a, Consumer c) { + if (i == a.length) { + c.accept(Arrays.copyOf(a, a.length)); + return; + } + for (int j = i; j < a.length; j++) { + swap(a, i, j); + permutations(i + 1, a, c); + swap(a, i, j); + } + } + + private static void swap(int[] a, int i, int j) { + int x = a[i]; + a[i] = a[j]; + a[j] = x; + } + + public static T assertThrows(Class clazz, + ThrowingProcedure code) { + @SuppressWarnings("unchecked") + T t = (T) assertThrows(clazz::isInstance, code); + return t; + } + + /* + * The rationale behind asking for a regex is to not pollute variable names + * space in the scope of assertion: if it's something as simple as checking + * a message, we can do it inside + */ + @SuppressWarnings("unchecked") + static T assertThrows(Class clazz, + String messageRegex, + ThrowingProcedure code) { + requireNonNull(messageRegex, "messagePattern"); + Predicate p = e -> clazz.isInstance(e) + && Pattern.matches(messageRegex, e.getMessage()); + return (T) assertThrows(p, code); + } + + static Throwable assertThrows(Predicate predicate, + ThrowingProcedure code) { + requireNonNull(predicate, "predicate"); + requireNonNull(code, "code"); + Throwable caught = null; + try { + code.run(); + } catch (Throwable t) { + caught = t; + } + if (caught == null) { + throw new AssertionFailedException("No exception was thrown"); + } + if (predicate.test(caught)) { + System.out.println("Got expected exception: " + caught); + return caught; + } + throw new AssertionFailedException("Caught exception didn't match the predicate", caught); + } + + /* + * Blocking assertion, waits for completion + */ + static Throwable assertCompletesExceptionally(Class clazz, + CompletionStage stage) { + CompletableFuture cf = + CompletableFuture.completedFuture(null).thenCompose(x -> stage); + return assertThrows(t -> clazz.isInstance(t.getCause()), cf::get); + } + + interface ThrowingProcedure { + void run() throws Throwable; + } + + static final class AssertionFailedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + AssertionFailedException(String message) { + super(message); + } + + AssertionFailedException(String message, Throwable cause) { + super(message, cause); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/WebSocketImplTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/websocket/java.net.http/jdk/internal/net/http/websocket/WebSocketImplTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,453 @@ +/* + * 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.internal.net.http.websocket; + +import java.net.http.WebSocket; +import org.testng.annotations.Test; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static java.net.http.WebSocket.MessagePart.FIRST; +import static java.net.http.WebSocket.MessagePart.LAST; +import static java.net.http.WebSocket.MessagePart.PART; +import static java.net.http.WebSocket.MessagePart.WHOLE; +import static java.net.http.WebSocket.NORMAL_CLOSURE; +import static jdk.internal.net.http.websocket.MockListener.Invocation.onClose; +import static jdk.internal.net.http.websocket.MockListener.Invocation.onError; +import static jdk.internal.net.http.websocket.MockListener.Invocation.onOpen; +import static jdk.internal.net.http.websocket.MockListener.Invocation.onPing; +import static jdk.internal.net.http.websocket.MockListener.Invocation.onPong; +import static jdk.internal.net.http.websocket.MockListener.Invocation.onText; +import static jdk.internal.net.http.websocket.MockTransport.onClose; +import static jdk.internal.net.http.websocket.MockTransport.onPing; +import static jdk.internal.net.http.websocket.MockTransport.onPong; +import static jdk.internal.net.http.websocket.MockTransport.onText; +import static jdk.internal.net.http.websocket.TestSupport.assertCompletesExceptionally; +import static org.testng.Assert.assertEquals; + +/* + * Formatting in this file may seem strange: + * + * ( + * ( ...) + * ... + * ) + * ... + * + * However there is a rationale behind it. Sometimes the level of argument + * nesting is high, which makes it hard to manage parentheses. + */ +public class WebSocketImplTest { + + // TODO: request in onClose/onError + // TODO: throw exception in onClose/onError + // TODO: exception is thrown from request() + // TODO: repeated sendClose complete normally + // TODO: default Close message is sent if IAE is thrown from sendClose + + @Test + public void testNonPositiveRequest() throws Exception { + MockListener listener = new MockListener(Long.MAX_VALUE) { + @Override + protected void onOpen0(WebSocket webSocket) { + webSocket.request(0); + } + }; + WebSocket ws = newInstance(listener, List.of(now(onText("1", WHOLE)))); + listener.onCloseOrOnErrorCalled().get(10, TimeUnit.SECONDS); + List invocations = listener.invocations(); + assertEquals( + invocations, + List.of( + onOpen(ws), + onError(ws, IllegalArgumentException.class) + ) + ); + } + + @Test + public void testText1() throws Exception { + MockListener listener = new MockListener(Long.MAX_VALUE); + WebSocket ws = newInstance( + listener, + List.of( + now(onText("1", FIRST)), + now(onText("2", PART)), + now(onText("3", LAST)), + now(onClose(NORMAL_CLOSURE, "no reason")) + ) + ); + listener.onCloseOrOnErrorCalled().get(10, TimeUnit.SECONDS); + List invocations = listener.invocations(); + assertEquals( + invocations, + List.of( + onOpen(ws), + onText(ws, "1", FIRST), + onText(ws, "2", PART), + onText(ws, "3", LAST), + onClose(ws, NORMAL_CLOSURE, "no reason") + ) + ); + } + + @Test + public void testText2() throws Exception { + MockListener listener = new MockListener(Long.MAX_VALUE); + WebSocket ws = newInstance( + listener, + List.of( + now(onText("1", FIRST)), + seconds(1, onText("2", PART)), + now(onText("3", LAST)), + seconds(1, onClose(NORMAL_CLOSURE, "no reason")) + ) + ); + listener.onCloseOrOnErrorCalled().get(10, TimeUnit.SECONDS); + List invocations = listener.invocations(); + assertEquals( + invocations, + List.of( + onOpen(ws), + onText(ws, "1", FIRST), + onText(ws, "2", PART), + onText(ws, "3", LAST), + onClose(ws, NORMAL_CLOSURE, "no reason") + ) + ); + } + + @Test + public void testTextIntermixedWithPongs() throws Exception { + MockListener listener = new MockListener(Long.MAX_VALUE); + WebSocket ws = newInstance( + listener, + List.of( + now(onText("1", FIRST)), + now(onText("2", PART)), + now(onPong(ByteBuffer.allocate(16))), + seconds(1, onPong(ByteBuffer.allocate(32))), + now(onText("3", LAST)), + now(onPong(ByteBuffer.allocate(64))), + now(onClose(NORMAL_CLOSURE, "no reason")) + ) + ); + listener.onCloseOrOnErrorCalled().get(10, TimeUnit.SECONDS); + List invocations = listener.invocations(); + assertEquals( + invocations, + List.of( + onOpen(ws), + onText(ws, "1", FIRST), + onText(ws, "2", PART), + onPong(ws, ByteBuffer.allocate(16)), + onPong(ws, ByteBuffer.allocate(32)), + onText(ws, "3", LAST), + onPong(ws, ByteBuffer.allocate(64)), + onClose(ws, NORMAL_CLOSURE, "no reason") + ) + ); + } + + @Test + public void testTextIntermixedWithPings() throws Exception { + MockListener listener = new MockListener(Long.MAX_VALUE); + WebSocket ws = newInstance( + listener, + List.of( + now(onText("1", FIRST)), + now(onText("2", PART)), + now(onPing(ByteBuffer.allocate(16))), + seconds(1, onPing(ByteBuffer.allocate(32))), + now(onText("3", LAST)), + now(onPing(ByteBuffer.allocate(64))), + now(onClose(NORMAL_CLOSURE, "no reason")) + ) + ); + listener.onCloseOrOnErrorCalled().get(10, TimeUnit.SECONDS); + List invocations = listener.invocations(); + assertEquals( + invocations, + List.of( + onOpen(ws), + onText(ws, "1", FIRST), + onText(ws, "2", PART), + onPing(ws, ByteBuffer.allocate(16)), + onPing(ws, ByteBuffer.allocate(32)), + onText(ws, "3", LAST), + onPing(ws, ByteBuffer.allocate(64)), + onClose(ws, NORMAL_CLOSURE, "no reason")) + ); + } + + // Tease out "java.lang.IllegalStateException: Send pending" due to possible + // race between sending a message and replenishing the permit + @Test + public void testManyTextMessages() { + WebSocketImpl ws = newInstance( + new MockListener(1), + new TransportFactory() { + @Override + public Transport createTransport(Supplier sendResultSupplier, + MessageStreamConsumer consumer) { + + final Random r = new Random(); + + return new MockTransport<>(sendResultSupplier, consumer) { + @Override + protected CompletableFuture defaultSend() { + return millis(r.nextInt(100), result()); + } + }; + } + }); + int NUM_MESSAGES = 512; + CompletableFuture current = CompletableFuture.completedFuture(ws); + for (int i = 0; i < NUM_MESSAGES; i++) { + current = current.thenCompose(w -> w.sendText(" ", true)); + } + current.join(); + MockTransport transport = (MockTransport) ws.transport(); + assertEquals(transport.invocations().size(), NUM_MESSAGES); + } + + @Test + public void testManyBinaryMessages() { + WebSocketImpl ws = newInstance( + new MockListener(1), + new TransportFactory() { + @Override + public Transport createTransport(Supplier sendResultSupplier, + MessageStreamConsumer consumer) { + + final Random r = new Random(); + + return new MockTransport<>(sendResultSupplier, consumer) { + @Override + protected CompletableFuture defaultSend() { + return millis(r.nextInt(150), result()); + } + }; + } + }); + CompletableFuture start = new CompletableFuture<>(); + + int NUM_MESSAGES = 512; + CompletableFuture current = start; + for (int i = 0; i < NUM_MESSAGES; i++) { + current = current.thenComposeAsync(w -> w.sendBinary(ByteBuffer.allocate(1), true)); + } + + start.completeAsync(() -> ws); + current.join(); + + MockTransport transport = (MockTransport) ws.transport(); + assertEquals(transport.invocations().size(), NUM_MESSAGES); + } + + + @Test + public void sendTextImmediately() { + WebSocketImpl ws = newInstance( + new MockListener(1), + new TransportFactory() { + @Override + public Transport createTransport(Supplier sendResultSupplier, + MessageStreamConsumer consumer) { + return new MockTransport<>(sendResultSupplier, consumer); + } + }); + CompletableFuture.completedFuture(ws) + .thenCompose(w -> w.sendText("1", true)) + .thenCompose(w -> w.sendText("2", true)) + .thenCompose(w -> w.sendText("3", true)) + .join(); + MockTransport transport = (MockTransport) ws.transport(); + assertEquals(transport.invocations().size(), 3); + } + + @Test + public void sendTextWithDelay() { + MockListener listener = new MockListener(1); + WebSocketImpl ws = newInstance( + listener, + new TransportFactory() { + @Override + public Transport createTransport(Supplier sendResultSupplier, + MessageStreamConsumer consumer) { + return new MockTransport<>(sendResultSupplier, consumer) { + @Override + protected CompletableFuture defaultSend() { + return seconds(1, result()); + } + }; + } + }); + CompletableFuture.completedFuture(ws) + .thenCompose(w -> w.sendText("1", true)) + .thenCompose(w -> w.sendText("2", true)) + .thenCompose(w -> w.sendText("3", true)) + .join(); + assertEquals(listener.invocations(), List.of(onOpen(ws))); + MockTransport transport = (MockTransport) ws.transport(); + assertEquals(transport.invocations().size(), 3); + } + + @Test + public void sendTextMixedDelay() { + MockListener listener = new MockListener(1); + WebSocketImpl ws = newInstance( + listener, + new TransportFactory() { + + final Random r = new Random(); + + @Override + public Transport createTransport(Supplier sendResultSupplier, + MessageStreamConsumer consumer) { + return new MockTransport<>(sendResultSupplier, consumer) { + @Override + protected CompletableFuture defaultSend() { + return r.nextBoolean() + ? seconds(1, result()) + : now(result()); + } + }; + } + }); + CompletableFuture.completedFuture(ws) + .thenCompose(w -> w.sendText("1", true)) + .thenCompose(w -> w.sendText("2", true)) + .thenCompose(w -> w.sendText("3", true)) + .thenCompose(w -> w.sendText("4", true)) + .thenCompose(w -> w.sendText("5", true)) + .thenCompose(w -> w.sendText("6", true)) + .thenCompose(w -> w.sendText("7", true)) + .thenCompose(w -> w.sendText("8", true)) + .thenCompose(w -> w.sendText("9", true)) + .join(); + assertEquals(listener.invocations(), List.of(onOpen(ws))); + MockTransport transport = (MockTransport) ws.transport(); + assertEquals(transport.invocations().size(), 9); + } + + @Test(enabled = false) // temporarily disabled + public void sendControlMessagesConcurrently() { + MockListener listener = new MockListener(1); + + CompletableFuture first = new CompletableFuture<>(); // barrier + + WebSocketImpl ws = newInstance( + listener, + new TransportFactory() { + + final AtomicInteger i = new AtomicInteger(); + + @Override + public Transport createTransport(Supplier sendResultSupplier, + MessageStreamConsumer consumer) { + return new MockTransport<>(sendResultSupplier, consumer) { + @Override + protected CompletableFuture defaultSend() { + if (i.incrementAndGet() == 1) { + return first.thenApply(o -> result()); + } else { + return now(result()); + } + } + }; + } + }); + + CompletableFuture cf1 = ws.sendPing(ByteBuffer.allocate(0)); + CompletableFuture cf2 = ws.sendPong(ByteBuffer.allocate(0)); + CompletableFuture cf3 = ws.sendClose(NORMAL_CLOSURE, ""); + CompletableFuture cf4 = ws.sendClose(NORMAL_CLOSURE, ""); + CompletableFuture cf5 = ws.sendPing(ByteBuffer.allocate(0)); + CompletableFuture cf6 = ws.sendPong(ByteBuffer.allocate(0)); + + first.complete(null); + // Don't care about exceptional completion, only that all of them have + // completed + CompletableFuture.allOf(cf1, cf2, cf3, cf4, cf5, cf6) + .handle((v, e) -> null).join(); + + cf3.join(); /* Check that sendClose has completed normally */ + cf4.join(); /* Check that repeated sendClose has completed normally */ + assertCompletesExceptionally(IllegalStateException.class, cf5); + assertCompletesExceptionally(IllegalStateException.class, cf6); + + assertEquals(listener.invocations(), List.of(onOpen(ws))); + MockTransport transport = (MockTransport) ws.transport(); + assertEquals(transport.invocations().size(), 3); // 6 minus 3 that were not accepted + } + + private static CompletableFuture seconds(long val, T result) { + return new CompletableFuture() + .completeOnTimeout(result, val, TimeUnit.SECONDS); + } + + private static CompletableFuture millis(long val, T result) { + return new CompletableFuture() + .completeOnTimeout(result, val, TimeUnit.MILLISECONDS); + } + + private static CompletableFuture now(T result) { + return CompletableFuture.completedFuture(result); + } + + private static WebSocketImpl newInstance( + WebSocket.Listener listener, + Collection>> input) { + TransportFactory factory = new TransportFactory() { + @Override + public Transport createTransport(Supplier sendResultSupplier, + MessageStreamConsumer consumer) { + return new MockTransport(sendResultSupplier, consumer) { + @Override + protected Collection>> receive() { + return input; + } + }; + } + }; + return newInstance(listener, factory); + } + + private static WebSocketImpl newInstance(WebSocket.Listener listener, + TransportFactory factory) { + URI uri = URI.create("ws://localhost"); + String subprotocol = ""; + return WebSocketImpl.newInstance(uri, subprotocol, listener, factory); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/ConnectionPoolTestDriver.java --- a/test/jdk/java/net/httpclient/whitebox/ConnectionPoolTestDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/ConnectionPoolTestDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -27,9 +27,9 @@ * @summary Verifies that the ConnectionPool correctly handle * connection deadlines and purges the right connections * from the cache. - * @modules java.net.http/java.net.http.internal + * @modules java.net.http/jdk.internal.net.http * java.management * @run main/othervm * --add-reads java.net.http=java.management - * java.net.http/java.net.http.internal.ConnectionPoolTest + * java.net.http/jdk.internal.net.http.ConnectionPoolTest */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/DemandTestDriver.java --- a/test/jdk/java/net/httpclient/whitebox/DemandTestDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/DemandTestDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -23,6 +23,6 @@ /* * @test - * @modules java.net.http/java.net.http.internal.common - * @run testng java.net.http/java.net.http.internal.common.DemandTest + * @modules java.net.http/jdk.internal.net.http.common + * @run testng java.net.http/jdk.internal.net.http.common.DemandTest */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/Driver.java --- a/test/jdk/java/net/httpclient/whitebox/Driver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/Driver.java Wed Feb 07 21:45:37 2018 +0000 @@ -24,7 +24,7 @@ /* * @test * @bug 8151299 8164704 - * @modules java.net.http/java.net.http.internal - * @run testng java.net.http/java.net.http.internal.SelectorTest - * @run testng java.net.http/java.net.http.internal.RawChannelTest + * @modules java.net.http/jdk.internal.net.http + * @run testng java.net.http/jdk.internal.net.http.SelectorTest + * @run testng java.net.http/jdk.internal.net.http.RawChannelTest */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/FlowTestDriver.java --- a/test/jdk/java/net/httpclient/whitebox/FlowTestDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/FlowTestDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -23,6 +23,6 @@ /* * @test - * @modules java.net.http - * @run testng java.net.http/java.net.http.FlowTest + * @modules java.net.http/jdk.internal.net.http + * @run testng java.net.http/jdk.internal.net.http.FlowTest */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/FramesDecoderTestDriver.java --- a/test/jdk/java/net/httpclient/whitebox/FramesDecoderTestDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/FramesDecoderTestDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -24,9 +24,9 @@ /* * @test * @bug 8195823 - * @modules java.net.http/java.net.http.internal.frame + * @modules java.net.http/jdk.internal.net.http.frame * @run testng/othervm * -Djdk.internal.httpclient.debug=true - * java.net.http/java.net.http.internal.frame.FramesDecoderTest + * java.net.http/jdk.internal.net.http.frame.FramesDecoderTest */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/Http1HeaderParserTestDriver.java --- a/test/jdk/java/net/httpclient/whitebox/Http1HeaderParserTestDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/Http1HeaderParserTestDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -24,6 +24,6 @@ /* * @test * @bug 8195138 - * @modules java.net.http/java.net.http.internal - * @run testng java.net.http/java.net.http.internal.Http1HeaderParserTest + * @modules java.net.http/jdk.internal.net.http + * @run testng java.net.http/jdk.internal.net.http.Http1HeaderParserTest */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/MinimalFutureTestDriver.java --- a/test/jdk/java/net/httpclient/whitebox/MinimalFutureTestDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/MinimalFutureTestDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -23,6 +23,6 @@ /* * @test - * @modules java.net.http/java.net.http.internal.common - * @run testng java.net.http/java.net.http.internal.common.MinimalFutureTest + * @modules java.net.http/jdk.internal.net.http.common + * @run testng java.net.http/jdk.internal.net.http.common.MinimalFutureTest */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/SSLEchoTubeTestDriver.java --- a/test/jdk/java/net/httpclient/whitebox/SSLEchoTubeTestDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/SSLEchoTubeTestDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -23,6 +23,8 @@ /* * @test - * @modules java.net.http - * @run testng/othervm -Djdk.internal.httpclient.debug=true java.net.http/java.net.http.SSLEchoTubeTest + * @modules java.net.http/jdk.internal.net.http + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * java.net.http/jdk.internal.net.http.SSLEchoTubeTest */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/SSLTubeTestDriver.java --- a/test/jdk/java/net/httpclient/whitebox/SSLTubeTestDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/SSLTubeTestDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -23,6 +23,8 @@ /* * @test - * @modules java.net.http - * @run testng/othervm -Djdk.internal.httpclient.debug=true java.net.http/java.net.http.SSLTubeTest + * @modules java.net.http/jdk.internal.net.http + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * java.net.http/jdk.internal.net.http.SSLTubeTest */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/WrapperTestDriver.java --- a/test/jdk/java/net/httpclient/whitebox/WrapperTestDriver.java Wed Feb 07 15:46:30 2018 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/WrapperTestDriver.java Wed Feb 07 21:45:37 2018 +0000 @@ -23,6 +23,6 @@ /* * @test - * @modules java.net.http - * @run testng java.net.http/java.net.http.WrapperTest + * @modules java.net.http/jdk.internal.net.http + * @run testng java.net.http/jdk.internal.net.http.WrapperTest */ diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/AbstractRandomTest.java --- a/test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/AbstractRandomTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,61 +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 java.net.http; - -import java.util.Random; - -/** Abstract supertype for tests that need random numbers within a given range. */ -public class AbstractRandomTest { - - private static Long getSystemSeed() { - Long seed = null; - try { - // note that Long.valueOf(null) also throws a NumberFormatException - // so if the property is undefined this will still work correctly - seed = Long.valueOf(System.getProperty("seed")); - } catch (NumberFormatException e) { - // do nothing: seed is still null - } - return seed; - } - - private static long getSeed() { - Long seed = getSystemSeed(); - if (seed == null) { - seed = (new Random()).nextLong(); - } - System.out.println("Seed from AbstractRandomTest.getSeed = "+seed+"L"); - return seed; - } - - private static Random random = new Random(getSeed()); - - protected static int randomRange(int lower, int upper) { - if (lower > upper) - throw new IllegalArgumentException("lower > upper"); - int diff = upper - lower; - int r = lower + random.nextInt(diff); - return r - (r % 8); // round down to multiple of 8 (align for longs) - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/AbstractSSLTubeTest.java --- a/test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/AbstractSSLTubeTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,319 +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 java.net.http; - -import java.net.http.internal.common.FlowTube; -import java.net.http.internal.common.SSLTube; -import java.net.http.internal.common.Utils; -import org.testng.annotations.Test; - -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLParameters; -import javax.net.ssl.TrustManagerFactory; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; -import java.util.List; -import java.util.StringTokenizer; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Flow; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.SubmissionPublisher; -import java.util.concurrent.atomic.AtomicLong; - -public class AbstractSSLTubeTest extends AbstractRandomTest { - - public static final long COUNTER = 600; - public static final int LONGS_PER_BUF = 800; - public static final long TOTAL_LONGS = COUNTER * LONGS_PER_BUF; - public static final ByteBuffer SENTINEL = ByteBuffer.allocate(0); - // This is a hack to work around an issue with SubmissionPublisher. - // SubmissionPublisher will call onComplete immediately without forwarding - // remaining pending data if SubmissionPublisher.close() is called when - // there is no demand. In other words, it doesn't wait for the subscriber - // to pull all the data before calling onComplete. - // We use a CountDownLatch to figure out when it is safe to call close(). - // This may cause the test to hang if data are buffered. - protected final CountDownLatch allBytesReceived = new CountDownLatch(1); - - - protected static ByteBuffer getBuffer(long startingAt) { - ByteBuffer buf = ByteBuffer.allocate(LONGS_PER_BUF * 8); - for (int j = 0; j < LONGS_PER_BUF; j++) { - buf.putLong(startingAt++); - } - buf.flip(); - return buf; - } - - protected void run(FlowTube server, - ExecutorService sslExecutor, - CountDownLatch allBytesReceived) throws IOException { - FlowTube client = new SSLTube(createSSLEngine(true), - sslExecutor, - server); - SubmissionPublisher> p = - new SubmissionPublisher<>(ForkJoinPool.commonPool(), - Integer.MAX_VALUE); - FlowTube.TubePublisher begin = p::subscribe; - CompletableFuture completion = new CompletableFuture<>(); - EndSubscriber end = new EndSubscriber(TOTAL_LONGS, completion, allBytesReceived); - client.connectFlows(begin, end); - /* End of wiring */ - - long count = 0; - System.out.printf("Submitting %d buffer arrays\n", COUNTER); - System.out.printf("LoopCount should be %d\n", TOTAL_LONGS); - for (long i = 0; i < COUNTER; i++) { - ByteBuffer b = getBuffer(count); - count += LONGS_PER_BUF; - p.submit(List.of(b)); - } - System.out.println("Finished submission. Waiting for loopback"); - completion.whenComplete((r,t) -> allBytesReceived.countDown()); - try { - allBytesReceived.await(); - } catch (InterruptedException e) { - throw new IOException(e); - } - p.close(); - System.out.println("All bytes received: calling publisher.close()"); - try { - completion.join(); - System.out.println("OK"); - } finally { - sslExecutor.shutdownNow(); - } - } - - protected static void sleep(long millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - - } - } - - /** - * The final subscriber which receives the decrypted looped-back data. Just - * needs to compare the data with what was sent. The given CF is either - * completed exceptionally with an error or normally on success. - */ - protected static class EndSubscriber implements FlowTube.TubeSubscriber { - - private static final int REQUEST_WINDOW = 13; - - private final long nbytes; - private final AtomicLong counter = new AtomicLong(); - private final CompletableFuture completion; - private final CountDownLatch allBytesReceived; - private volatile Flow.Subscription subscription; - private long unfulfilled; - - EndSubscriber(long nbytes, CompletableFuture completion, - CountDownLatch allBytesReceived) { - this.nbytes = nbytes; - this.completion = completion; - this.allBytesReceived = allBytesReceived; - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - unfulfilled = REQUEST_WINDOW; - System.out.println("EndSubscriber request " + REQUEST_WINDOW); - subscription.request(REQUEST_WINDOW); - } - - public static String info(List i) { - StringBuilder sb = new StringBuilder(); - sb.append("size: ").append(Integer.toString(i.size())); - int x = 0; - for (ByteBuffer b : i) - x += b.remaining(); - sb.append(" bytes: ").append(x); - return sb.toString(); - } - - @Override - public void onNext(List buffers) { - if (--unfulfilled == (REQUEST_WINDOW / 2)) { - long req = REQUEST_WINDOW - unfulfilled; - System.out.println("EndSubscriber request " + req); - unfulfilled = REQUEST_WINDOW; - subscription.request(req); - } - - long currval = counter.get(); - if (currval % 500 == 0) { - System.out.println("EndSubscriber: " + currval); - } - System.out.println("EndSubscriber onNext " + Utils.remaining(buffers)); - - for (ByteBuffer buf : buffers) { - while (buf.hasRemaining()) { - long n = buf.getLong(); - if (currval > (TOTAL_LONGS - 50)) { - System.out.println("End: " + currval); - } - if (n != currval++) { - System.out.println("ERROR at " + n + " != " + (currval - 1)); - completion.completeExceptionally(new RuntimeException("ERROR")); - subscription.cancel(); - return; - } - } - } - - counter.set(currval); - if (currval >= TOTAL_LONGS) { - allBytesReceived.countDown(); - } - } - - @Override - public void onError(Throwable throwable) { - System.out.println("EndSubscriber onError " + throwable); - completion.completeExceptionally(throwable); - allBytesReceived.countDown(); - } - - @Override - public void onComplete() { - long n = counter.get(); - if (n != nbytes) { - System.out.printf("nbytes=%d n=%d\n", nbytes, n); - completion.completeExceptionally(new RuntimeException("ERROR AT END")); - } else { - System.out.println("DONE OK"); - completion.complete(null); - } - allBytesReceived.countDown(); - } - - @Override - public String toString() { - return "EndSubscriber"; - } - } - - protected static SSLEngine createSSLEngine(boolean client) throws IOException { - SSLContext context = (new SimpleSSLContext()).get(); - SSLEngine engine = context.createSSLEngine(); - SSLParameters params = context.getSupportedSSLParameters(); - params.setProtocols(new String[]{"TLSv1.2"}); // TODO: This is essential. Needs to be protocol impl - if (client) { - params.setApplicationProtocols(new String[]{"proto1", "proto2"}); // server will choose proto2 - } else { - params.setApplicationProtocols(new String[]{"proto2"}); // server will choose proto2 - } - engine.setSSLParameters(params); - engine.setUseClientMode(client); - return engine; - } - - /** - * Creates a simple usable SSLContext for SSLSocketFactory or a HttpsServer - * using either a given keystore or a default one in the test tree. - * - * Using this class with a security manager requires the following - * permissions to be granted: - * - * permission "java.util.PropertyPermission" "test.src.path", "read"; - * permission java.io.FilePermission "${test.src}/../../../../lib/testlibrary/jdk/testlibrary/testkeys", - * "read"; The exact path above depends on the location of the test. - */ - protected static class SimpleSSLContext { - - private final SSLContext ssl; - - /** - * Loads default keystore from SimpleSSLContext source directory - */ - public SimpleSSLContext() throws IOException { - String paths = System.getProperty("test.src.path"); - StringTokenizer st = new StringTokenizer(paths, File.pathSeparator); - boolean securityExceptions = false; - SSLContext sslContext = null; - while (st.hasMoreTokens()) { - String path = st.nextToken(); - try { - File f = new File(path, "../../../../lib/testlibrary/jdk/testlibrary/testkeys"); - if (f.exists()) { - try (FileInputStream fis = new FileInputStream(f)) { - sslContext = init(fis); - break; - } - } - } catch (SecurityException e) { - // catch and ignore because permission only required - // for one entry on path (at most) - securityExceptions = true; - } - } - if (securityExceptions) { - System.err.println("SecurityExceptions thrown on loading testkeys"); - } - ssl = sslContext; - } - - private SSLContext init(InputStream i) throws IOException { - try { - char[] passphrase = "passphrase".toCharArray(); - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(i, passphrase); - - KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); - kmf.init(ks, passphrase); - - TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); - tmf.init(ks); - - SSLContext ssl = SSLContext.getInstance("TLS"); - ssl.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); - return ssl; - } catch (KeyManagementException | KeyStoreException | - UnrecoverableKeyException | CertificateException | - NoSuchAlgorithmException e) { - throw new RuntimeException(e.getMessage()); - } - } - - public SSLContext get() { - return ssl; - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/FlowTest.java --- a/test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/FlowTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,547 +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 java.net.http; - -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; -import java.nio.ByteBuffer; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; -import java.util.List; -import java.util.Random; -import java.util.StringTokenizer; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Flow; -import java.util.concurrent.Flow.Subscriber; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.SubmissionPublisher; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.*; -import javax.net.ssl.TrustManagerFactory; -import java.net.http.internal.common.Utils; -import org.testng.annotations.Test; -import java.net.http.internal.common.SSLFlowDelegate; - -@Test -public class FlowTest extends AbstractRandomTest { - - private final SubmissionPublisher> srcPublisher; - private final ExecutorService executor; - private static final long COUNTER = 3000; - private static final int LONGS_PER_BUF = 800; - static final long TOTAL_LONGS = COUNTER * LONGS_PER_BUF; - public static final ByteBuffer SENTINEL = ByteBuffer.allocate(0); - static volatile String alpn; - - // This is a hack to work around an issue with SubmissionPublisher. - // SubmissionPublisher will call onComplete immediately without forwarding - // remaining pending data if SubmissionPublisher.close() is called when - // there is no demand. In other words, it doesn't wait for the subscriber - // to pull all the data before calling onComplete. - // We use a CountDownLatch to figure out when it is safe to call close(). - // This may cause the test to hang if data are buffered. - final CountDownLatch allBytesReceived = new CountDownLatch(1); - - private final CompletableFuture completion; - - public FlowTest() throws IOException { - executor = Executors.newCachedThreadPool(); - srcPublisher = new SubmissionPublisher<>(executor, 20, - this::handlePublisherException); - SSLContext ctx = (new SimpleSSLContext()).get(); - SSLEngine engineClient = ctx.createSSLEngine(); - SSLParameters params = ctx.getSupportedSSLParameters(); - params.setApplicationProtocols(new String[]{"proto1", "proto2"}); // server will choose proto2 - params.setProtocols(new String[]{"TLSv1.2"}); // TODO: This is essential. Needs to be protocol impl - engineClient.setSSLParameters(params); - engineClient.setUseClientMode(true); - completion = new CompletableFuture<>(); - SSLLoopbackSubscriber looper = new SSLLoopbackSubscriber(ctx, executor, allBytesReceived); - looper.start(); - EndSubscriber end = new EndSubscriber(TOTAL_LONGS, completion, allBytesReceived); - SSLFlowDelegate sslClient = new SSLFlowDelegate(engineClient, executor, end, looper); - // going to measure how long handshake takes - final long start = System.currentTimeMillis(); - sslClient.alpn().whenComplete((String s, Throwable t) -> { - if (t != null) - t.printStackTrace(); - long endTime = System.currentTimeMillis(); - alpn = s; - System.out.println("ALPN: " + alpn); - long period = (endTime - start); - System.out.printf("Handshake took %d ms\n", period); - }); - Subscriber> reader = sslClient.upstreamReader(); - Subscriber> writer = sslClient.upstreamWriter(); - looper.setReturnSubscriber(reader); - // now connect all the pieces - srcPublisher.subscribe(writer); - String aa = sslClient.alpn().join(); - System.out.println("AAALPN = " + aa); - } - - private void handlePublisherException(Object o, Throwable t) { - System.out.println("Src Publisher exception"); - t.printStackTrace(System.out); - } - - private static ByteBuffer getBuffer(long startingAt) { - ByteBuffer buf = ByteBuffer.allocate(LONGS_PER_BUF * 8); - for (int j = 0; j < LONGS_PER_BUF; j++) { - buf.putLong(startingAt++); - } - buf.flip(); - return buf; - } - - @Test - public void run() { - long count = 0; - System.out.printf("Submitting %d buffer arrays\n", COUNTER); - System.out.printf("LoopCount should be %d\n", TOTAL_LONGS); - for (long i = 0; i < COUNTER; i++) { - ByteBuffer b = getBuffer(count); - count += LONGS_PER_BUF; - srcPublisher.submit(List.of(b)); - } - System.out.println("Finished submission. Waiting for loopback"); - // make sure we don't wait for allBytesReceived in case of error. - completion.whenComplete((r,t) -> allBytesReceived.countDown()); - try { - allBytesReceived.await(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - System.out.println("All bytes received: "); - srcPublisher.close(); - try { - completion.join(); - if (!alpn.equals("proto2")) { - throw new RuntimeException("wrong alpn received"); - } - System.out.println("OK"); - } finally { - executor.shutdownNow(); - } - } - -/* - public static void main(String[]args) throws Exception { - FlowTest test = new FlowTest(); - test.run(); - } -*/ - - /** - * This Subscriber simulates an SSL loopback network. The object itself - * accepts outgoing SSL encrypted data which is looped back via two sockets - * (one of which is an SSLSocket emulating a server). The method - * {@link #setReturnSubscriber(java.util.concurrent.Flow.Subscriber) } - * is used to provide the Subscriber which feeds the incoming side - * of SSLFlowDelegate. Three threads are used to implement this behavior - * and a SubmissionPublisher drives the incoming read side. - *

- * A thread reads from the buffer, writes - * to the client j.n.Socket which is connected to a SSLSocket operating - * in server mode. A second thread loops back data read from the SSLSocket back to the - * client again. A third thread reads the client socket and pushes the data to - * a SubmissionPublisher that drives the reader side of the SSLFlowDelegate - */ - static class SSLLoopbackSubscriber implements Subscriber> { - private final BlockingQueue buffer; - private final Socket clientSock; - private final SSLSocket serverSock; - private final Thread thread1, thread2, thread3; - private volatile Flow.Subscription clientSubscription; - private final SubmissionPublisher> publisher; - private final CountDownLatch allBytesReceived; - - SSLLoopbackSubscriber(SSLContext ctx, - ExecutorService exec, - CountDownLatch allBytesReceived) throws IOException { - SSLServerSocketFactory fac = ctx.getServerSocketFactory(); - SSLServerSocket serv = (SSLServerSocket) fac.createServerSocket(0); - SSLParameters params = serv.getSSLParameters(); - params.setApplicationProtocols(new String[]{"proto2"}); - serv.setSSLParameters(params); - - - int serverPort = serv.getLocalPort(); - clientSock = new Socket("127.0.0.1", serverPort); - serverSock = (SSLSocket) serv.accept(); - this.buffer = new LinkedBlockingQueue<>(); - this.allBytesReceived = allBytesReceived; - thread1 = new Thread(this::clientWriter, "clientWriter"); - thread2 = new Thread(this::serverLoopback, "serverLoopback"); - thread3 = new Thread(this::clientReader, "clientReader"); - publisher = new SubmissionPublisher<>(exec, Flow.defaultBufferSize(), - this::handlePublisherException); - SSLFlowDelegate.Monitor.add(this::monitor); - } - - public void start() { - thread1.start(); - thread2.start(); - thread3.start(); - } - - private void handlePublisherException(Object o, Throwable t) { - System.out.println("Loopback Publisher exception"); - t.printStackTrace(System.out); - } - - private final AtomicInteger readCount = new AtomicInteger(); - - // reads off the SSLSocket the data from the "server" - private void clientReader() { - try { - InputStream is = clientSock.getInputStream(); - final int bufsize = FlowTest.randomRange(512, 16 * 1024); - System.out.println("clientReader: bufsize = " + bufsize); - while (true) { - byte[] buf = new byte[bufsize]; - int n = is.read(buf); - if (n == -1) { - System.out.println("clientReader close: read " - + readCount.get() + " bytes"); - System.out.println("clientReader: got EOF. " - + "Waiting signal to close publisher."); - allBytesReceived.await(); - System.out.println("clientReader: closing publisher"); - publisher.close(); - sleep(2000); - Utils.close(is, clientSock); - return; - } - ByteBuffer bb = ByteBuffer.wrap(buf, 0, n); - readCount.addAndGet(n); - publisher.submit(List.of(bb)); - } - } catch (Throwable e) { - e.printStackTrace(); - Utils.close(clientSock); - } - } - - // writes the encrypted data from SSLFLowDelegate to the j.n.Socket - // which is connected to the SSLSocket emulating a server. - private void clientWriter() { - long nbytes = 0; - try { - OutputStream os = - new BufferedOutputStream(clientSock.getOutputStream()); - - while (true) { - ByteBuffer buf = buffer.take(); - if (buf == FlowTest.SENTINEL) { - // finished - //Utils.sleep(2000); - System.out.println("clientWriter close: " + nbytes + " written"); - clientSock.shutdownOutput(); - System.out.println("clientWriter close return"); - return; - } - int len = buf.remaining(); - int written = writeToStream(os, buf); - assert len == written; - nbytes += len; - assert !buf.hasRemaining() - : "buffer has " + buf.remaining() + " bytes left"; - clientSubscription.request(1); - } - } catch (Throwable e) { - e.printStackTrace(); - } - } - - private int writeToStream(OutputStream os, ByteBuffer buf) throws IOException { - byte[] b = buf.array(); - int offset = buf.arrayOffset() + buf.position(); - int n = buf.limit() - buf.position(); - os.write(b, offset, n); - buf.position(buf.limit()); - os.flush(); - return n; - } - - private final AtomicInteger loopCount = new AtomicInteger(); - - public String monitor() { - return "serverLoopback: loopcount = " + loopCount.toString() - + " clientRead: count = " + readCount.toString(); - } - - // thread2 - private void serverLoopback() { - try { - InputStream is = serverSock.getInputStream(); - OutputStream os = serverSock.getOutputStream(); - final int bufsize = FlowTest.randomRange(512, 16 * 1024); - System.out.println("serverLoopback: bufsize = " + bufsize); - byte[] bb = new byte[bufsize]; - while (true) { - int n = is.read(bb); - if (n == -1) { - sleep(2000); - is.close(); - serverSock.close(); - return; - } - os.write(bb, 0, n); - os.flush(); - loopCount.addAndGet(n); - } - } catch (Throwable e) { - e.printStackTrace(); - } - } - - - /** - * This needs to be called before the chain is subscribed. It can't be - * supplied in the constructor. - */ - public void setReturnSubscriber(Subscriber> returnSubscriber) { - publisher.subscribe(returnSubscriber); - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - clientSubscription = subscription; - clientSubscription.request(5); - } - - @Override - public void onNext(List item) { - try { - for (ByteBuffer b : item) - buffer.put(b); - } catch (InterruptedException e) { - e.printStackTrace(); - Utils.close(clientSock); - } - } - - @Override - public void onError(Throwable throwable) { - throwable.printStackTrace(); - Utils.close(clientSock); - } - - @Override - public void onComplete() { - try { - buffer.put(FlowTest.SENTINEL); - } catch (InterruptedException e) { - e.printStackTrace(); - Utils.close(clientSock); - } - } - } - - /** - * The final subscriber which receives the decrypted looped-back data. - * Just needs to compare the data with what was sent. The given CF is - * either completed exceptionally with an error or normally on success. - */ - static class EndSubscriber implements Subscriber> { - - private final long nbytes; - - private final AtomicLong counter; - private volatile Flow.Subscription subscription; - private final CompletableFuture completion; - private final CountDownLatch allBytesReceived; - - EndSubscriber(long nbytes, - CompletableFuture completion, - CountDownLatch allBytesReceived) { - counter = new AtomicLong(0); - this.nbytes = nbytes; - this.completion = completion; - this.allBytesReceived = allBytesReceived; - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(5); - } - - public static String info(List i) { - StringBuilder sb = new StringBuilder(); - sb.append("size: ").append(Integer.toString(i.size())); - int x = 0; - for (ByteBuffer b : i) - x += b.remaining(); - sb.append(" bytes: " + Integer.toString(x)); - return sb.toString(); - } - - @Override - public void onNext(List buffers) { - long currval = counter.get(); - //if (currval % 500 == 0) { - //System.out.println("End: " + currval); - //} - - for (ByteBuffer buf : buffers) { - while (buf.hasRemaining()) { - long n = buf.getLong(); - //if (currval > (FlowTest.TOTAL_LONGS - 50)) { - //System.out.println("End: " + currval); - //} - if (n != currval++) { - System.out.println("ERROR at " + n + " != " + (currval - 1)); - completion.completeExceptionally(new RuntimeException("ERROR")); - subscription.cancel(); - return; - } - } - } - - counter.set(currval); - subscription.request(1); - if (currval >= TOTAL_LONGS) { - allBytesReceived.countDown(); - } - } - - @Override - public void onError(Throwable throwable) { - allBytesReceived.countDown(); - completion.completeExceptionally(throwable); - } - - @Override - public void onComplete() { - long n = counter.get(); - if (n != nbytes) { - System.out.printf("nbytes=%d n=%d\n", nbytes, n); - completion.completeExceptionally(new RuntimeException("ERROR AT END")); - } else { - System.out.println("DONE OK: counter = " + n); - allBytesReceived.countDown(); - completion.complete(null); - } - } - } - - /** - * Creates a simple usable SSLContext for SSLSocketFactory - * or a HttpsServer using either a given keystore or a default - * one in the test tree. - *

- * Using this class with a security manager requires the following - * permissions to be granted: - *

- * permission "java.util.PropertyPermission" "test.src.path", "read"; - * permission java.io.FilePermission - * "${test.src}/../../../../lib/testlibrary/jdk/testlibrary/testkeys", "read"; - * The exact path above depends on the location of the test. - */ - static class SimpleSSLContext { - - private final SSLContext ssl; - - /** - * Loads default keystore from SimpleSSLContext source directory - */ - public SimpleSSLContext() throws IOException { - String paths = System.getProperty("test.src.path"); - StringTokenizer st = new StringTokenizer(paths, File.pathSeparator); - boolean securityExceptions = false; - SSLContext sslContext = null; - while (st.hasMoreTokens()) { - String path = st.nextToken(); - try { - File f = new File(path, "../../../../lib/testlibrary/jdk/testlibrary/testkeys"); - if (f.exists()) { - try (FileInputStream fis = new FileInputStream(f)) { - sslContext = init(fis); - break; - } - } - } catch (SecurityException e) { - // catch and ignore because permission only required - // for one entry on path (at most) - securityExceptions = true; - } - } - if (securityExceptions) { - System.out.println("SecurityExceptions thrown on loading testkeys"); - } - ssl = sslContext; - } - - private SSLContext init(InputStream i) throws IOException { - try { - char[] passphrase = "passphrase".toCharArray(); - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(i, passphrase); - - KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); - kmf.init(ks, passphrase); - - TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); - tmf.init(ks); - - SSLContext ssl = SSLContext.getInstance("TLS"); - ssl.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); - return ssl; - } catch (KeyManagementException | KeyStoreException | - UnrecoverableKeyException | CertificateException | - NoSuchAlgorithmException e) { - throw new RuntimeException(e.getMessage()); - } - } - - public SSLContext get() { - return ssl; - } - } - - private static void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (Exception e) { - e.printStackTrace(); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/SSLEchoTubeTest.java --- a/test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/SSLEchoTubeTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,420 +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. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package java.net.http; - -import java.net.http.internal.common.Demand; -import java.net.http.internal.common.FlowTube; -import java.net.http.internal.common.SSLTube; -import java.net.http.internal.common.SequentialScheduler; -import java.net.http.internal.common.Utils; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Flow; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - -@Test -public class SSLEchoTubeTest extends AbstractSSLTubeTest { - - @Test - public void runWithEchoServer() throws IOException { - ExecutorService sslExecutor = Executors.newCachedThreadPool(); - - /* Start of wiring */ - /* Emulates an echo server */ - FlowTube server = crossOverEchoServer(sslExecutor); - - run(server, sslExecutor, allBytesReceived); - } - - /** - * Creates a cross-over FlowTube than can be plugged into a client-side - * SSLTube (in place of the SSLLoopbackSubscriber). - * Note that the only method that can be called on the return tube - * is connectFlows(). Calling any other method will trigger an - * InternalError. - * @param sslExecutor an executor - * @return a cross-over FlowTube connected to an EchoTube. - * @throws IOException - */ - private FlowTube crossOverEchoServer(Executor sslExecutor) throws IOException { - LateBindingTube crossOver = new LateBindingTube(); - FlowTube server = new SSLTube(createSSLEngine(false), - sslExecutor, - crossOver); - EchoTube echo = new EchoTube(6); - server.connectFlows(FlowTube.asTubePublisher(echo), FlowTube.asTubeSubscriber(echo)); - - return new CrossOverTube(crossOver); - } - - /** - * A cross-over FlowTube that makes it possible to reverse the direction - * of flows. The typical usage is to connect an two opposite SSLTube, - * one encrypting, one decrypting, to e.g. an EchoTube, with the help - * of a LateBindingTube: - * {@code - * client app => SSLTube => CrossOverTube <= LateBindingTube <= SSLTube <= EchoTube - * } - *

- * Note that the only method that can be called on the CrossOverTube is - * connectFlows(). Calling any other method will cause an InternalError to - * be thrown. - * Also connectFlows() can be called only once. - */ - private static final class CrossOverTube implements FlowTube { - final LateBindingTube tube; - CrossOverTube(LateBindingTube tube) { - this.tube = tube; - } - - @Override - public void subscribe(Flow.Subscriber> subscriber) { - throw newInternalError(); - } - - @Override - public void connectFlows(TubePublisher writePublisher, TubeSubscriber readSubscriber) { - tube.start(writePublisher, readSubscriber); - } - - @Override - public boolean isFinished() { - return tube.isFinished(); - } - - Error newInternalError() { - InternalError error = new InternalError(); - error.printStackTrace(System.out); - return error; - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - throw newInternalError(); - } - - @Override - public void onError(Throwable throwable) { - throw newInternalError(); - } - - @Override - public void onComplete() { - throw newInternalError(); - } - - @Override - public void onNext(List item) { - throw newInternalError(); - } - } - - /** - * A late binding tube that makes it possible to create an - * SSLTube before the right-hand-side tube has been created. - * The typical usage is to make it possible to connect two - * opposite SSLTube (one encrypting, one decrypting) through a - * CrossOverTube: - * {@code - * client app => SSLTube => CrossOverTube <= LateBindingTube <= SSLTube <= EchoTube - * } - *

- * Note that this class only supports a single call to start(): it cannot be - * subscribed more than once from its left-hand-side (the cross over tube side). - */ - private static class LateBindingTube implements FlowTube { - - final CompletableFuture>> futurePublisher - = new CompletableFuture<>(); - final ConcurrentLinkedQueue>>> queue - = new ConcurrentLinkedQueue<>(); - AtomicReference>> subscriberRef = new AtomicReference<>(); - SequentialScheduler scheduler = SequentialScheduler.synchronizedScheduler(this::loop); - AtomicReference errorRef = new AtomicReference<>(); - private volatile boolean finished; - private volatile boolean completed; - - - public void start(Flow.Publisher> publisher, - Flow.Subscriber> subscriber) { - subscriberRef.set(subscriber); - futurePublisher.complete(publisher); - scheduler.runOrSchedule(); - } - - @Override - public boolean isFinished() { - return finished; - } - - @Override - public void subscribe(Flow.Subscriber> subscriber) { - futurePublisher.thenAccept((p) -> p.subscribe(subscriber)); - scheduler.runOrSchedule(); - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - queue.add((s) -> s.onSubscribe(subscription)); - scheduler.runOrSchedule(); - } - - @Override - public void onNext(List item) { - queue.add((s) -> s.onNext(item)); - scheduler.runOrSchedule(); - } - - @Override - public void onError(Throwable throwable) { - System.out.println("LateBindingTube onError"); - throwable.printStackTrace(System.out); - queue.add((s) -> { - errorRef.compareAndSet(null, throwable); - try { - System.out.println("LateBindingTube subscriber onError: " + throwable); - s.onError(errorRef.get()); - } finally { - finished = true; - System.out.println("LateBindingTube finished"); - } - }); - scheduler.runOrSchedule(); - } - - @Override - public void onComplete() { - System.out.println("LateBindingTube completing"); - queue.add((s) -> { - completed = true; - try { - System.out.println("LateBindingTube complete subscriber"); - s.onComplete(); - } finally { - finished = true; - System.out.println("LateBindingTube finished"); - } - }); - scheduler.runOrSchedule(); - } - - private void loop() { - if (finished) { - scheduler.stop(); - return; - } - Flow.Subscriber> subscriber = subscriberRef.get(); - if (subscriber == null) return; - try { - Consumer>> s; - while ((s = queue.poll()) != null) { - s.accept(subscriber); - } - } catch (Throwable t) { - if (errorRef.compareAndSet(null, t)) { - onError(t); - } - } - } - } - - /** - * An echo tube that just echoes back whatever bytes it receives. - * This cannot be plugged to the right-hand-side of an SSLTube - * since handshake data cannot be simply echoed back, and - * application data most likely also need to be decrypted and - * re-encrypted. - */ - private static final class EchoTube implements FlowTube { - - private final static Object EOF = new Object(); - private final Executor executor = Executors.newSingleThreadExecutor(); - - private final Queue queue = new ConcurrentLinkedQueue<>(); - private final int maxQueueSize; - private final SequentialScheduler processingScheduler = - new SequentialScheduler(createProcessingTask()); - - /* Writing into this tube */ - private volatile long requested; - private Flow.Subscription subscription; - - /* Reading from this tube */ - private final Demand demand = new Demand(); - private final AtomicBoolean cancelled = new AtomicBoolean(); - private Flow.Subscriber> subscriber; - - private EchoTube(int maxBufferSize) { - if (maxBufferSize < 1) - throw new IllegalArgumentException(); - this.maxQueueSize = maxBufferSize; - } - - @Override - public void subscribe(Flow.Subscriber> subscriber) { - this.subscriber = subscriber; - System.out.println("EchoTube got subscriber: " + subscriber); - this.subscriber.onSubscribe(new InternalSubscription()); - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - System.out.println("EchoTube request: " + maxQueueSize); - (this.subscription = subscription).request(requested = maxQueueSize); - } - - private void requestMore() { - Flow.Subscription s = subscription; - if (s == null || cancelled.get()) return; - long unfulfilled = queue.size() + --requested; - if (unfulfilled <= maxQueueSize/2) { - long req = maxQueueSize - unfulfilled; - requested += req; - s.request(req); - System.out.printf("EchoTube request: %s [requested:%s, queue:%s, unfulfilled:%s]%n", - req, requested-req, queue.size(), unfulfilled ); - } - } - - @Override - public void onNext(List item) { - System.out.printf("EchoTube add %s [requested:%s, queue:%s]%n", - Utils.remaining(item), requested, queue.size()); - queue.add(item); - processingScheduler.runOrSchedule(executor); - } - - @Override - public void onError(Throwable throwable) { - System.out.println("EchoTube add " + throwable); - queue.add(throwable); - processingScheduler.runOrSchedule(executor); - } - - @Override - public void onComplete() { - System.out.println("EchoTube add EOF"); - queue.add(EOF); - processingScheduler.runOrSchedule(executor); - } - - @Override - public boolean isFinished() { - return cancelled.get(); - } - - private class InternalSubscription implements Flow.Subscription { - - @Override - public void request(long n) { - System.out.println("EchoTube got request: " + n); - if (n <= 0) { - throw new InternalError(); - } - if (demand.increase(n)) { - processingScheduler.runOrSchedule(executor); - } - } - - @Override - public void cancel() { - cancelled.set(true); - } - } - - @Override - public String toString() { - return "EchoTube"; - } - - int transmitted = 0; - private SequentialScheduler.RestartableTask createProcessingTask() { - return new SequentialScheduler.CompleteRestartableTask() { - - @Override - protected void run() { - try { - while (!cancelled.get()) { - Object item = queue.peek(); - if (item == null) { - System.out.printf("EchoTube: queue empty, requested=%s, demand=%s, transmitted=%s%n", - requested, demand.get(), transmitted); - requestMore(); - return; - } - try { - System.out.printf("EchoTube processing item, requested=%s, demand=%s, transmitted=%s%n", - requested, demand.get(), transmitted); - if (item instanceof List) { - if (!demand.tryDecrement()) { - System.out.println("EchoTube no demand"); - return; - } - @SuppressWarnings("unchecked") - List bytes = (List) item; - Object removed = queue.remove(); - assert removed == item; - System.out.println("EchoTube processing " - + Utils.remaining(bytes)); - transmitted++; - subscriber.onNext(bytes); - requestMore(); - } else if (item instanceof Throwable) { - cancelled.set(true); - Object removed = queue.remove(); - assert removed == item; - System.out.println("EchoTube processing " + item); - subscriber.onError((Throwable) item); - } else if (item == EOF) { - cancelled.set(true); - Object removed = queue.remove(); - assert removed == item; - System.out.println("EchoTube processing EOF"); - subscriber.onComplete(); - } else { - throw new InternalError(String.valueOf(item)); - } - } finally { - } - } - } catch(Throwable t) { - t.printStackTrace(); - throw t; - } - } - }; - } - } - } diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/SSLTubeTest.java --- a/test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/SSLTubeTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,275 +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 java.net.http; - -import java.net.http.internal.common.FlowTube; -import java.net.http.internal.common.SSLFlowDelegate; -import java.net.http.internal.common.Utils; -import org.testng.annotations.Test; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLParameters; -import javax.net.ssl.SSLServerSocket; -import javax.net.ssl.SSLServerSocketFactory; -import javax.net.ssl.SSLSocket; -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Flow; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.SubmissionPublisher; -import java.util.concurrent.atomic.AtomicInteger; - -@Test -public class SSLTubeTest extends AbstractSSLTubeTest { - - @Test - public void runWithSSLLoopackServer() throws IOException { - ExecutorService sslExecutor = Executors.newCachedThreadPool(); - - /* Start of wiring */ - /* Emulates an echo server */ - SSLLoopbackSubscriber server = - new SSLLoopbackSubscriber((new SimpleSSLContext()).get(), - sslExecutor, - allBytesReceived); - server.start(); - - run(server, sslExecutor, allBytesReceived); - } - - /** - * This is a copy of the SSLLoopbackSubscriber used in FlowTest - */ - private static class SSLLoopbackSubscriber implements FlowTube { - private final BlockingQueue buffer; - private final Socket clientSock; - private final SSLSocket serverSock; - private final Thread thread1, thread2, thread3; - private volatile Flow.Subscription clientSubscription; - private final SubmissionPublisher> publisher; - private final CountDownLatch allBytesReceived; - - SSLLoopbackSubscriber(SSLContext ctx, - ExecutorService exec, - CountDownLatch allBytesReceived) throws IOException { - SSLServerSocketFactory fac = ctx.getServerSocketFactory(); - SSLServerSocket serv = (SSLServerSocket) fac.createServerSocket(0); - SSLParameters params = serv.getSSLParameters(); - params.setApplicationProtocols(new String[]{"proto2"}); - serv.setSSLParameters(params); - - - int serverPort = serv.getLocalPort(); - clientSock = new Socket("127.0.0.1", serverPort); - serverSock = (SSLSocket) serv.accept(); - this.buffer = new LinkedBlockingQueue<>(); - this.allBytesReceived = allBytesReceived; - thread1 = new Thread(this::clientWriter, "clientWriter"); - thread2 = new Thread(this::serverLoopback, "serverLoopback"); - thread3 = new Thread(this::clientReader, "clientReader"); - publisher = new SubmissionPublisher<>(exec, Flow.defaultBufferSize(), - this::handlePublisherException); - SSLFlowDelegate.Monitor.add(this::monitor); - } - - public void start() { - thread1.start(); - thread2.start(); - thread3.start(); - } - - private void handlePublisherException(Object o, Throwable t) { - System.out.println("Loopback Publisher exception"); - t.printStackTrace(System.out); - } - - private final AtomicInteger readCount = new AtomicInteger(); - - // reads off the SSLSocket the data from the "server" - private void clientReader() { - try { - InputStream is = clientSock.getInputStream(); - final int bufsize = randomRange(512, 16 * 1024); - System.out.println("clientReader: bufsize = " + bufsize); - while (true) { - byte[] buf = new byte[bufsize]; - int n = is.read(buf); - if (n == -1) { - System.out.println("clientReader close: read " - + readCount.get() + " bytes"); - System.out.println("clientReader: waiting signal to close publisher"); - allBytesReceived.await(); - System.out.println("clientReader: closing publisher"); - publisher.close(); - sleep(2000); - Utils.close(is, clientSock); - return; - } - ByteBuffer bb = ByteBuffer.wrap(buf, 0, n); - readCount.addAndGet(n); - publisher.submit(List.of(bb)); - } - } catch (Throwable e) { - e.printStackTrace(); - Utils.close(clientSock); - } - } - - // writes the encrypted data from SSLFLowDelegate to the j.n.Socket - // which is connected to the SSLSocket emulating a server. - private void clientWriter() { - long nbytes = 0; - try { - OutputStream os = - new BufferedOutputStream(clientSock.getOutputStream()); - - while (true) { - ByteBuffer buf = buffer.take(); - if (buf == SENTINEL) { - // finished - //Utils.sleep(2000); - System.out.println("clientWriter close: " + nbytes + " written"); - clientSock.shutdownOutput(); - System.out.println("clientWriter close return"); - return; - } - int len = buf.remaining(); - int written = writeToStream(os, buf); - assert len == written; - nbytes += len; - assert !buf.hasRemaining() - : "buffer has " + buf.remaining() + " bytes left"; - clientSubscription.request(1); - } - } catch (Throwable e) { - e.printStackTrace(); - } - } - - private int writeToStream(OutputStream os, ByteBuffer buf) throws IOException { - byte[] b = buf.array(); - int offset = buf.arrayOffset() + buf.position(); - int n = buf.limit() - buf.position(); - os.write(b, offset, n); - buf.position(buf.limit()); - os.flush(); - return n; - } - - private final AtomicInteger loopCount = new AtomicInteger(); - - public String monitor() { - return "serverLoopback: loopcount = " + loopCount.toString() - + " clientRead: count = " + readCount.toString(); - } - - // thread2 - private void serverLoopback() { - try { - InputStream is = serverSock.getInputStream(); - OutputStream os = serverSock.getOutputStream(); - final int bufsize = randomRange(512, 16 * 1024); - System.out.println("serverLoopback: bufsize = " + bufsize); - byte[] bb = new byte[bufsize]; - while (true) { - int n = is.read(bb); - if (n == -1) { - sleep(2000); - is.close(); - os.close(); - serverSock.close(); - return; - } - os.write(bb, 0, n); - os.flush(); - loopCount.addAndGet(n); - } - } catch (Throwable e) { - e.printStackTrace(); - } - } - - - /** - * This needs to be called before the chain is subscribed. It can't be - * supplied in the constructor. - */ - public void setReturnSubscriber(Flow.Subscriber> returnSubscriber) { - publisher.subscribe(returnSubscriber); - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - clientSubscription = subscription; - clientSubscription.request(5); - } - - @Override - public void onNext(List item) { - try { - for (ByteBuffer b : item) - buffer.put(b); - } catch (InterruptedException e) { - e.printStackTrace(); - Utils.close(clientSock); - } - } - - @Override - public void onError(Throwable throwable) { - throwable.printStackTrace(); - Utils.close(clientSock); - } - - @Override - public void onComplete() { - try { - buffer.put(SENTINEL); - } catch (InterruptedException e) { - e.printStackTrace(); - Utils.close(clientSock); - } - } - - @Override - public boolean isFinished() { - return false; - } - - @Override - public void subscribe(Flow.Subscriber> subscriber) { - publisher.subscribe(subscriber); - } - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/WrapperTest.java --- a/test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/WrapperTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,263 +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 java.net.http; - -import java.nio.ByteBuffer; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.*; -import java.util.concurrent.atomic.*; -import org.testng.annotations.Test; -import java.net.http.internal.common.SubscriberWrapper; - -@Test -public class WrapperTest { - static final int LO_PRI = 1; - static final int HI_PRI = 2; - static final int NUM_HI_PRI = 240; - static final int BUFSIZE = 1016; - static final int BUFSIZE_INT = BUFSIZE/4; - static final int HI_PRI_FREQ = 40; - - static final int TOTAL = 10000; - //static final int TOTAL = 500; - - final SubmissionPublisher> publisher; - final SubscriberWrapper sub1, sub2, sub3; - final ExecutorService executor = Executors.newCachedThreadPool(); - volatile int hipricount = 0; - - void errorHandler(Flow.Subscriber> sub, Throwable t) { - System.err.printf("Exception from %s : %s\n", sub.toString(), t.toString()); - } - - public WrapperTest() { - publisher = new SubmissionPublisher<>(executor, 600, - (a, b) -> { - errorHandler(a, b); - }); - - CompletableFuture notif = new CompletableFuture<>(); - LastSubscriber ls = new LastSubscriber(notif); - sub1 = new Filter1(ls); - sub2 = new Filter2(sub1); - sub3 = new Filter2(sub2); - } - - public class Filter2 extends SubscriberWrapper { - Filter2(SubscriberWrapper wrapper) { - super(wrapper); - } - - // reverse the order of the bytes in each buffer - public void incoming(List list, boolean complete) { - List out = new LinkedList<>(); - for (ByteBuffer inbuf : list) { - int size = inbuf.remaining(); - ByteBuffer outbuf = ByteBuffer.allocate(size); - for (int i=size; i>0; i--) { - byte b = inbuf.get(i-1); - outbuf.put(b); - } - outbuf.flip(); - out.add(outbuf); - } - if (complete) System.out.println("Filter2.complete"); - outgoing(out, complete); - } - - protected long windowUpdate(long currval) { - return currval == 0 ? 1 : 0; - } - } - - volatile int filter1Calls = 0; // every third call we insert hi pri data - - ByteBuffer getHiPri(int val) { - ByteBuffer buf = ByteBuffer.allocate(8); - buf.putInt(HI_PRI); - buf.putInt(val); - buf.flip(); - return buf; - } - - volatile int hiPriAdded = 0; - - public class Filter1 extends SubscriberWrapper { - Filter1(Flow.Subscriber> downstreamSubscriber) - { - super(); - subscribe(downstreamSubscriber); - } - - // Inserts up to NUM_HI_PRI hi priority buffers into flow - protected void incoming(List in, boolean complete) { - if ((++filter1Calls % HI_PRI_FREQ) == 0 && (hiPriAdded++ < NUM_HI_PRI)) { - sub1.outgoing(getHiPri(hipricount++), false); - } - // pass data thru - if (complete) System.out.println("Filter1.complete"); - outgoing(in, complete); - } - - protected long windowUpdate(long currval) { - return currval == 0 ? 1 : 0; - } - } - - /** - * Final subscriber in the chain. Compares the data sent by the original - * publisher. - */ - static public class LastSubscriber implements Flow.Subscriber> { - volatile Flow.Subscription subscription; - volatile int hipriCounter=0; - volatile int lopriCounter=0; - final CompletableFuture cf; - - LastSubscriber(CompletableFuture cf) { - this.cf = cf; - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(50); // say - } - - private void error(String...args) { - StringBuilder sb = new StringBuilder(); - for (String s : args) { - sb.append(s); - sb.append(' '); - } - String msg = sb.toString(); - System.out.println("Error: " + msg); - RuntimeException e = new RuntimeException(msg); - cf.completeExceptionally(e); - subscription.cancel(); // This is where we need a variant that include exception - } - - private void check(ByteBuffer buf) { - int type = buf.getInt(); - if (type == HI_PRI) { - // check next int is hi pri counter - int c = buf.getInt(); - if (c != hipriCounter) - error("hi pri counter", Integer.toString(c), Integer.toString(hipriCounter)); - hipriCounter++; - } else { - while (buf.hasRemaining()) { - if (buf.getInt() != lopriCounter) - error("lo pri counter", Integer.toString(lopriCounter)); - lopriCounter++; - } - } - } - - @Override - public void onNext(List items) { - for (ByteBuffer item : items) - check(item); - subscription.request(1); - } - - @Override - public void onError(Throwable throwable) { - error(throwable.getMessage()); - } - - @Override - public void onComplete() { - if (hipriCounter != NUM_HI_PRI) - error("hi pri at end wrong", Integer.toString(hipriCounter), Integer.toString(NUM_HI_PRI)); - else { - System.out.println("LastSubscriber.complete"); - cf.complete(null); // success - } - } - } - - List getBuffer(int c) { - ByteBuffer buf = ByteBuffer.allocate(BUFSIZE+4); - buf.putInt(LO_PRI); - for (int i=0; i completion = sub3.completion(); - publisher.subscribe(sub3); - // now submit a load of data - int counter = 0; - for (int i = 0; i < TOTAL; i++) { - List bufs = getBuffer(counter); - //if (i==2) - //bufs.get(0).putInt(41, 1234); // error - counter += BUFSIZE_INT; - publisher.submit(bufs); - //if (i % 1000 == 0) - //Thread.sleep(1000); - //if (i == 99) { - //publisher.closeExceptionally(new RuntimeException("Test error")); - //errorTest = true; - //break; - //} - } - if (!errorTest) { - publisher.close(); - } - System.out.println("Publisher completed"); - completion.join(); - System.out.println("Subscribers completed ok"); - } finally { - executor.shutdownNow(); - } - } - - static void display(CompletableFuture cf) { - System.out.print (cf); - if (!cf.isDone()) - return; - try { - cf.join(); // wont block - } catch (Exception e) { - System.out.println(" " + e); - } - } - -/* - public static void main(String[] args) throws InterruptedException { - WrapperTest test = new WrapperTest(); - test.run(); - } -*/ -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/internal/ConnectionPoolTest.java --- a/test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/internal/ConnectionPoolTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,252 +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. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package java.net.http.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 java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.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 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 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 item) { error();} - @Override - public void subscribe(Flow.Subscriber> 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 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() {return error();} - @Override public HttpClient.Redirect followRedirects() {return error();} - @Override public Optional proxy() {return error();} - @Override public SSLContext sslContext() {return error();} - @Override public SSLParameters sslParameters() {return error();} - @Override public Optional authenticator() {return error();} - @Override public HttpClient.Version version() {return HttpClient.Version.HTTP_1_1;} - @Override public Optional executor() {return error();} - @Override - public HttpResponse send(HttpRequest req, - HttpResponse.BodyHandler responseBodyHandler) - throws IOException, InterruptedException { - return error(); - } - @Override - public CompletableFuture> sendAsync(HttpRequest req, - HttpResponse.BodyHandler responseBodyHandler) { - return error(); - } - @Override - public CompletableFuture> sendAsync(HttpRequest req, - HttpResponse.BodyHandler bodyHandler, - HttpResponse.PushPromiseHandler multiHandler) { - return error(); - } - } - -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/internal/Http1HeaderParserTest.java --- a/test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/internal/Http1HeaderParserTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,379 +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. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package java.net.http.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 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> messageHeaderMap = m.getHeaders(); - int available = bais.available(); - - Http1HeaderParser decoder = new Http1HeaderParser(); - ByteBuffer b = ByteBuffer.wrap(bytes); - decoder.parse(b); - Map> 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> map = new HashMap<>(); - for (Map.Entry> 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 buffers = IntStream.range(0, bytes.length) - .mapToObj(i -> ByteBuffer.wrap(bytes, i, 1)) - .collect(toList()); - while (decoder.parse(buffers.remove(0)) != true); - Map> 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 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> expected, - Map> 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> e : expected.entrySet()) { - String key = e.getKey(); - List values = e.getValue(); - - boolean found = false; - for (Map.Entry> other: actual.entrySet()) { - if (key.equalsIgnoreCase(other.getKey())) { - found = true; - List 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> map) { - StringBuilder sb = new StringBuilder(); - List sortedKeys = new ArrayList(map.keySet()); - Collections.sort(sortedKeys); - for (String key : sortedKeys) { - List 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 */ } - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/internal/RawChannelTest.java --- a/test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/internal/RawChannelTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,333 +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. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package java.net.http.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 java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.internal.websocket.RawChannel; -import java.net.http.internal.websocket.WebSocketRequest; -import org.testng.annotations.Test; -import static java.net.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()); - 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)]; - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/internal/SelectorTest.java --- a/test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/internal/SelectorTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,224 +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. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package java.net.http.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 java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import org.testng.annotations.Test; -import java.net.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 java.net.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()); - 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); - } - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/internal/common/DemandTest.java --- a/test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/internal/common/DemandTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,202 +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 java.net.http.internal.common; - -import org.testng.annotations.Test; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.CyclicBarrier; -import java.util.concurrent.atomic.AtomicReference; - -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertTrue; - -public class DemandTest { - - @Test - public void test01() { - assertTrue(new Demand().isFulfilled()); - } - - @Test - public void test011() { - Demand d = new Demand(); - d.increase(3); - d.decreaseAndGet(3); - assertTrue(d.isFulfilled()); - } - - @Test - public void test02() { - Demand d = new Demand(); - d.increase(1); - assertFalse(d.isFulfilled()); - } - - @Test - public void test03() { - Demand d = new Demand(); - d.increase(3); - assertEquals(d.decreaseAndGet(3), 3); - } - - @Test - public void test04() { - Demand d = new Demand(); - d.increase(3); - assertEquals(d.decreaseAndGet(5), 3); - } - - @Test - public void test05() { - Demand d = new Demand(); - d.increase(7); - assertEquals(d.decreaseAndGet(4), 4); - } - - @Test - public void test06() { - Demand d = new Demand(); - assertEquals(d.decreaseAndGet(3), 0); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void test07() { - Demand d = new Demand(); - d.increase(0); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void test08() { - Demand d = new Demand(); - d.increase(-1); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void test09() { - Demand d = new Demand(); - d.increase(10); - d.decreaseAndGet(0); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void test10() { - Demand d = new Demand(); - d.increase(13); - d.decreaseAndGet(-3); - } - - @Test - public void test11() { - Demand d = new Demand(); - d.increase(1); - assertTrue(d.tryDecrement()); - } - - @Test - public void test12() { - Demand d = new Demand(); - d.increase(2); - assertTrue(d.tryDecrement()); - } - - @Test - public void test14() { - Demand d = new Demand(); - assertFalse(d.tryDecrement()); - } - - @Test - public void test141() { - Demand d = new Demand(); - d.increase(Long.MAX_VALUE); - assertFalse(d.isFulfilled()); - } - - @Test - public void test142() { - Demand d = new Demand(); - d.increase(Long.MAX_VALUE); - d.increase(1); - assertFalse(d.isFulfilled()); - } - - @Test - public void test143() { - Demand d = new Demand(); - d.increase(Long.MAX_VALUE); - d.increase(1); - assertFalse(d.isFulfilled()); - } - - @Test - public void test144() { - Demand d = new Demand(); - d.increase(Long.MAX_VALUE); - d.increase(Long.MAX_VALUE); - d.decreaseAndGet(3); - d.decreaseAndGet(5); - assertFalse(d.isFulfilled()); - } - - @Test - public void test145() { - Demand d = new Demand(); - d.increase(Long.MAX_VALUE); - d.decreaseAndGet(Long.MAX_VALUE); - assertTrue(d.isFulfilled()); - } - - @Test(invocationCount = 32) - public void test15() throws InterruptedException { - int N = Math.max(2, Runtime.getRuntime().availableProcessors() + 1); - int M = ((N + 1) * N) / 2; // 1 + 2 + 3 + ... N - Demand d = new Demand(); - d.increase(M); - CyclicBarrier start = new CyclicBarrier(N); - CountDownLatch stop = new CountDownLatch(N); - AtomicReference error = new AtomicReference<>(); - for (int i = 0; i < N; i++) { - int j = i + 1; - new Thread(() -> { - try { - start.await(); - } catch (Exception e) { - error.compareAndSet(null, e); - } - try { - assertEquals(d.decreaseAndGet(j), j); - } catch (Throwable t) { - error.compareAndSet(null, t); - } finally { - stop.countDown(); - } - }).start(); - } - stop.await(); - assertTrue(d.isFulfilled()); - assertEquals(error.get(), null); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/internal/common/MinimalFutureTest.java --- a/test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/internal/common/MinimalFutureTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,161 +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. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package java.net.http.internal.common; - -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import static org.testng.Assert.assertThrows; - -public class MinimalFutureTest { - - @Test(dataProvider = "futures") - public void test(CompletableFuture mf) { - ExecutorService executor = Executors.newSingleThreadExecutor(); - try { - assertNoObtrusion(mf.thenApply(MinimalFutureTest::apply)); - assertNoObtrusion(mf.thenApplyAsync(MinimalFutureTest::apply)); - assertNoObtrusion(mf.thenApplyAsync(MinimalFutureTest::apply, executor)); - - assertNoObtrusion(mf.thenAccept(MinimalFutureTest::accept)); - assertNoObtrusion(mf.thenAcceptAsync(MinimalFutureTest::accept)); - assertNoObtrusion(mf.thenAcceptAsync(MinimalFutureTest::accept, executor)); - - assertNoObtrusion(mf.thenRun(MinimalFutureTest::run)); - assertNoObtrusion(mf.thenRunAsync(MinimalFutureTest::run)); - assertNoObtrusion(mf.thenRunAsync(MinimalFutureTest::run, executor)); - - assertNoObtrusion(mf.thenCombine(otherFuture(), MinimalFutureTest::apply)); - assertNoObtrusion(mf.thenCombineAsync(otherFuture(), MinimalFutureTest::apply)); - assertNoObtrusion(mf.thenCombineAsync(otherFuture(), MinimalFutureTest::apply, executor)); - - assertNoObtrusion(mf.thenAcceptBoth(otherFuture(), MinimalFutureTest::accept)); - assertNoObtrusion(mf.thenAcceptBothAsync(otherFuture(), MinimalFutureTest::accept)); - assertNoObtrusion(mf.thenAcceptBothAsync(otherFuture(), MinimalFutureTest::accept, executor)); - - assertNoObtrusion(mf.runAfterBoth(otherFuture(), MinimalFutureTest::run)); - assertNoObtrusion(mf.runAfterBothAsync(otherFuture(), MinimalFutureTest::run)); - assertNoObtrusion(mf.runAfterBothAsync(otherFuture(), MinimalFutureTest::run, executor)); - - // "either" methods may return something else if otherFuture() is - // not MinimalFuture - - assertNoObtrusion(mf.applyToEither(otherFuture(), MinimalFutureTest::apply)); - assertNoObtrusion(mf.applyToEitherAsync(otherFuture(), MinimalFutureTest::apply)); - assertNoObtrusion(mf.applyToEitherAsync(otherFuture(), MinimalFutureTest::apply, executor)); - - assertNoObtrusion(mf.acceptEither(otherFuture(), MinimalFutureTest::accept)); - assertNoObtrusion(mf.acceptEitherAsync(otherFuture(), MinimalFutureTest::accept)); - assertNoObtrusion(mf.acceptEitherAsync(otherFuture(), MinimalFutureTest::accept, executor)); - - assertNoObtrusion(mf.runAfterEither(otherFuture(), MinimalFutureTest::run)); - assertNoObtrusion(mf.runAfterEitherAsync(otherFuture(), MinimalFutureTest::run)); - assertNoObtrusion(mf.runAfterEitherAsync(otherFuture(), MinimalFutureTest::run, executor)); - - assertNoObtrusion(mf.thenCompose(MinimalFutureTest::completionStageOf)); - assertNoObtrusion(mf.thenComposeAsync(MinimalFutureTest::completionStageOf)); - assertNoObtrusion(mf.thenComposeAsync(MinimalFutureTest::completionStageOf, executor)); - - assertNoObtrusion(mf.handle(MinimalFutureTest::relay)); - assertNoObtrusion(mf.handleAsync(MinimalFutureTest::relay)); - assertNoObtrusion(mf.handleAsync(MinimalFutureTest::relay, executor)); - - assertNoObtrusion(mf.whenComplete(MinimalFutureTest::accept)); - assertNoObtrusion(mf.whenCompleteAsync(MinimalFutureTest::accept)); - assertNoObtrusion(mf.whenCompleteAsync(MinimalFutureTest::accept, executor)); - - assertNoObtrusion(mf.toCompletableFuture()); - assertNoObtrusion(mf.exceptionally(t -> null)); - - assertNoObtrusion(mf); - assertNoObtrusion(mf.copy()); - assertNoObtrusion(mf.newIncompleteFuture()); - } finally { - executor.shutdownNow(); - } - } - - private static CompletableFuture otherFuture() { - return MinimalFuture.completedFuture(new Object()); - } - - private static Object relay(Object r, Throwable e) { - if (e != null) - throw new CompletionException(e); - else - return r; - } - - private static CompletableFuture completionStageOf(Object r) { - return new CompletableFuture<>(); - } - - private static void accept(Object arg) { - } - - private static void accept(Object arg1, Object arg2) { - } - - private static void run() { - } - - private static Object apply(Object arg) { - return new Object(); - } - - private static Object apply(Object arg1, Object arg2) { - return new Object(); - } - - - @DataProvider(name = "futures") - public Object[][] futures() { - - MinimalFuture mf = new MinimalFuture<>(); - mf.completeExceptionally(new Throwable()); - - MinimalFuture mf1 = new MinimalFuture<>(); - mf1.complete(new Object()); - - return new Object[][]{ - new Object[]{new MinimalFuture<>()}, - new Object[]{MinimalFuture.failedFuture(new Throwable())}, - new Object[]{MinimalFuture.completedFuture(new Object())}, - new Object[]{mf}, - new Object[]{mf1}, - }; - } - - private void assertNoObtrusion(CompletableFuture cf) { - assertThrows(UnsupportedOperationException.class, - () -> cf.obtrudeValue(null)); - assertThrows(UnsupportedOperationException.class, - () -> cf.obtrudeException(new RuntimeException())); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/internal/frame/FramesDecoderTest.java --- a/test/jdk/java/net/httpclient/whitebox/java.net.http/java/net/http/internal/frame/FramesDecoderTest.java Wed Feb 07 15:46:30 2018 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,140 +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. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package java.net.http.internal.frame; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import org.testng.Assert; -import org.testng.annotations.Test; -import static java.lang.System.out; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.testng.Assert.*; - -public class FramesDecoderTest { - - abstract class TestFrameProcessor implements FramesDecoder.FrameProcessor { - protected volatile int count; - public int numberOfFramesDecoded() { return count; } - } - - /** - * Verifies that a ByteBuffer containing more that one frame, destined - * to be returned to the user's subscriber, i.e. a data frame, does not - * inadvertently expose the following frame ( between its limit and - * capacity ). - */ - @Test - public void decodeDataFrameFollowedByAnother() throws Exception { - // input frames for to the decoder - List data1 = List.of(ByteBuffer.wrap("XXXX".getBytes(UTF_8))); - DataFrame dataFrame1 = new DataFrame(1, 0, data1); - List data2 = List.of(ByteBuffer.wrap("YYYY".getBytes(UTF_8))); - DataFrame dataFrame2 = new DataFrame(1, 0, data2); - - List buffers = new ArrayList<>(); - FramesEncoder encoder = new FramesEncoder(); - buffers.addAll(encoder.encodeFrame(dataFrame1)); - buffers.addAll(encoder.encodeFrame(dataFrame2)); - - ByteBuffer combined = ByteBuffer.allocate(1024); - buffers.stream().forEach(combined::put); - combined.flip(); - - TestFrameProcessor testFrameProcessor = new TestFrameProcessor() { - @Override - public void processFrame(Http2Frame frame) throws IOException { - assertTrue(frame instanceof DataFrame); - DataFrame dataFrame = (DataFrame) frame; - List list = dataFrame.getData(); - assertEquals(list.size(), 1); - ByteBuffer data = list.get(0); - byte[] bytes = new byte[data.remaining()]; - data.get(bytes); - if (count == 0) { - assertEquals(new String(bytes, UTF_8), "XXXX"); - out.println("First data received:" + data); - assertEquals(data.position(), data.limit()); // since bytes read - assertEquals(data.limit(), data.capacity()); - } else { - assertEquals(new String(bytes, UTF_8), "YYYY"); - out.println("Second data received:" + data); - } - count++; - } - }; - FramesDecoder decoder = new FramesDecoder(testFrameProcessor); - - out.println("Sending " + combined + " to decoder: "); - decoder.decode(combined); - Assert.assertEquals(testFrameProcessor.numberOfFramesDecoded(), 2); - } - - - /** - * Verifies that a ByteBuffer containing ONLY data one frame, destined - * to be returned to the user's subscriber, does not restrict the capacity. - * The complete buffer ( all its capacity ), since no longer used by the - * HTTP Client, should be returned to the user. - */ - @Test - public void decodeDataFrameEnsureNotCapped() throws Exception { - // input frames for to the decoder - List data1 = List.of(ByteBuffer.wrap("XXXX".getBytes(UTF_8))); - DataFrame dataFrame1 = new DataFrame(1, 0, data1); - - List buffers = new ArrayList<>(); - FramesEncoder encoder = new FramesEncoder(); - buffers.addAll(encoder.encodeFrame(dataFrame1)); - - ByteBuffer combined = ByteBuffer.allocate(1024); - buffers.stream().forEach(combined::put); - combined.flip(); - - TestFrameProcessor testFrameProcessor = new TestFrameProcessor() { - @Override - public void processFrame(Http2Frame frame) throws IOException { - assertTrue(frame instanceof DataFrame); - DataFrame dataFrame = (DataFrame) frame; - List list = dataFrame.getData(); - assertEquals(list.size(), 1); - ByteBuffer data = list.get(0); - byte[] bytes = new byte[data.remaining()]; - data.get(bytes); - assertEquals(new String(bytes, UTF_8), "XXXX"); - out.println("First data received:" + data); - assertEquals(data.position(), data.limit()); // since bytes read - //assertNotEquals(data.limit(), data.capacity()); - assertEquals(data.capacity(), 1024 - 9 /*frame header*/); - count++; - } - }; - FramesDecoder decoder = new FramesDecoder(testFrameProcessor); - - out.println("Sending " + combined + " to decoder: "); - decoder.decode(combined); - Assert.assertEquals(testFrameProcessor.numberOfFramesDecoded(), 1); - } -} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/AbstractRandomTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/AbstractRandomTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,61 @@ +/* + * 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.internal.net.http; + +import java.util.Random; + +/** Abstract supertype for tests that need random numbers within a given range. */ +public class AbstractRandomTest { + + private static Long getSystemSeed() { + Long seed = null; + try { + // note that Long.valueOf(null) also throws a NumberFormatException + // so if the property is undefined this will still work correctly + seed = Long.valueOf(System.getProperty("seed")); + } catch (NumberFormatException e) { + // do nothing: seed is still null + } + return seed; + } + + private static long getSeed() { + Long seed = getSystemSeed(); + if (seed == null) { + seed = (new Random()).nextLong(); + } + System.out.println("Seed from AbstractRandomTest.getSeed = "+seed+"L"); + return seed; + } + + private static Random random = new Random(getSeed()); + + protected static int randomRange(int lower, int upper) { + if (lower > upper) + throw new IllegalArgumentException("lower > upper"); + int diff = upper - lower; + int r = lower + random.nextInt(diff); + return r - (r % 8); // round down to multiple of 8 (align for longs) + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/AbstractSSLTubeTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/AbstractSSLTubeTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,319 @@ +/* + * 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.internal.net.http; + +import jdk.internal.net.http.common.FlowTube; +import jdk.internal.net.http.common.SSLTube; +import jdk.internal.net.http.common.Utils; +import org.testng.annotations.Test; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.TrustManagerFactory; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.StringTokenizer; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Flow; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.SubmissionPublisher; +import java.util.concurrent.atomic.AtomicLong; + +public class AbstractSSLTubeTest extends AbstractRandomTest { + + public static final long COUNTER = 600; + public static final int LONGS_PER_BUF = 800; + public static final long TOTAL_LONGS = COUNTER * LONGS_PER_BUF; + public static final ByteBuffer SENTINEL = ByteBuffer.allocate(0); + // This is a hack to work around an issue with SubmissionPublisher. + // SubmissionPublisher will call onComplete immediately without forwarding + // remaining pending data if SubmissionPublisher.close() is called when + // there is no demand. In other words, it doesn't wait for the subscriber + // to pull all the data before calling onComplete. + // We use a CountDownLatch to figure out when it is safe to call close(). + // This may cause the test to hang if data are buffered. + protected final CountDownLatch allBytesReceived = new CountDownLatch(1); + + + protected static ByteBuffer getBuffer(long startingAt) { + ByteBuffer buf = ByteBuffer.allocate(LONGS_PER_BUF * 8); + for (int j = 0; j < LONGS_PER_BUF; j++) { + buf.putLong(startingAt++); + } + buf.flip(); + return buf; + } + + protected void run(FlowTube server, + ExecutorService sslExecutor, + CountDownLatch allBytesReceived) throws IOException { + FlowTube client = new SSLTube(createSSLEngine(true), + sslExecutor, + server); + SubmissionPublisher> p = + new SubmissionPublisher<>(ForkJoinPool.commonPool(), + Integer.MAX_VALUE); + FlowTube.TubePublisher begin = p::subscribe; + CompletableFuture completion = new CompletableFuture<>(); + EndSubscriber end = new EndSubscriber(TOTAL_LONGS, completion, allBytesReceived); + client.connectFlows(begin, end); + /* End of wiring */ + + long count = 0; + System.out.printf("Submitting %d buffer arrays\n", COUNTER); + System.out.printf("LoopCount should be %d\n", TOTAL_LONGS); + for (long i = 0; i < COUNTER; i++) { + ByteBuffer b = getBuffer(count); + count += LONGS_PER_BUF; + p.submit(List.of(b)); + } + System.out.println("Finished submission. Waiting for loopback"); + completion.whenComplete((r,t) -> allBytesReceived.countDown()); + try { + allBytesReceived.await(); + } catch (InterruptedException e) { + throw new IOException(e); + } + p.close(); + System.out.println("All bytes received: calling publisher.close()"); + try { + completion.join(); + System.out.println("OK"); + } finally { + sslExecutor.shutdownNow(); + } + } + + protected static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + + } + } + + /** + * The final subscriber which receives the decrypted looped-back data. Just + * needs to compare the data with what was sent. The given CF is either + * completed exceptionally with an error or normally on success. + */ + protected static class EndSubscriber implements FlowTube.TubeSubscriber { + + private static final int REQUEST_WINDOW = 13; + + private final long nbytes; + private final AtomicLong counter = new AtomicLong(); + private final CompletableFuture completion; + private final CountDownLatch allBytesReceived; + private volatile Flow.Subscription subscription; + private long unfulfilled; + + EndSubscriber(long nbytes, CompletableFuture completion, + CountDownLatch allBytesReceived) { + this.nbytes = nbytes; + this.completion = completion; + this.allBytesReceived = allBytesReceived; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + unfulfilled = REQUEST_WINDOW; + System.out.println("EndSubscriber request " + REQUEST_WINDOW); + subscription.request(REQUEST_WINDOW); + } + + public static String info(List i) { + StringBuilder sb = new StringBuilder(); + sb.append("size: ").append(Integer.toString(i.size())); + int x = 0; + for (ByteBuffer b : i) + x += b.remaining(); + sb.append(" bytes: ").append(x); + return sb.toString(); + } + + @Override + public void onNext(List buffers) { + if (--unfulfilled == (REQUEST_WINDOW / 2)) { + long req = REQUEST_WINDOW - unfulfilled; + System.out.println("EndSubscriber request " + req); + unfulfilled = REQUEST_WINDOW; + subscription.request(req); + } + + long currval = counter.get(); + if (currval % 500 == 0) { + System.out.println("EndSubscriber: " + currval); + } + System.out.println("EndSubscriber onNext " + Utils.remaining(buffers)); + + for (ByteBuffer buf : buffers) { + while (buf.hasRemaining()) { + long n = buf.getLong(); + if (currval > (TOTAL_LONGS - 50)) { + System.out.println("End: " + currval); + } + if (n != currval++) { + System.out.println("ERROR at " + n + " != " + (currval - 1)); + completion.completeExceptionally(new RuntimeException("ERROR")); + subscription.cancel(); + return; + } + } + } + + counter.set(currval); + if (currval >= TOTAL_LONGS) { + allBytesReceived.countDown(); + } + } + + @Override + public void onError(Throwable throwable) { + System.out.println("EndSubscriber onError " + throwable); + completion.completeExceptionally(throwable); + allBytesReceived.countDown(); + } + + @Override + public void onComplete() { + long n = counter.get(); + if (n != nbytes) { + System.out.printf("nbytes=%d n=%d\n", nbytes, n); + completion.completeExceptionally(new RuntimeException("ERROR AT END")); + } else { + System.out.println("DONE OK"); + completion.complete(null); + } + allBytesReceived.countDown(); + } + + @Override + public String toString() { + return "EndSubscriber"; + } + } + + protected static SSLEngine createSSLEngine(boolean client) throws IOException { + SSLContext context = (new SimpleSSLContext()).get(); + SSLEngine engine = context.createSSLEngine(); + SSLParameters params = context.getSupportedSSLParameters(); + params.setProtocols(new String[]{"TLSv1.2"}); // TODO: This is essential. Needs to be protocol impl + if (client) { + params.setApplicationProtocols(new String[]{"proto1", "proto2"}); // server will choose proto2 + } else { + params.setApplicationProtocols(new String[]{"proto2"}); // server will choose proto2 + } + engine.setSSLParameters(params); + engine.setUseClientMode(client); + return engine; + } + + /** + * Creates a simple usable SSLContext for SSLSocketFactory or a HttpsServer + * using either a given keystore or a default one in the test tree. + * + * Using this class with a security manager requires the following + * permissions to be granted: + * + * permission "java.util.PropertyPermission" "test.src.path", "read"; + * permission java.io.FilePermission "${test.src}/../../../../lib/testlibrary/jdk/testlibrary/testkeys", + * "read"; The exact path above depends on the location of the test. + */ + protected static class SimpleSSLContext { + + private final SSLContext ssl; + + /** + * Loads default keystore from SimpleSSLContext source directory + */ + public SimpleSSLContext() throws IOException { + String paths = System.getProperty("test.src.path"); + StringTokenizer st = new StringTokenizer(paths, File.pathSeparator); + boolean securityExceptions = false; + SSLContext sslContext = null; + while (st.hasMoreTokens()) { + String path = st.nextToken(); + try { + File f = new File(path, "../../../../lib/testlibrary/jdk/testlibrary/testkeys"); + if (f.exists()) { + try (FileInputStream fis = new FileInputStream(f)) { + sslContext = init(fis); + break; + } + } + } catch (SecurityException e) { + // catch and ignore because permission only required + // for one entry on path (at most) + securityExceptions = true; + } + } + if (securityExceptions) { + System.err.println("SecurityExceptions thrown on loading testkeys"); + } + ssl = sslContext; + } + + private SSLContext init(InputStream i) throws IOException { + try { + char[] passphrase = "passphrase".toCharArray(); + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(i, passphrase); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, passphrase); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(ks); + + SSLContext ssl = SSLContext.getInstance("TLS"); + ssl.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + return ssl; + } catch (KeyManagementException | KeyStoreException | + UnrecoverableKeyException | CertificateException | + NoSuchAlgorithmException e) { + throw new RuntimeException(e.getMessage()); + } + } + + public SSLContext get() { + return ssl; + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/ConnectionPoolTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/ConnectionPoolTest.java Wed Feb 07 21:45:37 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.internal.net.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 java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import jdk.internal.net.http.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 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 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 item) { error();} + @Override + public void subscribe(Flow.Subscriber> 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 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() {return error();} + @Override public HttpClient.Redirect followRedirects() {return error();} + @Override public Optional proxy() {return error();} + @Override public SSLContext sslContext() {return error();} + @Override public SSLParameters sslParameters() {return error();} + @Override public Optional authenticator() {return error();} + @Override public HttpClient.Version version() {return HttpClient.Version.HTTP_1_1;} + @Override public Optional executor() {return error();} + @Override + public HttpResponse send(HttpRequest req, + HttpResponse.BodyHandler responseBodyHandler) + throws IOException, InterruptedException { + return error(); + } + @Override + public CompletableFuture> sendAsync(HttpRequest req, + HttpResponse.BodyHandler responseBodyHandler) { + return error(); + } + @Override + public CompletableFuture> sendAsync(HttpRequest req, + HttpResponse.BodyHandler bodyHandler, + HttpResponse.PushPromiseHandler multiHandler) { + return error(); + } + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/FlowTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/FlowTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,547 @@ +/* + * 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.internal.net.http; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.Random; +import java.util.StringTokenizer; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Flow; +import java.util.concurrent.Flow.Subscriber; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.SubmissionPublisher; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.*; +import javax.net.ssl.TrustManagerFactory; +import jdk.internal.net.http.common.Utils; +import org.testng.annotations.Test; +import jdk.internal.net.http.common.SSLFlowDelegate; + +@Test +public class FlowTest extends AbstractRandomTest { + + private final SubmissionPublisher> srcPublisher; + private final ExecutorService executor; + private static final long COUNTER = 3000; + private static final int LONGS_PER_BUF = 800; + static final long TOTAL_LONGS = COUNTER * LONGS_PER_BUF; + public static final ByteBuffer SENTINEL = ByteBuffer.allocate(0); + static volatile String alpn; + + // This is a hack to work around an issue with SubmissionPublisher. + // SubmissionPublisher will call onComplete immediately without forwarding + // remaining pending data if SubmissionPublisher.close() is called when + // there is no demand. In other words, it doesn't wait for the subscriber + // to pull all the data before calling onComplete. + // We use a CountDownLatch to figure out when it is safe to call close(). + // This may cause the test to hang if data are buffered. + final CountDownLatch allBytesReceived = new CountDownLatch(1); + + private final CompletableFuture completion; + + public FlowTest() throws IOException { + executor = Executors.newCachedThreadPool(); + srcPublisher = new SubmissionPublisher<>(executor, 20, + this::handlePublisherException); + SSLContext ctx = (new SimpleSSLContext()).get(); + SSLEngine engineClient = ctx.createSSLEngine(); + SSLParameters params = ctx.getSupportedSSLParameters(); + params.setApplicationProtocols(new String[]{"proto1", "proto2"}); // server will choose proto2 + params.setProtocols(new String[]{"TLSv1.2"}); // TODO: This is essential. Needs to be protocol impl + engineClient.setSSLParameters(params); + engineClient.setUseClientMode(true); + completion = new CompletableFuture<>(); + SSLLoopbackSubscriber looper = new SSLLoopbackSubscriber(ctx, executor, allBytesReceived); + looper.start(); + EndSubscriber end = new EndSubscriber(TOTAL_LONGS, completion, allBytesReceived); + SSLFlowDelegate sslClient = new SSLFlowDelegate(engineClient, executor, end, looper); + // going to measure how long handshake takes + final long start = System.currentTimeMillis(); + sslClient.alpn().whenComplete((String s, Throwable t) -> { + if (t != null) + t.printStackTrace(); + long endTime = System.currentTimeMillis(); + alpn = s; + System.out.println("ALPN: " + alpn); + long period = (endTime - start); + System.out.printf("Handshake took %d ms\n", period); + }); + Subscriber> reader = sslClient.upstreamReader(); + Subscriber> writer = sslClient.upstreamWriter(); + looper.setReturnSubscriber(reader); + // now connect all the pieces + srcPublisher.subscribe(writer); + String aa = sslClient.alpn().join(); + System.out.println("AAALPN = " + aa); + } + + private void handlePublisherException(Object o, Throwable t) { + System.out.println("Src Publisher exception"); + t.printStackTrace(System.out); + } + + private static ByteBuffer getBuffer(long startingAt) { + ByteBuffer buf = ByteBuffer.allocate(LONGS_PER_BUF * 8); + for (int j = 0; j < LONGS_PER_BUF; j++) { + buf.putLong(startingAt++); + } + buf.flip(); + return buf; + } + + @Test + public void run() { + long count = 0; + System.out.printf("Submitting %d buffer arrays\n", COUNTER); + System.out.printf("LoopCount should be %d\n", TOTAL_LONGS); + for (long i = 0; i < COUNTER; i++) { + ByteBuffer b = getBuffer(count); + count += LONGS_PER_BUF; + srcPublisher.submit(List.of(b)); + } + System.out.println("Finished submission. Waiting for loopback"); + // make sure we don't wait for allBytesReceived in case of error. + completion.whenComplete((r,t) -> allBytesReceived.countDown()); + try { + allBytesReceived.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + System.out.println("All bytes received: "); + srcPublisher.close(); + try { + completion.join(); + if (!alpn.equals("proto2")) { + throw new RuntimeException("wrong alpn received"); + } + System.out.println("OK"); + } finally { + executor.shutdownNow(); + } + } + +/* + public static void main(String[]args) throws Exception { + FlowTest test = new FlowTest(); + test.run(); + } +*/ + + /** + * This Subscriber simulates an SSL loopback network. The object itself + * accepts outgoing SSL encrypted data which is looped back via two sockets + * (one of which is an SSLSocket emulating a server). The method + * {@link #setReturnSubscriber(java.util.concurrent.Flow.Subscriber) } + * is used to provide the Subscriber which feeds the incoming side + * of SSLFlowDelegate. Three threads are used to implement this behavior + * and a SubmissionPublisher drives the incoming read side. + *

+ * A thread reads from the buffer, writes + * to the client j.n.Socket which is connected to a SSLSocket operating + * in server mode. A second thread loops back data read from the SSLSocket back to the + * client again. A third thread reads the client socket and pushes the data to + * a SubmissionPublisher that drives the reader side of the SSLFlowDelegate + */ + static class SSLLoopbackSubscriber implements Subscriber> { + private final BlockingQueue buffer; + private final Socket clientSock; + private final SSLSocket serverSock; + private final Thread thread1, thread2, thread3; + private volatile Flow.Subscription clientSubscription; + private final SubmissionPublisher> publisher; + private final CountDownLatch allBytesReceived; + + SSLLoopbackSubscriber(SSLContext ctx, + ExecutorService exec, + CountDownLatch allBytesReceived) throws IOException { + SSLServerSocketFactory fac = ctx.getServerSocketFactory(); + SSLServerSocket serv = (SSLServerSocket) fac.createServerSocket(0); + SSLParameters params = serv.getSSLParameters(); + params.setApplicationProtocols(new String[]{"proto2"}); + serv.setSSLParameters(params); + + + int serverPort = serv.getLocalPort(); + clientSock = new Socket("127.0.0.1", serverPort); + serverSock = (SSLSocket) serv.accept(); + this.buffer = new LinkedBlockingQueue<>(); + this.allBytesReceived = allBytesReceived; + thread1 = new Thread(this::clientWriter, "clientWriter"); + thread2 = new Thread(this::serverLoopback, "serverLoopback"); + thread3 = new Thread(this::clientReader, "clientReader"); + publisher = new SubmissionPublisher<>(exec, Flow.defaultBufferSize(), + this::handlePublisherException); + SSLFlowDelegate.Monitor.add(this::monitor); + } + + public void start() { + thread1.start(); + thread2.start(); + thread3.start(); + } + + private void handlePublisherException(Object o, Throwable t) { + System.out.println("Loopback Publisher exception"); + t.printStackTrace(System.out); + } + + private final AtomicInteger readCount = new AtomicInteger(); + + // reads off the SSLSocket the data from the "server" + private void clientReader() { + try { + InputStream is = clientSock.getInputStream(); + final int bufsize = FlowTest.randomRange(512, 16 * 1024); + System.out.println("clientReader: bufsize = " + bufsize); + while (true) { + byte[] buf = new byte[bufsize]; + int n = is.read(buf); + if (n == -1) { + System.out.println("clientReader close: read " + + readCount.get() + " bytes"); + System.out.println("clientReader: got EOF. " + + "Waiting signal to close publisher."); + allBytesReceived.await(); + System.out.println("clientReader: closing publisher"); + publisher.close(); + sleep(2000); + Utils.close(is, clientSock); + return; + } + ByteBuffer bb = ByteBuffer.wrap(buf, 0, n); + readCount.addAndGet(n); + publisher.submit(List.of(bb)); + } + } catch (Throwable e) { + e.printStackTrace(); + Utils.close(clientSock); + } + } + + // writes the encrypted data from SSLFLowDelegate to the j.n.Socket + // which is connected to the SSLSocket emulating a server. + private void clientWriter() { + long nbytes = 0; + try { + OutputStream os = + new BufferedOutputStream(clientSock.getOutputStream()); + + while (true) { + ByteBuffer buf = buffer.take(); + if (buf == FlowTest.SENTINEL) { + // finished + //Utils.sleep(2000); + System.out.println("clientWriter close: " + nbytes + " written"); + clientSock.shutdownOutput(); + System.out.println("clientWriter close return"); + return; + } + int len = buf.remaining(); + int written = writeToStream(os, buf); + assert len == written; + nbytes += len; + assert !buf.hasRemaining() + : "buffer has " + buf.remaining() + " bytes left"; + clientSubscription.request(1); + } + } catch (Throwable e) { + e.printStackTrace(); + } + } + + private int writeToStream(OutputStream os, ByteBuffer buf) throws IOException { + byte[] b = buf.array(); + int offset = buf.arrayOffset() + buf.position(); + int n = buf.limit() - buf.position(); + os.write(b, offset, n); + buf.position(buf.limit()); + os.flush(); + return n; + } + + private final AtomicInteger loopCount = new AtomicInteger(); + + public String monitor() { + return "serverLoopback: loopcount = " + loopCount.toString() + + " clientRead: count = " + readCount.toString(); + } + + // thread2 + private void serverLoopback() { + try { + InputStream is = serverSock.getInputStream(); + OutputStream os = serverSock.getOutputStream(); + final int bufsize = FlowTest.randomRange(512, 16 * 1024); + System.out.println("serverLoopback: bufsize = " + bufsize); + byte[] bb = new byte[bufsize]; + while (true) { + int n = is.read(bb); + if (n == -1) { + sleep(2000); + is.close(); + serverSock.close(); + return; + } + os.write(bb, 0, n); + os.flush(); + loopCount.addAndGet(n); + } + } catch (Throwable e) { + e.printStackTrace(); + } + } + + + /** + * This needs to be called before the chain is subscribed. It can't be + * supplied in the constructor. + */ + public void setReturnSubscriber(Subscriber> returnSubscriber) { + publisher.subscribe(returnSubscriber); + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + clientSubscription = subscription; + clientSubscription.request(5); + } + + @Override + public void onNext(List item) { + try { + for (ByteBuffer b : item) + buffer.put(b); + } catch (InterruptedException e) { + e.printStackTrace(); + Utils.close(clientSock); + } + } + + @Override + public void onError(Throwable throwable) { + throwable.printStackTrace(); + Utils.close(clientSock); + } + + @Override + public void onComplete() { + try { + buffer.put(FlowTest.SENTINEL); + } catch (InterruptedException e) { + e.printStackTrace(); + Utils.close(clientSock); + } + } + } + + /** + * The final subscriber which receives the decrypted looped-back data. + * Just needs to compare the data with what was sent. The given CF is + * either completed exceptionally with an error or normally on success. + */ + static class EndSubscriber implements Subscriber> { + + private final long nbytes; + + private final AtomicLong counter; + private volatile Flow.Subscription subscription; + private final CompletableFuture completion; + private final CountDownLatch allBytesReceived; + + EndSubscriber(long nbytes, + CompletableFuture completion, + CountDownLatch allBytesReceived) { + counter = new AtomicLong(0); + this.nbytes = nbytes; + this.completion = completion; + this.allBytesReceived = allBytesReceived; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(5); + } + + public static String info(List i) { + StringBuilder sb = new StringBuilder(); + sb.append("size: ").append(Integer.toString(i.size())); + int x = 0; + for (ByteBuffer b : i) + x += b.remaining(); + sb.append(" bytes: " + Integer.toString(x)); + return sb.toString(); + } + + @Override + public void onNext(List buffers) { + long currval = counter.get(); + //if (currval % 500 == 0) { + //System.out.println("End: " + currval); + //} + + for (ByteBuffer buf : buffers) { + while (buf.hasRemaining()) { + long n = buf.getLong(); + //if (currval > (FlowTest.TOTAL_LONGS - 50)) { + //System.out.println("End: " + currval); + //} + if (n != currval++) { + System.out.println("ERROR at " + n + " != " + (currval - 1)); + completion.completeExceptionally(new RuntimeException("ERROR")); + subscription.cancel(); + return; + } + } + } + + counter.set(currval); + subscription.request(1); + if (currval >= TOTAL_LONGS) { + allBytesReceived.countDown(); + } + } + + @Override + public void onError(Throwable throwable) { + allBytesReceived.countDown(); + completion.completeExceptionally(throwable); + } + + @Override + public void onComplete() { + long n = counter.get(); + if (n != nbytes) { + System.out.printf("nbytes=%d n=%d\n", nbytes, n); + completion.completeExceptionally(new RuntimeException("ERROR AT END")); + } else { + System.out.println("DONE OK: counter = " + n); + allBytesReceived.countDown(); + completion.complete(null); + } + } + } + + /** + * Creates a simple usable SSLContext for SSLSocketFactory + * or a HttpsServer using either a given keystore or a default + * one in the test tree. + *

+ * Using this class with a security manager requires the following + * permissions to be granted: + *

+ * permission "java.util.PropertyPermission" "test.src.path", "read"; + * permission java.io.FilePermission + * "${test.src}/../../../../lib/testlibrary/jdk/testlibrary/testkeys", "read"; + * The exact path above depends on the location of the test. + */ + static class SimpleSSLContext { + + private final SSLContext ssl; + + /** + * Loads default keystore from SimpleSSLContext source directory + */ + public SimpleSSLContext() throws IOException { + String paths = System.getProperty("test.src.path"); + StringTokenizer st = new StringTokenizer(paths, File.pathSeparator); + boolean securityExceptions = false; + SSLContext sslContext = null; + while (st.hasMoreTokens()) { + String path = st.nextToken(); + try { + File f = new File(path, "../../../../lib/testlibrary/jdk/testlibrary/testkeys"); + if (f.exists()) { + try (FileInputStream fis = new FileInputStream(f)) { + sslContext = init(fis); + break; + } + } + } catch (SecurityException e) { + // catch and ignore because permission only required + // for one entry on path (at most) + securityExceptions = true; + } + } + if (securityExceptions) { + System.out.println("SecurityExceptions thrown on loading testkeys"); + } + ssl = sslContext; + } + + private SSLContext init(InputStream i) throws IOException { + try { + char[] passphrase = "passphrase".toCharArray(); + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(i, passphrase); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, passphrase); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(ks); + + SSLContext ssl = SSLContext.getInstance("TLS"); + ssl.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + return ssl; + } catch (KeyManagementException | KeyStoreException | + UnrecoverableKeyException | CertificateException | + NoSuchAlgorithmException e) { + throw new RuntimeException(e.getMessage()); + } + } + + public SSLContext get() { + return ssl; + } + } + + private static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/Http1HeaderParserTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/Http1HeaderParserTest.java Wed Feb 07 21:45:37 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.internal.net.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 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> messageHeaderMap = m.getHeaders(); + int available = bais.available(); + + Http1HeaderParser decoder = new Http1HeaderParser(); + ByteBuffer b = ByteBuffer.wrap(bytes); + decoder.parse(b); + Map> 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> map = new HashMap<>(); + for (Map.Entry> 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 buffers = IntStream.range(0, bytes.length) + .mapToObj(i -> ByteBuffer.wrap(bytes, i, 1)) + .collect(toList()); + while (decoder.parse(buffers.remove(0)) != true); + Map> 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 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> expected, + Map> 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> e : expected.entrySet()) { + String key = e.getKey(); + List values = e.getValue(); + + boolean found = false; + for (Map.Entry> other: actual.entrySet()) { + if (key.equalsIgnoreCase(other.getKey())) { + found = true; + List 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> map) { + StringBuilder sb = new StringBuilder(); + List sortedKeys = new ArrayList(map.keySet()); + Collections.sort(sortedKeys); + for (String key : sortedKeys) { + List 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 */ } + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/RawChannelTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/RawChannelTest.java Wed Feb 07 21:45:37 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.internal.net.http; + +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 java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import jdk.internal.net.http.websocket.RawChannel; +import jdk.internal.net.http.websocket.WebSocketRequest; +import org.testng.annotations.Test; +import static java.net.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()); + 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)]; + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/SSLEchoTubeTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/SSLEchoTubeTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,420 @@ +/* + * 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.internal.net.http; + +import jdk.internal.net.http.common.Demand; +import jdk.internal.net.http.common.FlowTube; +import jdk.internal.net.http.common.SSLTube; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +@Test +public class SSLEchoTubeTest extends AbstractSSLTubeTest { + + @Test + public void runWithEchoServer() throws IOException { + ExecutorService sslExecutor = Executors.newCachedThreadPool(); + + /* Start of wiring */ + /* Emulates an echo server */ + FlowTube server = crossOverEchoServer(sslExecutor); + + run(server, sslExecutor, allBytesReceived); + } + + /** + * Creates a cross-over FlowTube than can be plugged into a client-side + * SSLTube (in place of the SSLLoopbackSubscriber). + * Note that the only method that can be called on the return tube + * is connectFlows(). Calling any other method will trigger an + * InternalError. + * @param sslExecutor an executor + * @return a cross-over FlowTube connected to an EchoTube. + * @throws IOException + */ + private FlowTube crossOverEchoServer(Executor sslExecutor) throws IOException { + LateBindingTube crossOver = new LateBindingTube(); + FlowTube server = new SSLTube(createSSLEngine(false), + sslExecutor, + crossOver); + EchoTube echo = new EchoTube(6); + server.connectFlows(FlowTube.asTubePublisher(echo), FlowTube.asTubeSubscriber(echo)); + + return new CrossOverTube(crossOver); + } + + /** + * A cross-over FlowTube that makes it possible to reverse the direction + * of flows. The typical usage is to connect an two opposite SSLTube, + * one encrypting, one decrypting, to e.g. an EchoTube, with the help + * of a LateBindingTube: + * {@code + * client app => SSLTube => CrossOverTube <= LateBindingTube <= SSLTube <= EchoTube + * } + *

+ * Note that the only method that can be called on the CrossOverTube is + * connectFlows(). Calling any other method will cause an InternalError to + * be thrown. + * Also connectFlows() can be called only once. + */ + private static final class CrossOverTube implements FlowTube { + final LateBindingTube tube; + CrossOverTube(LateBindingTube tube) { + this.tube = tube; + } + + @Override + public void subscribe(Flow.Subscriber> subscriber) { + throw newInternalError(); + } + + @Override + public void connectFlows(TubePublisher writePublisher, TubeSubscriber readSubscriber) { + tube.start(writePublisher, readSubscriber); + } + + @Override + public boolean isFinished() { + return tube.isFinished(); + } + + Error newInternalError() { + InternalError error = new InternalError(); + error.printStackTrace(System.out); + return error; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + throw newInternalError(); + } + + @Override + public void onError(Throwable throwable) { + throw newInternalError(); + } + + @Override + public void onComplete() { + throw newInternalError(); + } + + @Override + public void onNext(List item) { + throw newInternalError(); + } + } + + /** + * A late binding tube that makes it possible to create an + * SSLTube before the right-hand-side tube has been created. + * The typical usage is to make it possible to connect two + * opposite SSLTube (one encrypting, one decrypting) through a + * CrossOverTube: + * {@code + * client app => SSLTube => CrossOverTube <= LateBindingTube <= SSLTube <= EchoTube + * } + *

+ * Note that this class only supports a single call to start(): it cannot be + * subscribed more than once from its left-hand-side (the cross over tube side). + */ + private static class LateBindingTube implements FlowTube { + + final CompletableFuture>> futurePublisher + = new CompletableFuture<>(); + final ConcurrentLinkedQueue>>> queue + = new ConcurrentLinkedQueue<>(); + AtomicReference>> subscriberRef = new AtomicReference<>(); + SequentialScheduler scheduler = SequentialScheduler.synchronizedScheduler(this::loop); + AtomicReference errorRef = new AtomicReference<>(); + private volatile boolean finished; + private volatile boolean completed; + + + public void start(Flow.Publisher> publisher, + Flow.Subscriber> subscriber) { + subscriberRef.set(subscriber); + futurePublisher.complete(publisher); + scheduler.runOrSchedule(); + } + + @Override + public boolean isFinished() { + return finished; + } + + @Override + public void subscribe(Flow.Subscriber> subscriber) { + futurePublisher.thenAccept((p) -> p.subscribe(subscriber)); + scheduler.runOrSchedule(); + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + queue.add((s) -> s.onSubscribe(subscription)); + scheduler.runOrSchedule(); + } + + @Override + public void onNext(List item) { + queue.add((s) -> s.onNext(item)); + scheduler.runOrSchedule(); + } + + @Override + public void onError(Throwable throwable) { + System.out.println("LateBindingTube onError"); + throwable.printStackTrace(System.out); + queue.add((s) -> { + errorRef.compareAndSet(null, throwable); + try { + System.out.println("LateBindingTube subscriber onError: " + throwable); + s.onError(errorRef.get()); + } finally { + finished = true; + System.out.println("LateBindingTube finished"); + } + }); + scheduler.runOrSchedule(); + } + + @Override + public void onComplete() { + System.out.println("LateBindingTube completing"); + queue.add((s) -> { + completed = true; + try { + System.out.println("LateBindingTube complete subscriber"); + s.onComplete(); + } finally { + finished = true; + System.out.println("LateBindingTube finished"); + } + }); + scheduler.runOrSchedule(); + } + + private void loop() { + if (finished) { + scheduler.stop(); + return; + } + Flow.Subscriber> subscriber = subscriberRef.get(); + if (subscriber == null) return; + try { + Consumer>> s; + while ((s = queue.poll()) != null) { + s.accept(subscriber); + } + } catch (Throwable t) { + if (errorRef.compareAndSet(null, t)) { + onError(t); + } + } + } + } + + /** + * An echo tube that just echoes back whatever bytes it receives. + * This cannot be plugged to the right-hand-side of an SSLTube + * since handshake data cannot be simply echoed back, and + * application data most likely also need to be decrypted and + * re-encrypted. + */ + private static final class EchoTube implements FlowTube { + + private final static Object EOF = new Object(); + private final Executor executor = Executors.newSingleThreadExecutor(); + + private final Queue queue = new ConcurrentLinkedQueue<>(); + private final int maxQueueSize; + private final SequentialScheduler processingScheduler = + new SequentialScheduler(createProcessingTask()); + + /* Writing into this tube */ + private volatile long requested; + private Flow.Subscription subscription; + + /* Reading from this tube */ + private final Demand demand = new Demand(); + private final AtomicBoolean cancelled = new AtomicBoolean(); + private Flow.Subscriber> subscriber; + + private EchoTube(int maxBufferSize) { + if (maxBufferSize < 1) + throw new IllegalArgumentException(); + this.maxQueueSize = maxBufferSize; + } + + @Override + public void subscribe(Flow.Subscriber> subscriber) { + this.subscriber = subscriber; + System.out.println("EchoTube got subscriber: " + subscriber); + this.subscriber.onSubscribe(new InternalSubscription()); + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + System.out.println("EchoTube request: " + maxQueueSize); + (this.subscription = subscription).request(requested = maxQueueSize); + } + + private void requestMore() { + Flow.Subscription s = subscription; + if (s == null || cancelled.get()) return; + long unfulfilled = queue.size() + --requested; + if (unfulfilled <= maxQueueSize/2) { + long req = maxQueueSize - unfulfilled; + requested += req; + s.request(req); + System.out.printf("EchoTube request: %s [requested:%s, queue:%s, unfulfilled:%s]%n", + req, requested-req, queue.size(), unfulfilled ); + } + } + + @Override + public void onNext(List item) { + System.out.printf("EchoTube add %s [requested:%s, queue:%s]%n", + Utils.remaining(item), requested, queue.size()); + queue.add(item); + processingScheduler.runOrSchedule(executor); + } + + @Override + public void onError(Throwable throwable) { + System.out.println("EchoTube add " + throwable); + queue.add(throwable); + processingScheduler.runOrSchedule(executor); + } + + @Override + public void onComplete() { + System.out.println("EchoTube add EOF"); + queue.add(EOF); + processingScheduler.runOrSchedule(executor); + } + + @Override + public boolean isFinished() { + return cancelled.get(); + } + + private class InternalSubscription implements Flow.Subscription { + + @Override + public void request(long n) { + System.out.println("EchoTube got request: " + n); + if (n <= 0) { + throw new InternalError(); + } + if (demand.increase(n)) { + processingScheduler.runOrSchedule(executor); + } + } + + @Override + public void cancel() { + cancelled.set(true); + } + } + + @Override + public String toString() { + return "EchoTube"; + } + + int transmitted = 0; + private SequentialScheduler.RestartableTask createProcessingTask() { + return new SequentialScheduler.CompleteRestartableTask() { + + @Override + protected void run() { + try { + while (!cancelled.get()) { + Object item = queue.peek(); + if (item == null) { + System.out.printf("EchoTube: queue empty, requested=%s, demand=%s, transmitted=%s%n", + requested, demand.get(), transmitted); + requestMore(); + return; + } + try { + System.out.printf("EchoTube processing item, requested=%s, demand=%s, transmitted=%s%n", + requested, demand.get(), transmitted); + if (item instanceof List) { + if (!demand.tryDecrement()) { + System.out.println("EchoTube no demand"); + return; + } + @SuppressWarnings("unchecked") + List bytes = (List) item; + Object removed = queue.remove(); + assert removed == item; + System.out.println("EchoTube processing " + + Utils.remaining(bytes)); + transmitted++; + subscriber.onNext(bytes); + requestMore(); + } else if (item instanceof Throwable) { + cancelled.set(true); + Object removed = queue.remove(); + assert removed == item; + System.out.println("EchoTube processing " + item); + subscriber.onError((Throwable) item); + } else if (item == EOF) { + cancelled.set(true); + Object removed = queue.remove(); + assert removed == item; + System.out.println("EchoTube processing EOF"); + subscriber.onComplete(); + } else { + throw new InternalError(String.valueOf(item)); + } + } finally { + } + } + } catch(Throwable t) { + t.printStackTrace(); + throw t; + } + } + }; + } + } + } diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/SSLTubeTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/SSLTubeTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,275 @@ +/* + * 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.internal.net.http; + +import jdk.internal.net.http.common.FlowTube; +import jdk.internal.net.http.common.SSLFlowDelegate; +import jdk.internal.net.http.common.Utils; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocket; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Flow; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.SubmissionPublisher; +import java.util.concurrent.atomic.AtomicInteger; + +@Test +public class SSLTubeTest extends AbstractSSLTubeTest { + + @Test + public void runWithSSLLoopackServer() throws IOException { + ExecutorService sslExecutor = Executors.newCachedThreadPool(); + + /* Start of wiring */ + /* Emulates an echo server */ + SSLLoopbackSubscriber server = + new SSLLoopbackSubscriber((new SimpleSSLContext()).get(), + sslExecutor, + allBytesReceived); + server.start(); + + run(server, sslExecutor, allBytesReceived); + } + + /** + * This is a copy of the SSLLoopbackSubscriber used in FlowTest + */ + private static class SSLLoopbackSubscriber implements FlowTube { + private final BlockingQueue buffer; + private final Socket clientSock; + private final SSLSocket serverSock; + private final Thread thread1, thread2, thread3; + private volatile Flow.Subscription clientSubscription; + private final SubmissionPublisher> publisher; + private final CountDownLatch allBytesReceived; + + SSLLoopbackSubscriber(SSLContext ctx, + ExecutorService exec, + CountDownLatch allBytesReceived) throws IOException { + SSLServerSocketFactory fac = ctx.getServerSocketFactory(); + SSLServerSocket serv = (SSLServerSocket) fac.createServerSocket(0); + SSLParameters params = serv.getSSLParameters(); + params.setApplicationProtocols(new String[]{"proto2"}); + serv.setSSLParameters(params); + + + int serverPort = serv.getLocalPort(); + clientSock = new Socket("127.0.0.1", serverPort); + serverSock = (SSLSocket) serv.accept(); + this.buffer = new LinkedBlockingQueue<>(); + this.allBytesReceived = allBytesReceived; + thread1 = new Thread(this::clientWriter, "clientWriter"); + thread2 = new Thread(this::serverLoopback, "serverLoopback"); + thread3 = new Thread(this::clientReader, "clientReader"); + publisher = new SubmissionPublisher<>(exec, Flow.defaultBufferSize(), + this::handlePublisherException); + SSLFlowDelegate.Monitor.add(this::monitor); + } + + public void start() { + thread1.start(); + thread2.start(); + thread3.start(); + } + + private void handlePublisherException(Object o, Throwable t) { + System.out.println("Loopback Publisher exception"); + t.printStackTrace(System.out); + } + + private final AtomicInteger readCount = new AtomicInteger(); + + // reads off the SSLSocket the data from the "server" + private void clientReader() { + try { + InputStream is = clientSock.getInputStream(); + final int bufsize = randomRange(512, 16 * 1024); + System.out.println("clientReader: bufsize = " + bufsize); + while (true) { + byte[] buf = new byte[bufsize]; + int n = is.read(buf); + if (n == -1) { + System.out.println("clientReader close: read " + + readCount.get() + " bytes"); + System.out.println("clientReader: waiting signal to close publisher"); + allBytesReceived.await(); + System.out.println("clientReader: closing publisher"); + publisher.close(); + sleep(2000); + Utils.close(is, clientSock); + return; + } + ByteBuffer bb = ByteBuffer.wrap(buf, 0, n); + readCount.addAndGet(n); + publisher.submit(List.of(bb)); + } + } catch (Throwable e) { + e.printStackTrace(); + Utils.close(clientSock); + } + } + + // writes the encrypted data from SSLFLowDelegate to the j.n.Socket + // which is connected to the SSLSocket emulating a server. + private void clientWriter() { + long nbytes = 0; + try { + OutputStream os = + new BufferedOutputStream(clientSock.getOutputStream()); + + while (true) { + ByteBuffer buf = buffer.take(); + if (buf == SENTINEL) { + // finished + //Utils.sleep(2000); + System.out.println("clientWriter close: " + nbytes + " written"); + clientSock.shutdownOutput(); + System.out.println("clientWriter close return"); + return; + } + int len = buf.remaining(); + int written = writeToStream(os, buf); + assert len == written; + nbytes += len; + assert !buf.hasRemaining() + : "buffer has " + buf.remaining() + " bytes left"; + clientSubscription.request(1); + } + } catch (Throwable e) { + e.printStackTrace(); + } + } + + private int writeToStream(OutputStream os, ByteBuffer buf) throws IOException { + byte[] b = buf.array(); + int offset = buf.arrayOffset() + buf.position(); + int n = buf.limit() - buf.position(); + os.write(b, offset, n); + buf.position(buf.limit()); + os.flush(); + return n; + } + + private final AtomicInteger loopCount = new AtomicInteger(); + + public String monitor() { + return "serverLoopback: loopcount = " + loopCount.toString() + + " clientRead: count = " + readCount.toString(); + } + + // thread2 + private void serverLoopback() { + try { + InputStream is = serverSock.getInputStream(); + OutputStream os = serverSock.getOutputStream(); + final int bufsize = randomRange(512, 16 * 1024); + System.out.println("serverLoopback: bufsize = " + bufsize); + byte[] bb = new byte[bufsize]; + while (true) { + int n = is.read(bb); + if (n == -1) { + sleep(2000); + is.close(); + os.close(); + serverSock.close(); + return; + } + os.write(bb, 0, n); + os.flush(); + loopCount.addAndGet(n); + } + } catch (Throwable e) { + e.printStackTrace(); + } + } + + + /** + * This needs to be called before the chain is subscribed. It can't be + * supplied in the constructor. + */ + public void setReturnSubscriber(Flow.Subscriber> returnSubscriber) { + publisher.subscribe(returnSubscriber); + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + clientSubscription = subscription; + clientSubscription.request(5); + } + + @Override + public void onNext(List item) { + try { + for (ByteBuffer b : item) + buffer.put(b); + } catch (InterruptedException e) { + e.printStackTrace(); + Utils.close(clientSock); + } + } + + @Override + public void onError(Throwable throwable) { + throwable.printStackTrace(); + Utils.close(clientSock); + } + + @Override + public void onComplete() { + try { + buffer.put(SENTINEL); + } catch (InterruptedException e) { + e.printStackTrace(); + Utils.close(clientSock); + } + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public void subscribe(Flow.Subscriber> subscriber) { + publisher.subscribe(subscriber); + } + } + +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/SelectorTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/SelectorTest.java Wed Feb 07 21:45:37 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.internal.net.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 java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import org.testng.annotations.Test; +import jdk.internal.net.http.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 java.net.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()); + 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); + } + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/WrapperTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/WrapperTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,263 @@ +/* + * 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.internal.net.http; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; +import org.testng.annotations.Test; +import jdk.internal.net.http.common.SubscriberWrapper; + +@Test +public class WrapperTest { + static final int LO_PRI = 1; + static final int HI_PRI = 2; + static final int NUM_HI_PRI = 240; + static final int BUFSIZE = 1016; + static final int BUFSIZE_INT = BUFSIZE/4; + static final int HI_PRI_FREQ = 40; + + static final int TOTAL = 10000; + //static final int TOTAL = 500; + + final SubmissionPublisher> publisher; + final SubscriberWrapper sub1, sub2, sub3; + final ExecutorService executor = Executors.newCachedThreadPool(); + volatile int hipricount = 0; + + void errorHandler(Flow.Subscriber> sub, Throwable t) { + System.err.printf("Exception from %s : %s\n", sub.toString(), t.toString()); + } + + public WrapperTest() { + publisher = new SubmissionPublisher<>(executor, 600, + (a, b) -> { + errorHandler(a, b); + }); + + CompletableFuture notif = new CompletableFuture<>(); + LastSubscriber ls = new LastSubscriber(notif); + sub1 = new Filter1(ls); + sub2 = new Filter2(sub1); + sub3 = new Filter2(sub2); + } + + public class Filter2 extends SubscriberWrapper { + Filter2(SubscriberWrapper wrapper) { + super(wrapper); + } + + // reverse the order of the bytes in each buffer + public void incoming(List list, boolean complete) { + List out = new LinkedList<>(); + for (ByteBuffer inbuf : list) { + int size = inbuf.remaining(); + ByteBuffer outbuf = ByteBuffer.allocate(size); + for (int i=size; i>0; i--) { + byte b = inbuf.get(i-1); + outbuf.put(b); + } + outbuf.flip(); + out.add(outbuf); + } + if (complete) System.out.println("Filter2.complete"); + outgoing(out, complete); + } + + protected long windowUpdate(long currval) { + return currval == 0 ? 1 : 0; + } + } + + volatile int filter1Calls = 0; // every third call we insert hi pri data + + ByteBuffer getHiPri(int val) { + ByteBuffer buf = ByteBuffer.allocate(8); + buf.putInt(HI_PRI); + buf.putInt(val); + buf.flip(); + return buf; + } + + volatile int hiPriAdded = 0; + + public class Filter1 extends SubscriberWrapper { + Filter1(Flow.Subscriber> downstreamSubscriber) + { + super(); + subscribe(downstreamSubscriber); + } + + // Inserts up to NUM_HI_PRI hi priority buffers into flow + protected void incoming(List in, boolean complete) { + if ((++filter1Calls % HI_PRI_FREQ) == 0 && (hiPriAdded++ < NUM_HI_PRI)) { + sub1.outgoing(getHiPri(hipricount++), false); + } + // pass data thru + if (complete) System.out.println("Filter1.complete"); + outgoing(in, complete); + } + + protected long windowUpdate(long currval) { + return currval == 0 ? 1 : 0; + } + } + + /** + * Final subscriber in the chain. Compares the data sent by the original + * publisher. + */ + static public class LastSubscriber implements Flow.Subscriber> { + volatile Flow.Subscription subscription; + volatile int hipriCounter=0; + volatile int lopriCounter=0; + final CompletableFuture cf; + + LastSubscriber(CompletableFuture cf) { + this.cf = cf; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(50); // say + } + + private void error(String...args) { + StringBuilder sb = new StringBuilder(); + for (String s : args) { + sb.append(s); + sb.append(' '); + } + String msg = sb.toString(); + System.out.println("Error: " + msg); + RuntimeException e = new RuntimeException(msg); + cf.completeExceptionally(e); + subscription.cancel(); // This is where we need a variant that include exception + } + + private void check(ByteBuffer buf) { + int type = buf.getInt(); + if (type == HI_PRI) { + // check next int is hi pri counter + int c = buf.getInt(); + if (c != hipriCounter) + error("hi pri counter", Integer.toString(c), Integer.toString(hipriCounter)); + hipriCounter++; + } else { + while (buf.hasRemaining()) { + if (buf.getInt() != lopriCounter) + error("lo pri counter", Integer.toString(lopriCounter)); + lopriCounter++; + } + } + } + + @Override + public void onNext(List items) { + for (ByteBuffer item : items) + check(item); + subscription.request(1); + } + + @Override + public void onError(Throwable throwable) { + error(throwable.getMessage()); + } + + @Override + public void onComplete() { + if (hipriCounter != NUM_HI_PRI) + error("hi pri at end wrong", Integer.toString(hipriCounter), Integer.toString(NUM_HI_PRI)); + else { + System.out.println("LastSubscriber.complete"); + cf.complete(null); // success + } + } + } + + List getBuffer(int c) { + ByteBuffer buf = ByteBuffer.allocate(BUFSIZE+4); + buf.putInt(LO_PRI); + for (int i=0; i completion = sub3.completion(); + publisher.subscribe(sub3); + // now submit a load of data + int counter = 0; + for (int i = 0; i < TOTAL; i++) { + List bufs = getBuffer(counter); + //if (i==2) + //bufs.get(0).putInt(41, 1234); // error + counter += BUFSIZE_INT; + publisher.submit(bufs); + //if (i % 1000 == 0) + //Thread.sleep(1000); + //if (i == 99) { + //publisher.closeExceptionally(new RuntimeException("Test error")); + //errorTest = true; + //break; + //} + } + if (!errorTest) { + publisher.close(); + } + System.out.println("Publisher completed"); + completion.join(); + System.out.println("Subscribers completed ok"); + } finally { + executor.shutdownNow(); + } + } + + static void display(CompletableFuture cf) { + System.out.print (cf); + if (!cf.isDone()) + return; + try { + cf.join(); // wont block + } catch (Exception e) { + System.out.println(" " + e); + } + } + +/* + public static void main(String[] args) throws InterruptedException { + WrapperTest test = new WrapperTest(); + test.run(); + } +*/ +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/common/DemandTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/common/DemandTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,202 @@ +/* + * 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.internal.net.http.common; + +import org.testng.annotations.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.atomic.AtomicReference; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class DemandTest { + + @Test + public void test01() { + assertTrue(new Demand().isFulfilled()); + } + + @Test + public void test011() { + Demand d = new Demand(); + d.increase(3); + d.decreaseAndGet(3); + assertTrue(d.isFulfilled()); + } + + @Test + public void test02() { + Demand d = new Demand(); + d.increase(1); + assertFalse(d.isFulfilled()); + } + + @Test + public void test03() { + Demand d = new Demand(); + d.increase(3); + assertEquals(d.decreaseAndGet(3), 3); + } + + @Test + public void test04() { + Demand d = new Demand(); + d.increase(3); + assertEquals(d.decreaseAndGet(5), 3); + } + + @Test + public void test05() { + Demand d = new Demand(); + d.increase(7); + assertEquals(d.decreaseAndGet(4), 4); + } + + @Test + public void test06() { + Demand d = new Demand(); + assertEquals(d.decreaseAndGet(3), 0); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void test07() { + Demand d = new Demand(); + d.increase(0); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void test08() { + Demand d = new Demand(); + d.increase(-1); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void test09() { + Demand d = new Demand(); + d.increase(10); + d.decreaseAndGet(0); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void test10() { + Demand d = new Demand(); + d.increase(13); + d.decreaseAndGet(-3); + } + + @Test + public void test11() { + Demand d = new Demand(); + d.increase(1); + assertTrue(d.tryDecrement()); + } + + @Test + public void test12() { + Demand d = new Demand(); + d.increase(2); + assertTrue(d.tryDecrement()); + } + + @Test + public void test14() { + Demand d = new Demand(); + assertFalse(d.tryDecrement()); + } + + @Test + public void test141() { + Demand d = new Demand(); + d.increase(Long.MAX_VALUE); + assertFalse(d.isFulfilled()); + } + + @Test + public void test142() { + Demand d = new Demand(); + d.increase(Long.MAX_VALUE); + d.increase(1); + assertFalse(d.isFulfilled()); + } + + @Test + public void test143() { + Demand d = new Demand(); + d.increase(Long.MAX_VALUE); + d.increase(1); + assertFalse(d.isFulfilled()); + } + + @Test + public void test144() { + Demand d = new Demand(); + d.increase(Long.MAX_VALUE); + d.increase(Long.MAX_VALUE); + d.decreaseAndGet(3); + d.decreaseAndGet(5); + assertFalse(d.isFulfilled()); + } + + @Test + public void test145() { + Demand d = new Demand(); + d.increase(Long.MAX_VALUE); + d.decreaseAndGet(Long.MAX_VALUE); + assertTrue(d.isFulfilled()); + } + + @Test(invocationCount = 32) + public void test15() throws InterruptedException { + int N = Math.max(2, Runtime.getRuntime().availableProcessors() + 1); + int M = ((N + 1) * N) / 2; // 1 + 2 + 3 + ... N + Demand d = new Demand(); + d.increase(M); + CyclicBarrier start = new CyclicBarrier(N); + CountDownLatch stop = new CountDownLatch(N); + AtomicReference error = new AtomicReference<>(); + for (int i = 0; i < N; i++) { + int j = i + 1; + new Thread(() -> { + try { + start.await(); + } catch (Exception e) { + error.compareAndSet(null, e); + } + try { + assertEquals(d.decreaseAndGet(j), j); + } catch (Throwable t) { + error.compareAndSet(null, t); + } finally { + stop.countDown(); + } + }).start(); + } + stop.await(); + assertTrue(d.isFulfilled()); + assertEquals(error.get(), null); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/common/MinimalFutureTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/common/MinimalFutureTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,161 @@ +/* + * 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. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.common; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.testng.Assert.assertThrows; + +public class MinimalFutureTest { + + @Test(dataProvider = "futures") + public void test(CompletableFuture mf) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + assertNoObtrusion(mf.thenApply(MinimalFutureTest::apply)); + assertNoObtrusion(mf.thenApplyAsync(MinimalFutureTest::apply)); + assertNoObtrusion(mf.thenApplyAsync(MinimalFutureTest::apply, executor)); + + assertNoObtrusion(mf.thenAccept(MinimalFutureTest::accept)); + assertNoObtrusion(mf.thenAcceptAsync(MinimalFutureTest::accept)); + assertNoObtrusion(mf.thenAcceptAsync(MinimalFutureTest::accept, executor)); + + assertNoObtrusion(mf.thenRun(MinimalFutureTest::run)); + assertNoObtrusion(mf.thenRunAsync(MinimalFutureTest::run)); + assertNoObtrusion(mf.thenRunAsync(MinimalFutureTest::run, executor)); + + assertNoObtrusion(mf.thenCombine(otherFuture(), MinimalFutureTest::apply)); + assertNoObtrusion(mf.thenCombineAsync(otherFuture(), MinimalFutureTest::apply)); + assertNoObtrusion(mf.thenCombineAsync(otherFuture(), MinimalFutureTest::apply, executor)); + + assertNoObtrusion(mf.thenAcceptBoth(otherFuture(), MinimalFutureTest::accept)); + assertNoObtrusion(mf.thenAcceptBothAsync(otherFuture(), MinimalFutureTest::accept)); + assertNoObtrusion(mf.thenAcceptBothAsync(otherFuture(), MinimalFutureTest::accept, executor)); + + assertNoObtrusion(mf.runAfterBoth(otherFuture(), MinimalFutureTest::run)); + assertNoObtrusion(mf.runAfterBothAsync(otherFuture(), MinimalFutureTest::run)); + assertNoObtrusion(mf.runAfterBothAsync(otherFuture(), MinimalFutureTest::run, executor)); + + // "either" methods may return something else if otherFuture() is + // not MinimalFuture + + assertNoObtrusion(mf.applyToEither(otherFuture(), MinimalFutureTest::apply)); + assertNoObtrusion(mf.applyToEitherAsync(otherFuture(), MinimalFutureTest::apply)); + assertNoObtrusion(mf.applyToEitherAsync(otherFuture(), MinimalFutureTest::apply, executor)); + + assertNoObtrusion(mf.acceptEither(otherFuture(), MinimalFutureTest::accept)); + assertNoObtrusion(mf.acceptEitherAsync(otherFuture(), MinimalFutureTest::accept)); + assertNoObtrusion(mf.acceptEitherAsync(otherFuture(), MinimalFutureTest::accept, executor)); + + assertNoObtrusion(mf.runAfterEither(otherFuture(), MinimalFutureTest::run)); + assertNoObtrusion(mf.runAfterEitherAsync(otherFuture(), MinimalFutureTest::run)); + assertNoObtrusion(mf.runAfterEitherAsync(otherFuture(), MinimalFutureTest::run, executor)); + + assertNoObtrusion(mf.thenCompose(MinimalFutureTest::completionStageOf)); + assertNoObtrusion(mf.thenComposeAsync(MinimalFutureTest::completionStageOf)); + assertNoObtrusion(mf.thenComposeAsync(MinimalFutureTest::completionStageOf, executor)); + + assertNoObtrusion(mf.handle(MinimalFutureTest::relay)); + assertNoObtrusion(mf.handleAsync(MinimalFutureTest::relay)); + assertNoObtrusion(mf.handleAsync(MinimalFutureTest::relay, executor)); + + assertNoObtrusion(mf.whenComplete(MinimalFutureTest::accept)); + assertNoObtrusion(mf.whenCompleteAsync(MinimalFutureTest::accept)); + assertNoObtrusion(mf.whenCompleteAsync(MinimalFutureTest::accept, executor)); + + assertNoObtrusion(mf.toCompletableFuture()); + assertNoObtrusion(mf.exceptionally(t -> null)); + + assertNoObtrusion(mf); + assertNoObtrusion(mf.copy()); + assertNoObtrusion(mf.newIncompleteFuture()); + } finally { + executor.shutdownNow(); + } + } + + private static CompletableFuture otherFuture() { + return MinimalFuture.completedFuture(new Object()); + } + + private static Object relay(Object r, Throwable e) { + if (e != null) + throw new CompletionException(e); + else + return r; + } + + private static CompletableFuture completionStageOf(Object r) { + return new CompletableFuture<>(); + } + + private static void accept(Object arg) { + } + + private static void accept(Object arg1, Object arg2) { + } + + private static void run() { + } + + private static Object apply(Object arg) { + return new Object(); + } + + private static Object apply(Object arg1, Object arg2) { + return new Object(); + } + + + @DataProvider(name = "futures") + public Object[][] futures() { + + MinimalFuture mf = new MinimalFuture<>(); + mf.completeExceptionally(new Throwable()); + + MinimalFuture mf1 = new MinimalFuture<>(); + mf1.complete(new Object()); + + return new Object[][]{ + new Object[]{new MinimalFuture<>()}, + new Object[]{MinimalFuture.failedFuture(new Throwable())}, + new Object[]{MinimalFuture.completedFuture(new Object())}, + new Object[]{mf}, + new Object[]{mf1}, + }; + } + + private void assertNoObtrusion(CompletableFuture cf) { + assertThrows(UnsupportedOperationException.class, + () -> cf.obtrudeValue(null)); + assertThrows(UnsupportedOperationException.class, + () -> cf.obtrudeException(new RuntimeException())); + } +} diff -r aedd6133e7a0 -r fd85b2bf2b0d test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/frame/FramesDecoderTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/frame/FramesDecoderTest.java Wed Feb 07 21:45:37 2018 +0000 @@ -0,0 +1,140 @@ +/* + * 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. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import org.testng.Assert; +import org.testng.annotations.Test; +import static java.lang.System.out; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.*; + +public class FramesDecoderTest { + + abstract class TestFrameProcessor implements FramesDecoder.FrameProcessor { + protected volatile int count; + public int numberOfFramesDecoded() { return count; } + } + + /** + * Verifies that a ByteBuffer containing more that one frame, destined + * to be returned to the user's subscriber, i.e. a data frame, does not + * inadvertently expose the following frame ( between its limit and + * capacity ). + */ + @Test + public void decodeDataFrameFollowedByAnother() throws Exception { + // input frames for to the decoder + List data1 = List.of(ByteBuffer.wrap("XXXX".getBytes(UTF_8))); + DataFrame dataFrame1 = new DataFrame(1, 0, data1); + List data2 = List.of(ByteBuffer.wrap("YYYY".getBytes(UTF_8))); + DataFrame dataFrame2 = new DataFrame(1, 0, data2); + + List buffers = new ArrayList<>(); + FramesEncoder encoder = new FramesEncoder(); + buffers.addAll(encoder.encodeFrame(dataFrame1)); + buffers.addAll(encoder.encodeFrame(dataFrame2)); + + ByteBuffer combined = ByteBuffer.allocate(1024); + buffers.stream().forEach(combined::put); + combined.flip(); + + TestFrameProcessor testFrameProcessor = new TestFrameProcessor() { + @Override + public void processFrame(Http2Frame frame) throws IOException { + assertTrue(frame instanceof DataFrame); + DataFrame dataFrame = (DataFrame) frame; + List list = dataFrame.getData(); + assertEquals(list.size(), 1); + ByteBuffer data = list.get(0); + byte[] bytes = new byte[data.remaining()]; + data.get(bytes); + if (count == 0) { + assertEquals(new String(bytes, UTF_8), "XXXX"); + out.println("First data received:" + data); + assertEquals(data.position(), data.limit()); // since bytes read + assertEquals(data.limit(), data.capacity()); + } else { + assertEquals(new String(bytes, UTF_8), "YYYY"); + out.println("Second data received:" + data); + } + count++; + } + }; + FramesDecoder decoder = new FramesDecoder(testFrameProcessor); + + out.println("Sending " + combined + " to decoder: "); + decoder.decode(combined); + Assert.assertEquals(testFrameProcessor.numberOfFramesDecoded(), 2); + } + + + /** + * Verifies that a ByteBuffer containing ONLY data one frame, destined + * to be returned to the user's subscriber, does not restrict the capacity. + * The complete buffer ( all its capacity ), since no longer used by the + * HTTP Client, should be returned to the user. + */ + @Test + public void decodeDataFrameEnsureNotCapped() throws Exception { + // input frames for to the decoder + List data1 = List.of(ByteBuffer.wrap("XXXX".getBytes(UTF_8))); + DataFrame dataFrame1 = new DataFrame(1, 0, data1); + + List buffers = new ArrayList<>(); + FramesEncoder encoder = new FramesEncoder(); + buffers.addAll(encoder.encodeFrame(dataFrame1)); + + ByteBuffer combined = ByteBuffer.allocate(1024); + buffers.stream().forEach(combined::put); + combined.flip(); + + TestFrameProcessor testFrameProcessor = new TestFrameProcessor() { + @Override + public void processFrame(Http2Frame frame) throws IOException { + assertTrue(frame instanceof DataFrame); + DataFrame dataFrame = (DataFrame) frame; + List list = dataFrame.getData(); + assertEquals(list.size(), 1); + ByteBuffer data = list.get(0); + byte[] bytes = new byte[data.remaining()]; + data.get(bytes); + assertEquals(new String(bytes, UTF_8), "XXXX"); + out.println("First data received:" + data); + assertEquals(data.position(), data.limit()); // since bytes read + //assertNotEquals(data.limit(), data.capacity()); + assertEquals(data.capacity(), 1024 - 9 /*frame header*/); + count++; + } + }; + FramesDecoder decoder = new FramesDecoder(testFrameProcessor); + + out.println("Sending " + combined + " to decoder: "); + decoder.decode(combined); + Assert.assertEquals(testFrameProcessor.numberOfFramesDecoded(), 1); + } +}