--- 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.
--- 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;
/**
--- 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}.
--- 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<String> getALPN() {
- assert connected();
- return getConnectionFlow().getALPN();
- }
-
- final SSLEngine getEngine() { return engine; }
-
- private static SSLParameters createSSLParameters(HttpClientImpl client,
- String serverName,
- String[] alpn) {
- SSLParameters sslp = client.sslParameters();
- SSLParameters sslParameters = Utils.copySSLParameters(sslp);
- if (alpn != null) {
- Log.logSSL("AbstractAsyncSSLConnection: Setting application protocols: {0}",
- Arrays.toString(alpn));
- sslParameters.setApplicationProtocols(alpn);
- } else {
- Log.logSSL("AbstractAsyncSSLConnection: no applications set!");
- }
- if (serverName != null) {
- sslParameters.setServerNames(List.of(new SNIHostName(serverName)));
- }
- return sslParameters;
- }
-
- private static SSLEngine createEngine(SSLContext context,
- SSLParameters sslParameters) {
- SSLEngine engine = context.createSSLEngine();
- engine.setUseClientMode(true);
- engine.setSSLParameters(sslParameters);
- return engine;
- }
-
- @Override
- final boolean isSecure() {
- return true;
- }
-
- // Support for WebSocket/RawChannelImpl which unfortunately
- // still depends on synchronous read/writes.
- // It should be removed when RawChannelImpl moves to using asynchronous APIs.
- static final class SSLConnectionChannel extends DetachedConnectionChannel {
- final DetachedConnectionChannel delegate;
- final SSLDelegate sslDelegate;
- SSLConnectionChannel(DetachedConnectionChannel delegate, SSLDelegate sslDelegate) {
- this.delegate = delegate;
- this.sslDelegate = sslDelegate;
- }
-
- SocketChannel channel() {
- return delegate.channel();
- }
-
- @Override
- ByteBuffer read() throws IOException {
- SSLDelegate.WrapperResult r = sslDelegate.recvData(ByteBuffer.allocate(8192));
- // TODO: check for closure
- int n = r.result.bytesProduced();
- if (n > 0) {
- return r.buf;
- } else if (n == 0) {
- return Utils.EMPTY_BYTEBUFFER;
- } else {
- return null;
- }
- }
- @Override
- long write(ByteBuffer[] buffers, int start, int number) throws IOException {
- long l = SSLDelegate.countBytes(buffers, start, number);
- SSLDelegate.WrapperResult r = sslDelegate.sendData(buffers, start, number);
- if (r.result.getStatus() == SSLEngineResult.Status.CLOSED) {
- if (l > 0) {
- throw new IOException("SSLHttpConnection closed");
- }
- }
- return l;
- }
- @Override
- public void shutdownInput() throws IOException {
- delegate.shutdownInput();
- }
- @Override
- public void shutdownOutput() throws IOException {
- delegate.shutdownOutput();
- }
- @Override
- public void close() {
- delegate.close();
- }
- }
-
- // Support for WebSocket/RawChannelImpl which unfortunately
- // still depends on synchronous read/writes.
- // It should be removed when RawChannelImpl moves to using asynchronous APIs.
- @Override
- DetachedConnectionChannel detachChannel() {
- assert client() != null;
- DetachedConnectionChannel detachedChannel = plainConnection().detachChannel();
- SSLDelegate sslDelegate = new SSLDelegate(engine,
- detachedChannel.channel());
- return new SSLConnectionChannel(detachedChannel, sslDelegate);
- }
-
-}
--- a/src/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; }
-
-}
--- 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;
- }
-}
--- 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<Void> connectAsync() {
- return plainConnection
- .connectAsync()
- .thenApply( unused -> {
- // create the SSLTube wrapping the SocketTube, with the given engine
- flow = new SSLTube(engine,
- client().theExecutor(),
- plainConnection.getConnectionFlow());
- return null; } );
- }
-
- @Override
- boolean connected() {
- return plainConnection.connected();
- }
-
- @Override
- HttpPublisher publisher() { return writePublisher; }
-
- @Override
- boolean isProxied() {
- return false;
- }
-
- @Override
- SocketChannel channel() {
- return plainConnection.channel();
- }
-
- @Override
- ConnectionPool.CacheKey cacheKey() {
- return ConnectionPool.cacheKey(address, null);
- }
-
- @Override
- public void close() {
- plainConnection.close();
- }
-
- @Override
- void shutdownInput() throws IOException {
- debug.log(Level.DEBUG, "plainConnection.channel().shutdownInput()");
- plainConnection.channel().shutdownInput();
- }
-
- @Override
- void shutdownOutput() throws IOException {
- debug.log(Level.DEBUG, "plainConnection.channel().shutdownOutput()");
- plainConnection.channel().shutdownOutput();
- }
-
- @Override
- SSLTube getConnectionFlow() {
- return flow;
- }
-}
--- a/src/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<Void> connectAsync() {
- debug.log(Level.DEBUG, "Connecting plain tunnel connection");
- // This will connect the PlainHttpConnection flow, so that
- // its HttpSubscriber and HttpPublisher are subscribed to the
- // SocketTube
- return plainConnection
- .connectAsync()
- .thenApply( unused -> {
- debug.log(Level.DEBUG, "creating SSLTube");
- // create the SSLTube wrapping the SocketTube, with the given engine
- flow = new SSLTube(engine,
- client().theExecutor(),
- plainConnection.getConnectionFlow());
- return null;} );
- }
-
- @Override
- boolean isTunnel() { return true; }
-
- @Override
- boolean connected() {
- return plainConnection.connected(); // && sslDelegate.connected();
- }
-
- @Override
- HttpPublisher publisher() { return writePublisher; }
-
- @Override
- public String toString() {
- return "AsyncSSLTunnelConnection: " + super.toString();
- }
-
- @Override
- PlainTunnelingConnection plainConnection() {
- return plainConnection;
- }
-
- @Override
- ConnectionPool.CacheKey cacheKey() {
- return ConnectionPool.cacheKey(address, plainConnection.proxyAddr);
- }
-
- @Override
- public void close() {
- plainConnection.close();
- }
-
- @Override
- void shutdownInput() throws IOException {
- plainConnection.channel().shutdownInput();
- }
-
- @Override
- void shutdownOutput() throws IOException {
- plainConnection.channel().shutdownOutput();
- }
-
- @Override
- SocketChannel channel() {
- return plainConnection.channel();
- }
-
- @Override
- boolean isProxied() {
- return true;
- }
-
- @Override
- SSLTube getConnectionFlow() {
- return flow;
- }
-}
--- a/src/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<? super IOException> errorHandler;
- AsyncTriggerEvent(Consumer<? super IOException> errorHandler,
- Runnable trigger) {
- super(0);
- this.trigger = Objects.requireNonNull(trigger);
- this.errorHandler = Objects.requireNonNull(errorHandler);
- }
- /** Returns null */
- @Override
- public SelectableChannel channel() { return null; }
- /** Returns 0 */
- @Override
- public int interestOps() { return 0; }
- @Override
- public void handle() { trigger.run(); }
- @Override
- public void abort(IOException ioe) { errorHandler.accept(ioe); }
- @Override
- public boolean repeating() { return false; }
-}
--- a/src/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<String> BASIC_DUMMY =
- List.of("Basic " + Base64.getEncoder()
- .encodeToString("o:o".getBytes(ISO_8859_1)));
-
- // A public no-arg constructor is required by FilterFactory
- public AuthenticationFilter() {}
-
- private PasswordAuthentication getCredentials(String header,
- boolean proxy,
- HttpRequestImpl req)
- throws IOException
- {
- HttpClientImpl client = exchange.client();
- java.net.Authenticator auth =
- client.authenticator()
- .orElseThrow(() -> new IOException("No authenticator set"));
- URI uri = req.uri();
- HeaderParser parser = new HeaderParser(header);
- String authscheme = parser.findKey(0);
-
- String realm = parser.findValue("realm");
- java.net.Authenticator.RequestorType rtype = proxy ? PROXY : SERVER;
- URL url = toURL(uri, req.method(), proxy);
-
- // needs to be instance method in Authenticator
- return auth.requestPasswordAuthenticationInstance(uri.getHost(),
- null,
- uri.getPort(),
- uri.getScheme(),
- realm,
- authscheme,
- url,
- rtype
- );
- }
-
- private URL toURL(URI uri, String method, boolean proxy)
- throws MalformedURLException
- {
- if (proxy && "CONNECT".equalsIgnoreCase(method)
- && "socket".equalsIgnoreCase(uri.getScheme())) {
- return null; // proxy tunneling
- }
- return uri.toURL();
- }
-
- private URI getProxyURI(HttpRequestImpl r) {
- InetSocketAddress proxy = r.proxy();
- if (proxy == null) {
- return null;
- }
-
- // our own private scheme for proxy URLs
- // eg. proxy.http://host:port/
- String scheme = "proxy." + r.uri().getScheme();
- try {
- return new URI(scheme,
- null,
- proxy.getHostString(),
- proxy.getPort(),
- null,
- null,
- null);
- } catch (URISyntaxException e) {
- throw new InternalError(e);
- }
- }
-
- @Override
- public void request(HttpRequestImpl r, MultiExchange<?> e) throws IOException {
- // use preemptive authentication if an entry exists.
- Cache cache = getCache(e);
- this.exchange = e;
-
- // Proxy
- if (exchange.proxyauth == null) {
- URI proxyURI = getProxyURI(r);
- if (proxyURI != null) {
- CacheEntry ca = cache.get(proxyURI, true);
- if (ca != null) {
- exchange.proxyauth = new AuthInfo(true, ca.scheme, null, ca);
- addBasicCredentials(r, true, ca.value);
- }
- }
- }
-
- // Server
- if (exchange.serverauth == null) {
- CacheEntry ca = cache.get(r.uri(), false);
- if (ca != null) {
- exchange.serverauth = new AuthInfo(true, ca.scheme, null, ca);
- addBasicCredentials(r, false, ca.value);
- }
- }
- }
-
- // TODO: refactor into per auth scheme class
- private static void addBasicCredentials(HttpRequestImpl r,
- boolean proxy,
- PasswordAuthentication pw) {
- String hdrname = proxy ? "Proxy-Authorization" : "Authorization";
- StringBuilder sb = new StringBuilder(128);
- sb.append(pw.getUserName()).append(':').append(pw.getPassword());
- String s = encoder.encodeToString(sb.toString().getBytes(ISO_8859_1));
- String value = "Basic " + s;
- if (proxy) {
- if (r.isConnect()) {
- if (!Utils.PROXY_TUNNEL_FILTER
- .test(hdrname, List.of(value))) {
- Log.logError("{0} disabled", hdrname);
- return;
- }
- } else if (r.proxy() != null) {
- if (!Utils.PROXY_FILTER
- .test(hdrname, List.of(value))) {
- Log.logError("{0} disabled", hdrname);
- return;
- }
- }
- }
- r.setSystemHeader(hdrname, value);
- }
-
- // Information attached to a HttpRequestImpl relating to authentication
- static class AuthInfo {
- final boolean fromcache;
- final String scheme;
- int retries;
- PasswordAuthentication credentials; // used in request
- CacheEntry cacheEntry; // if used
-
- AuthInfo(boolean fromcache,
- String scheme,
- PasswordAuthentication credentials) {
- this.fromcache = fromcache;
- this.scheme = scheme;
- this.credentials = credentials;
- this.retries = 1;
- }
-
- AuthInfo(boolean fromcache,
- String scheme,
- PasswordAuthentication credentials,
- CacheEntry ca) {
- this(fromcache, scheme, credentials);
- assert credentials == null || (ca != null && ca.value == null);
- cacheEntry = ca;
- }
-
- AuthInfo retryWithCredentials(PasswordAuthentication pw) {
- // If the info was already in the cache we need to create a new
- // instance with fromCache==false so that it's put back in the
- // cache if authentication succeeds
- AuthInfo res = fromcache ? new AuthInfo(false, scheme, pw) : this;
- res.credentials = Objects.requireNonNull(pw);
- res.retries = retries;
- return res;
- }
-
- }
-
- @Override
- public HttpRequestImpl response(Response r) throws IOException {
- Cache cache = getCache(exchange);
- int status = r.statusCode();
- HttpHeaders hdrs = r.headers();
- HttpRequestImpl req = r.request();
-
- if (status != UNAUTHORIZED && status != PROXY_UNAUTHORIZED) {
- // check if any authentication succeeded for first time
- if (exchange.serverauth != null && !exchange.serverauth.fromcache) {
- AuthInfo au = exchange.serverauth;
- cache.store(au.scheme, req.uri(), false, au.credentials);
- }
- if (exchange.proxyauth != null && !exchange.proxyauth.fromcache) {
- AuthInfo au = exchange.proxyauth;
- cache.store(au.scheme, req.uri(), false, au.credentials);
- }
- return null;
- }
-
- boolean proxy = status == PROXY_UNAUTHORIZED;
- String authname = proxy ? "Proxy-Authenticate" : "WWW-Authenticate";
- String authval = hdrs.firstValue(authname).orElseThrow(() -> {
- return new IOException("Invalid auth header");
- });
- HeaderParser parser = new HeaderParser(authval);
- String scheme = parser.findKey(0);
-
- // TODO: Need to generalise from Basic only. Delegate to a provider class etc.
-
- if (!scheme.equalsIgnoreCase("Basic")) {
- return null; // error gets returned to app
- }
-
- if (proxy) {
- if (r.isConnectResponse) {
- if (!Utils.PROXY_TUNNEL_FILTER
- .test("Proxy-Authorization", BASIC_DUMMY)) {
- Log.logError("{0} disabled", "Proxy-Authorization");
- return null;
- }
- } else if (req.proxy() != null) {
- if (!Utils.PROXY_FILTER
- .test("Proxy-Authorization", BASIC_DUMMY)) {
- Log.logError("{0} disabled", "Proxy-Authorization");
- return null;
- }
- }
- }
-
- AuthInfo au = proxy ? exchange.proxyauth : exchange.serverauth;
- if (au == null) {
- // if no authenticator, let the user deal with 407/401
- if (!exchange.client().authenticator().isPresent()) return null;
-
- PasswordAuthentication pw = getCredentials(authval, proxy, req);
- if (pw == null) {
- throw new IOException("No credentials provided");
- }
- // No authentication in request. Get credentials from user
- au = new AuthInfo(false, "Basic", pw);
- if (proxy) {
- exchange.proxyauth = au;
- } else {
- exchange.serverauth = au;
- }
- addBasicCredentials(req, proxy, pw);
- return req;
- } else if (au.retries > retry_limit) {
- throw new IOException("too many authentication attempts. Limit: " +
- Integer.toString(retry_limit));
- } else {
- // we sent credentials, but they were rejected
- if (au.fromcache) {
- cache.remove(au.cacheEntry);
- }
-
- // if no authenticator, let the user deal with 407/401
- if (!exchange.client().authenticator().isPresent()) return null;
-
- // try again
- PasswordAuthentication pw = getCredentials(authval, proxy, req);
- if (pw == null) {
- throw new IOException("No credentials provided");
- }
- au = au.retryWithCredentials(pw);
- if (proxy) {
- exchange.proxyauth = au;
- } else {
- exchange.serverauth = au;
- }
- addBasicCredentials(req, proxy, au.credentials);
- au.retries++;
- return req;
- }
- }
-
- // Use a WeakHashMap to make it possible for the HttpClient to
- // be garbaged collected when no longer referenced.
- static final WeakHashMap<HttpClientImpl,Cache> caches = new WeakHashMap<>();
-
- static synchronized Cache getCache(MultiExchange<?> exchange) {
- HttpClientImpl client = exchange.client();
- Cache c = caches.get(client);
- if (c == null) {
- c = new Cache();
- caches.put(client, c);
- }
- return c;
- }
-
- // Note: Make sure that Cache and CacheEntry do not keep any strong
- // reference to the HttpClient: it would prevent the client being
- // GC'ed when no longer referenced.
- static class Cache {
- final LinkedList<CacheEntry> entries = new LinkedList<>();
-
- synchronized CacheEntry get(URI uri, boolean proxy) {
- for (CacheEntry entry : entries) {
- if (entry.equalsKey(uri, proxy)) {
- return entry;
- }
- }
- return null;
- }
-
- synchronized void remove(String authscheme, URI domain, boolean proxy) {
- for (CacheEntry entry : entries) {
- if (entry.equalsKey(domain, proxy)) {
- entries.remove(entry);
- }
- }
- }
-
- synchronized void remove(CacheEntry entry) {
- entries.remove(entry);
- }
-
- synchronized void store(String authscheme,
- URI domain,
- boolean proxy,
- PasswordAuthentication value) {
- remove(authscheme, domain, proxy);
- entries.add(new CacheEntry(authscheme, domain, proxy, value));
- }
- }
-
- static class CacheEntry {
- final String root;
- final String scheme;
- final boolean proxy;
- final PasswordAuthentication value;
-
- CacheEntry(String authscheme,
- URI uri,
- boolean proxy,
- PasswordAuthentication value) {
- this.scheme = authscheme;
- this.root = uri.resolve(".").toString(); // remove extraneous components
- this.proxy = proxy;
- this.value = value;
- }
-
- public PasswordAuthentication value() {
- return value;
- }
-
- public boolean equalsKey(URI uri, boolean proxy) {
- if (this.proxy != proxy) {
- return false;
- }
- String other = uri.toString();
- return other.startsWith(root);
- }
- }
-}
--- a/src/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<T> implements BodySubscriber<T>
-{
- /** The downstream consumer of the data. */
- private final BodySubscriber<T> downstreamSubscriber;
- /** The amount of data to be accumulate before pushing downstream. */
- private final int bufferSize;
-
- /** The subscription, created lazily. */
- private volatile Flow.Subscription subscription;
- /** The downstream subscription, created lazily. */
- private volatile DownstreamSubscription downstreamSubscription;
-
- /** Must be held when accessing the internal buffers. */
- private final Object buffersLock = new Object();
- /** The internal buffers holding the buffered data. */
- private ArrayList<ByteBuffer> internalBuffers;
- /** The actual accumulated remaining bytes in internalBuffers. */
- private int accumulatedBytes;
-
- /** Holds the Throwable from upstream's onError. */
- private volatile Throwable throwable;
-
- /** State of the buffering subscriber:
- * 1) [UNSUBSCRIBED] when initially created
- * 2) [ACTIVE] when subscribed and can receive data
- * 3) [ERROR | CANCELLED | COMPLETE] (terminal state)
- */
- static final int UNSUBSCRIBED = 0x01;
- static final int ACTIVE = 0x02;
- static final int ERROR = 0x04;
- static final int CANCELLED = 0x08;
- static final int COMPLETE = 0x10;
-
- private volatile int state;
-
- public BufferingSubscriber(BodySubscriber<T> downstreamSubscriber,
- int bufferSize) {
- this.downstreamSubscriber = Objects.requireNonNull(downstreamSubscriber);
- this.bufferSize = bufferSize;
- synchronized (buffersLock) {
- internalBuffers = new ArrayList<>();
- }
- state = UNSUBSCRIBED;
- }
-
- /** Returns the number of bytes remaining in the given buffers. */
- private static final long remaining(List<ByteBuffer> buffers) {
- return buffers.stream().mapToLong(ByteBuffer::remaining).sum();
- }
-
- /**
- * Tells whether, or not, there is at least a sufficient number of bytes
- * accumulated in the internal buffers. If the subscriber is COMPLETE, and
- * has some buffered data, then there is always enough ( to pass downstream ).
- */
- private final boolean hasEnoughAccumulatedBytes() {
- assert Thread.holdsLock(buffersLock);
- return accumulatedBytes >= bufferSize
- || (state == COMPLETE && accumulatedBytes > 0);
- }
-
- /**
- * Returns a new, unmodifiable, List<ByteBuffer> containing exactly the
- * amount of data as required before pushing downstream. The amount of data
- * may be less than required ( bufferSize ), in the case where the subscriber
- * is COMPLETE.
- */
- private List<ByteBuffer> fromInternalBuffers() {
- assert Thread.holdsLock(buffersLock);
- int leftToFill = bufferSize;
- int state = this.state;
- assert (state == ACTIVE || state == CANCELLED)
- ? accumulatedBytes >= leftToFill : true;
- List<ByteBuffer> dsts = new ArrayList<>();
-
- ListIterator<ByteBuffer> itr = internalBuffers.listIterator();
- while (itr.hasNext()) {
- ByteBuffer b = itr.next();
- if (b.remaining() <= leftToFill) {
- itr.remove();
- if (b.position() != 0)
- b = b.slice(); // ensure position = 0 when propagated
- dsts.add(b);
- leftToFill -= b.remaining();
- accumulatedBytes -= b.remaining();
- if (leftToFill == 0)
- break;
- } else {
- int prevLimit = b.limit();
- b.limit(b.position() + leftToFill);
- ByteBuffer slice = b.slice();
- dsts.add(slice);
- b.limit(prevLimit);
- b.position(b.position() + leftToFill);
- accumulatedBytes -= leftToFill;
- leftToFill = 0;
- break;
- }
- }
- assert (state == ACTIVE || state == CANCELLED)
- ? leftToFill == 0 : state == COMPLETE;
- assert (state == ACTIVE || state == CANCELLED)
- ? remaining(dsts) == bufferSize : state == COMPLETE;
- assert accumulatedBytes >= 0;
- assert dsts.stream().noneMatch(b -> b.position() != 0);
- return Collections.unmodifiableList(dsts);
- }
-
- /** Subscription that is passed to the downstream subscriber. */
- private class DownstreamSubscription implements Flow.Subscription {
- private final AtomicBoolean cancelled = new AtomicBoolean(); // false
- private final Demand demand = new Demand();
- private volatile boolean illegalArg;
-
- @Override
- public void request(long n) {
- if (cancelled.get() || illegalArg) {
- return;
- }
- if (n <= 0L) {
- // pass the "bad" value upstream so the Publisher can deal with
- // it appropriately, i.e. invoke onError
- illegalArg = true;
- subscription.request(n);
- return;
- }
-
- demand.increase(n);
-
- pushDemanded();
- }
-
- private final SequentialScheduler pushDemandedScheduler =
- new SequentialScheduler(new PushDemandedTask());
-
- void pushDemanded() {
- if (cancelled.get())
- return;
- pushDemandedScheduler.runOrSchedule();
- }
-
- class PushDemandedTask extends SequentialScheduler.CompleteRestartableTask {
- @Override
- public void run() {
- try {
- Throwable t = throwable;
- if (t != null) {
- pushDemandedScheduler.stop(); // stop the demand scheduler
- downstreamSubscriber.onError(t);
- return;
- }
-
- while (true) {
- List<ByteBuffer> item;
- synchronized (buffersLock) {
- if (cancelled.get())
- return;
- if (!hasEnoughAccumulatedBytes())
- break;
- if (!demand.tryDecrement())
- break;
- item = fromInternalBuffers();
- }
- assert item != null;
-
- downstreamSubscriber.onNext(item);
- }
- if (cancelled.get())
- return;
-
- // complete only if all data consumed
- boolean complete;
- synchronized (buffersLock) {
- complete = state == COMPLETE && internalBuffers.isEmpty();
- }
- if (complete) {
- assert internalBuffers.isEmpty();
- pushDemandedScheduler.stop(); // stop the demand scheduler
- downstreamSubscriber.onComplete();
- return;
- }
- } catch (Throwable t) {
- cancel(); // cancel if there is any error
- throw t;
- }
-
- boolean requestMore = false;
- synchronized (buffersLock) {
- if (!hasEnoughAccumulatedBytes() && !demand.isFulfilled()) {
- // request more upstream data
- requestMore = true;
- }
- }
- if (requestMore)
- subscription.request(1);
- }
- }
-
- @Override
- public void cancel() {
- if (cancelled.compareAndExchange(false, true))
- return; // already cancelled
-
- state = CANCELLED; // set CANCELLED state of upstream subscriber
- subscription.cancel(); // cancel upstream subscription
- pushDemandedScheduler.stop(); // stop the demand scheduler
- }
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- Objects.requireNonNull(subscription);
- if (this.subscription != null) {
- subscription.cancel();
- return;
- }
-
- int s = this.state;
- assert s == UNSUBSCRIBED;
- state = ACTIVE;
- this.subscription = subscription;
- downstreamSubscription = new DownstreamSubscription();
- downstreamSubscriber.onSubscribe(downstreamSubscription);
- }
-
- @Override
- public void onNext(List<ByteBuffer> item) {
- Objects.requireNonNull(item);
-
- int s = state;
- if (s == CANCELLED)
- return;
-
- if (s != ACTIVE)
- throw new InternalError("onNext on inactive subscriber");
-
- synchronized (buffersLock) {
- internalBuffers.addAll(item);
- accumulatedBytes += remaining(item);
- }
-
- downstreamSubscription.pushDemanded();
- }
-
- @Override
- public void onError(Throwable incomingThrowable) {
- Objects.requireNonNull(incomingThrowable);
- int s = state;
- assert s == ACTIVE : "Expected ACTIVE, got:" + s;
- state = ERROR;
- Throwable t = this.throwable;
- assert t == null : "Expected null, got:" + t;
- this.throwable = incomingThrowable;
- downstreamSubscription.pushDemanded();
- }
-
- @Override
- public void onComplete() {
- int s = state;
- assert s == ACTIVE : "Expected ACTIVE, got:" + s;
- state = COMPLETE;
- downstreamSubscription.pushDemanded();
- }
-
- @Override
- public CompletionStage<T> getBody() {
- return downstreamSubscriber.getBody();
- }
-}
--- 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<CacheKey,LinkedList<HttpConnection>> plainPool;
- private final HashMap<CacheKey,LinkedList<HttpConnection>> sslPool;
- private final ExpiryList expiryList;
- private final String dbgTag; // used for debug
- boolean stopped;
-
- /**
- * Entries in connection pool are keyed by destination address and/or
- * proxy address:
- * case 1: plain TCP not via proxy (destination only)
- * case 2: plain TCP via proxy (proxy only)
- * case 3: SSL not via proxy (destination only)
- * case 4: SSL over tunnel (destination and proxy)
- */
- static class CacheKey {
- final InetSocketAddress proxy;
- final InetSocketAddress destination;
-
- CacheKey(InetSocketAddress destination, InetSocketAddress proxy) {
- this.proxy = proxy;
- this.destination = destination;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- final CacheKey other = (CacheKey) obj;
- if (!Objects.equals(this.proxy, other.proxy)) {
- return false;
- }
- if (!Objects.equals(this.destination, other.destination)) {
- return false;
- }
- return true;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(proxy, destination);
- }
- }
-
- ConnectionPool(long clientId) {
- this("ConnectionPool("+clientId+")");
- }
-
- /**
- * There should be one of these per HttpClient.
- */
- private ConnectionPool(String tag) {
- dbgTag = tag;
- plainPool = new HashMap<>();
- sslPool = new HashMap<>();
- expiryList = new ExpiryList();
- }
-
- final String dbgString() {
- return dbgTag;
- }
-
- synchronized void start() {
- assert !stopped : "Already stopped";
- }
-
- static CacheKey cacheKey(InetSocketAddress destination,
- InetSocketAddress proxy)
- {
- return new CacheKey(destination, proxy);
- }
-
- synchronized HttpConnection getConnection(boolean secure,
- InetSocketAddress addr,
- InetSocketAddress proxy) {
- if (stopped) return null;
- CacheKey key = new CacheKey(addr, proxy);
- HttpConnection c = secure ? findConnection(key, sslPool)
- : findConnection(key, plainPool);
- //System.out.println ("getConnection returning: " + c);
- return c;
- }
-
- /**
- * Returns the connection to the pool.
- */
- void returnToPool(HttpConnection conn) {
- returnToPool(conn, Instant.now(), KEEP_ALIVE);
- }
-
- // Called also by whitebox tests
- void returnToPool(HttpConnection conn, Instant now, long keepAlive) {
-
- // Don't call registerCleanupTrigger while holding a lock,
- // but register it before the connection is added to the pool,
- // since we don't want to trigger the cleanup if the connection
- // is not in the pool.
- CleanupTrigger cleanup = registerCleanupTrigger(conn);
-
- // it's possible that cleanup may have been called.
- synchronized(this) {
- if (cleanup.isDone()) {
- return;
- } else if (stopped) {
- conn.close();
- return;
- }
- if (conn instanceof PlainHttpConnection) {
- putConnection(conn, plainPool);
- } else {
- assert conn.isSecure();
- putConnection(conn, sslPool);
- }
- expiryList.add(conn, now, keepAlive);
- }
- //System.out.println("Return to pool: " + conn);
- }
-
- private CleanupTrigger registerCleanupTrigger(HttpConnection conn) {
- // Connect the connection flow to a pub/sub pair that will take the
- // connection out of the pool and close it if anything happens
- // while the connection is sitting in the pool.
- CleanupTrigger cleanup = new CleanupTrigger(conn);
- FlowTube flow = conn.getConnectionFlow();
- debug.log(Level.DEBUG, "registering %s", cleanup);
- flow.connectFlows(cleanup, cleanup);
- return cleanup;
- }
-
- private HttpConnection
- findConnection(CacheKey key,
- HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
- LinkedList<HttpConnection> l = pool.get(key);
- if (l == null || l.isEmpty()) {
- return null;
- } else {
- HttpConnection c = l.removeFirst();
- expiryList.remove(c);
- return c;
- }
- }
-
- /* called from cache cleaner only */
- private boolean
- removeFromPool(HttpConnection c,
- HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
- //System.out.println("cacheCleaner removing: " + c);
- assert Thread.holdsLock(this);
- CacheKey k = c.cacheKey();
- List<HttpConnection> l = pool.get(k);
- if (l == null || l.isEmpty()) {
- pool.remove(k);
- return false;
- }
- return l.remove(c);
- }
-
- private void
- putConnection(HttpConnection c,
- HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
- CacheKey key = c.cacheKey();
- LinkedList<HttpConnection> l = pool.get(key);
- if (l == null) {
- l = new LinkedList<>();
- pool.put(key, l);
- }
- l.add(c);
- }
-
- /**
- * Purge expired connection and return the number of milliseconds
- * in which the next connection is scheduled to expire.
- * If no connections are scheduled to be purged return 0.
- * @return the delay in milliseconds in which the next connection will
- * expire.
- */
- long purgeExpiredConnectionsAndReturnNextDeadline() {
- if (!expiryList.purgeMaybeRequired()) return 0;
- return purgeExpiredConnectionsAndReturnNextDeadline(Instant.now());
- }
-
- // Used for whitebox testing
- long purgeExpiredConnectionsAndReturnNextDeadline(Instant now) {
- long nextPurge = 0;
-
- // We may be in the process of adding new elements
- // to the expiry list - but those elements will not
- // have outlast their keep alive timer yet since we're
- // just adding them.
- if (!expiryList.purgeMaybeRequired()) return nextPurge;
-
- List<HttpConnection> closelist;
- synchronized (this) {
- closelist = expiryList.purgeUntil(now);
- for (HttpConnection c : closelist) {
- if (c instanceof PlainHttpConnection) {
- boolean wasPresent = removeFromPool(c, plainPool);
- assert wasPresent;
- } else {
- boolean wasPresent = removeFromPool(c, sslPool);
- assert wasPresent;
- }
- }
- nextPurge = now.until(
- expiryList.nextExpiryDeadline().orElse(now),
- ChronoUnit.MILLIS);
- }
- closelist.forEach(this::close);
- return nextPurge;
- }
-
- private void close(HttpConnection c) {
- try {
- c.close();
- } catch (Throwable e) {} // ignore
- }
-
- void stop() {
- List<HttpConnection> closelist = Collections.emptyList();
- try {
- synchronized (this) {
- stopped = true;
- closelist = expiryList.stream()
- .map(e -> e.connection)
- .collect(Collectors.toList());
- expiryList.clear();
- plainPool.clear();
- sslPool.clear();
- }
- } finally {
- closelist.forEach(this::close);
- }
- }
-
- static final class ExpiryEntry {
- final HttpConnection connection;
- final Instant expiry; // absolute time in seconds of expiry time
- ExpiryEntry(HttpConnection connection, Instant expiry) {
- this.connection = connection;
- this.expiry = expiry;
- }
- }
-
- /**
- * Manages a LinkedList of sorted ExpiryEntry. The entry with the closer
- * deadline is at the tail of the list, and the entry with the farther
- * deadline is at the head. In the most common situation, new elements
- * will need to be added at the head (or close to it), and expired elements
- * will need to be purged from the tail.
- */
- private static final class ExpiryList {
- private final LinkedList<ExpiryEntry> list = new LinkedList<>();
- private volatile boolean mayContainEntries;
-
- // A loosely accurate boolean whose value is computed
- // at the end of each operation performed on ExpiryList;
- // Does not require synchronizing on the ConnectionPool.
- boolean purgeMaybeRequired() {
- return mayContainEntries;
- }
-
- // Returns the next expiry deadline
- // should only be called while holding a synchronization
- // lock on the ConnectionPool
- Optional<Instant> nextExpiryDeadline() {
- if (list.isEmpty()) return Optional.empty();
- else return Optional.of(list.getLast().expiry);
- }
-
- // should only be called while holding a synchronization
- // lock on the ConnectionPool
- void add(HttpConnection conn) {
- add(conn, Instant.now(), KEEP_ALIVE);
- }
-
- // Used by whitebox test.
- void add(HttpConnection conn, Instant now, long keepAlive) {
- Instant then = now.truncatedTo(ChronoUnit.SECONDS)
- .plus(keepAlive, ChronoUnit.SECONDS);
-
- // Elements with the farther deadline are at the head of
- // the list. It's more likely that the new element will
- // have the farthest deadline, and will need to be inserted
- // at the head of the list, so we're using an ascending
- // list iterator to find the right insertion point.
- ListIterator<ExpiryEntry> li = list.listIterator();
- while (li.hasNext()) {
- ExpiryEntry entry = li.next();
-
- if (then.isAfter(entry.expiry)) {
- li.previous();
- // insert here
- li.add(new ExpiryEntry(conn, then));
- mayContainEntries = true;
- return;
- }
- }
- // last (or first) element of list (the last element is
- // the first when the list is empty)
- list.add(new ExpiryEntry(conn, then));
- mayContainEntries = true;
- }
-
- // should only be called while holding a synchronization
- // lock on the ConnectionPool
- void remove(HttpConnection c) {
- if (c == null || list.isEmpty()) return;
- ListIterator<ExpiryEntry> li = list.listIterator();
- while (li.hasNext()) {
- ExpiryEntry e = li.next();
- if (e.connection.equals(c)) {
- li.remove();
- mayContainEntries = !list.isEmpty();
- return;
- }
- }
- }
-
- // should only be called while holding a synchronization
- // lock on the ConnectionPool.
- // Purge all elements whose deadline is before now (now included).
- List<HttpConnection> purgeUntil(Instant now) {
- if (list.isEmpty()) return Collections.emptyList();
-
- List<HttpConnection> closelist = new ArrayList<>();
-
- // elements with the closest deadlines are at the tail
- // of the queue, so we're going to use a descending iterator
- // to remove them, and stop when we find the first element
- // that has not expired yet.
- Iterator<ExpiryEntry> li = list.descendingIterator();
- while (li.hasNext()) {
- ExpiryEntry entry = li.next();
- // use !isAfter instead of isBefore in order to
- // remove the entry if its expiry == now
- if (!entry.expiry.isAfter(now)) {
- li.remove();
- HttpConnection c = entry.connection;
- closelist.add(c);
- } else break; // the list is sorted
- }
- mayContainEntries = !list.isEmpty();
- return closelist;
- }
-
- // should only be called while holding a synchronization
- // lock on the ConnectionPool
- java.util.stream.Stream<ExpiryEntry> stream() {
- return list.stream();
- }
-
- // should only be called while holding a synchronization
- // lock on the ConnectionPool
- void clear() {
- list.clear();
- mayContainEntries = false;
- }
- }
-
- void cleanup(HttpConnection c, Throwable error) {
- debug.log(Level.DEBUG,
- "%s : ConnectionPool.cleanup(%s)",
- String.valueOf(c.getConnectionFlow()),
- error);
- synchronized(this) {
- if (c instanceof PlainHttpConnection) {
- removeFromPool(c, plainPool);
- } else {
- assert c.isSecure();
- removeFromPool(c, sslPool);
- }
- expiryList.remove(c);
- }
- c.close();
- }
-
- /**
- * An object that subscribes to the flow while the connection is in
- * the pool. Anything that comes in will cause the connection to be closed
- * and removed from the pool.
- */
- private final class CleanupTrigger implements
- FlowTube.TubeSubscriber, FlowTube.TubePublisher,
- Flow.Subscription {
-
- private final HttpConnection connection;
- private volatile boolean done;
-
- public CleanupTrigger(HttpConnection connection) {
- this.connection = connection;
- }
-
- public boolean isDone() { return done;}
-
- private void triggerCleanup(Throwable error) {
- done = true;
- cleanup(connection, error);
- }
-
- @Override public void request(long n) {}
- @Override public void cancel() {}
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- subscription.request(1);
- }
- @Override
- public void onError(Throwable error) { triggerCleanup(error); }
- @Override
- public void onComplete() { triggerCleanup(null); }
- @Override
- public void onNext(List<ByteBuffer> item) {
- triggerCleanup(new IOException("Data received while in pool"));
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
- subscriber.onSubscribe(this);
- }
-
- @Override
- public String toString() {
- return "CleanupTrigger(" + connection.getConnectionFlow() + ")";
- }
-
- }
-
-}
--- a/src/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<CookieHandler> cookieHandlerOpt = client.cookieHandler();
- if (cookieHandlerOpt.isPresent()) {
- CookieHandler cookieHandler = cookieHandlerOpt.get();
- Map<String,List<String>> userheaders = r.getUserHeaders().map();
- Map<String,List<String>> cookies = cookieHandler.get(r.uri(), userheaders);
-
- // add the returned cookies
- HttpHeadersImpl systemHeaders = r.getSystemHeaders();
- if (cookies.isEmpty()) {
- Log.logTrace("Request: no cookie to add for {0}",
- r.uri());
- } else {
- Log.logTrace("Request: adding cookies for {0}",
- r.uri());
- }
- for (String hdrname : cookies.keySet()) {
- List<String> vals = cookies.get(hdrname);
- for (String val : vals) {
- systemHeaders.addHeader(hdrname, val);
- }
- }
- } else {
- Log.logTrace("Request: No cookie manager found for {0}",
- r.uri());
- }
- }
-
- @Override
- public HttpRequestImpl response(Response r) throws IOException {
- HttpHeaders hdrs = r.headers();
- HttpRequestImpl request = r.request();
- Exchange<?> e = r.exchange;
- Log.logTrace("Response: processing cookies for {0}", request.uri());
- Optional<CookieHandler> cookieHandlerOpt = e.client().cookieHandler();
- if (cookieHandlerOpt.isPresent()) {
- CookieHandler cookieHandler = cookieHandlerOpt.get();
- Log.logTrace("Response: parsing cookies from {0}", hdrs.map());
- cookieHandler.put(request.uri(), hdrs.map());
- } else {
- Log.logTrace("Response: No cookie manager found for {0}",
- request.uri());
- }
- return null;
- }
-}
--- a/src/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<T> {
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
-
- final HttpRequestImpl request;
- final HttpClientImpl client;
- volatile ExchangeImpl<T> exchImpl;
- volatile CompletableFuture<? extends ExchangeImpl<T>> exchangeCF;
- volatile CompletableFuture<Void> bodyIgnored;
-
- // used to record possible cancellation raised before the exchImpl
- // has been established.
- private volatile IOException failed;
- final AccessControlContext acc;
- final MultiExchange<T> multi;
- final Executor parentExecutor;
- boolean upgrading; // to HTTP/2
- final PushGroup<T> pushGroup;
- final String dbgTag;
-
- Exchange(HttpRequestImpl request, MultiExchange<T> multi) {
- this.request = request;
- this.upgrading = false;
- this.client = multi.client();
- this.multi = multi;
- this.acc = multi.acc;
- this.parentExecutor = multi.executor;
- this.pushGroup = multi.pushGroup;
- this.dbgTag = "Exchange";
- }
-
- /* If different AccessControlContext to be used */
- Exchange(HttpRequestImpl request,
- MultiExchange<T> multi,
- AccessControlContext acc)
- {
- this.request = request;
- this.acc = acc;
- this.upgrading = false;
- this.client = multi.client();
- this.multi = multi;
- this.parentExecutor = multi.executor;
- this.pushGroup = multi.pushGroup;
- this.dbgTag = "Exchange";
- }
-
- PushGroup<T> getPushGroup() {
- return pushGroup;
- }
-
- Executor executor() {
- return parentExecutor;
- }
-
- public HttpRequestImpl request() {
- return request;
- }
-
- HttpClientImpl client() {
- return client;
- }
-
-
- public CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler) {
- // If we received a 407 while establishing the exchange
- // there will be no body to read: bodyIgnored will be true,
- // and exchImpl will be null (if we were trying to establish
- // an HTTP/2 tunnel through an HTTP/1.1 proxy)
- if (bodyIgnored != null) return MinimalFuture.completedFuture(null);
-
- // The connection will not be returned to the pool in the case of WebSocket
- return exchImpl.readBodyAsync(handler, !request.isWebSocket(), parentExecutor)
- .whenComplete((r,t) -> exchImpl.completed());
- }
-
- /**
- * Called after a redirect or similar kind of retry where a body might
- * be sent but we don't want it. Should send a RESET in h2. For http/1.1
- * we can consume small quantity of data, or close the connection in
- * other cases.
- */
- public CompletableFuture<Void> ignoreBody() {
- if (bodyIgnored != null) return bodyIgnored;
- return exchImpl.ignoreBody();
- }
-
- /**
- * Called when a new exchange is created to replace this exchange.
- * At this point it is guaranteed that readBody/readBodyAsync will
- * not be called.
- */
- public void released() {
- ExchangeImpl<?> impl = exchImpl;
- if (impl != null) impl.released();
- // Don't set exchImpl to null here. We need to keep
- // it alive until it's replaced by a Stream in wrapForUpgrade.
- // Setting it to null here might get it GC'ed too early, because
- // the Http1Response is now only weakly referenced by the Selector.
- }
-
- public void cancel() {
- // cancel can be called concurrently before or at the same time
- // that the exchange impl is being established.
- // In that case we won't be able to propagate the cancellation
- // right away
- if (exchImpl != null) {
- exchImpl.cancel();
- } else {
- // no impl - can't cancel impl yet.
- // call cancel(IOException) instead which takes care
- // of race conditions between impl/cancel.
- cancel(new IOException("Request cancelled"));
- }
- }
-
- public void cancel(IOException cause) {
- // If the impl is non null, propagate the exception right away.
- // Otherwise record it so that it can be propagated once the
- // exchange impl has been established.
- ExchangeImpl<?> impl = exchImpl;
- if (impl != null) {
- // propagate the exception to the impl
- debug.log(Level.DEBUG, "Cancelling exchImpl: %s", exchImpl);
- impl.cancel(cause);
- } else {
- // no impl yet. record the exception
- failed = cause;
- // now call checkCancelled to recheck the impl.
- // if the failed state is set and the impl is not null, reset
- // the failed state and propagate the exception to the impl.
- checkCancelled();
- }
- }
-
- // This method will raise an exception if one was reported and if
- // it is possible to do so. If the exception can be raised, then
- // the failed state will be reset. Otherwise, the failed state
- // will persist until the exception can be raised and the failed state
- // can be cleared.
- // Takes care of possible race conditions.
- private void checkCancelled() {
- ExchangeImpl<?> impl = null;
- IOException cause = null;
- CompletableFuture<? extends ExchangeImpl<T>> cf = null;
- if (failed != null) {
- synchronized(this) {
- cause = failed;
- impl = exchImpl;
- cf = exchangeCF;
- }
- }
- if (cause == null) return;
- if (impl != null) {
- // The exception is raised by propagating it to the impl.
- debug.log(Level.DEBUG, "Cancelling exchImpl: %s", impl);
- impl.cancel(cause);
- failed = null;
- } else {
- Log.logTrace("Exchange: request [{0}/timeout={1}ms] no impl is set."
- + "\n\tCan''t cancel yet with {2}",
- request.uri(),
- request.timeout().isPresent() ?
- // calling duration.toMillis() can throw an exception.
- // this is just debugging, we don't care if it overflows.
- (request.timeout().get().getSeconds() * 1000
- + request.timeout().get().getNano() / 1000000) : -1,
- cause);
- if (cf != null) cf.completeExceptionally(cause);
- }
- }
-
- public void h2Upgrade() {
- upgrading = true;
- request.setH2Upgrade(client.client2());
- }
-
- synchronized IOException getCancelCause() {
- return failed;
- }
-
- // get/set the exchange impl, solving race condition issues with
- // potential concurrent calls to cancel() or cancel(IOException)
- private CompletableFuture<? extends ExchangeImpl<T>>
- establishExchange(HttpConnection connection) {
- if (debug.isLoggable(Level.DEBUG)) {
- debug.log(Level.DEBUG,
- "establishing exchange for %s,%n\t proxy=%s",
- request,
- request.proxy());
- }
- // check if we have been cancelled first.
- Throwable t = getCancelCause();
- checkCancelled();
- if (t != null) {
- return MinimalFuture.failedFuture(t);
- }
-
- CompletableFuture<? extends ExchangeImpl<T>> cf, res;
- cf = ExchangeImpl.get(this, connection);
- // We should probably use a VarHandle to get/set exchangeCF
- // instead - as we need CAS semantics.
- synchronized (this) { exchangeCF = cf; };
- res = cf.whenComplete((r,x) -> {
- synchronized(Exchange.this) {
- if (exchangeCF == cf) exchangeCF = null;
- }
- });
- checkCancelled();
- return res.thenCompose((eimpl) -> {
- // recheck for cancelled, in case of race conditions
- exchImpl = eimpl;
- IOException tt = getCancelCause();
- checkCancelled();
- if (tt != null) {
- return MinimalFuture.failedFuture(tt);
- } else {
- // Now we're good to go. Because exchImpl is no longer
- // null cancel() will be able to propagate directly to
- // the impl after this point ( if needed ).
- return MinimalFuture.completedFuture(eimpl);
- } });
- }
-
- // Completed HttpResponse will be null if response succeeded
- // will be a non null responseAsync if expect continue returns an error
-
- public CompletableFuture<Response> responseAsync() {
- return responseAsyncImpl(null);
- }
-
- CompletableFuture<Response> responseAsyncImpl(HttpConnection connection) {
- SecurityException e = checkPermissions();
- if (e != null) {
- return MinimalFuture.failedFuture(e);
- } else {
- return responseAsyncImpl0(connection);
- }
- }
-
- // check whether the headersSentCF was completed exceptionally with
- // ProxyAuthorizationRequired. If so the Response embedded in the
- // exception is returned. Otherwise we proceed.
- private CompletableFuture<Response> checkFor407(ExchangeImpl<T> ex, Throwable t,
- Function<ExchangeImpl<T>,CompletableFuture<Response>> andThen) {
- t = Utils.getCompletionCause(t);
- if (t instanceof ProxyAuthenticationRequired) {
- bodyIgnored = MinimalFuture.completedFuture(null);
- Response proxyResponse = ((ProxyAuthenticationRequired)t).proxyResponse;
- Response syntheticResponse = new Response(request, this,
- proxyResponse.headers, proxyResponse.statusCode,
- proxyResponse.version, true);
- return MinimalFuture.completedFuture(syntheticResponse);
- } else if (t != null) {
- return MinimalFuture.failedFuture(t);
- } else {
- return andThen.apply(ex);
- }
- }
-
- // After sending the request headers, if no ProxyAuthorizationRequired
- // was raised and the expectContinue flag is on, we need to wait
- // for the 100-Continue response
- private CompletableFuture<Response> expectContinue(ExchangeImpl<T> ex) {
- assert request.expectContinue();
- return ex.getResponseAsync(parentExecutor)
- .thenCompose((Response r1) -> {
- Log.logResponse(r1::toString);
- int rcode = r1.statusCode();
- if (rcode == 100) {
- Log.logTrace("Received 100-Continue: sending body");
- CompletableFuture<Response> cf =
- exchImpl.sendBodyAsync()
- .thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
- cf = wrapForUpgrade(cf);
- cf = wrapForLog(cf);
- return cf;
- } else {
- Log.logTrace("Expectation failed: Received {0}",
- rcode);
- if (upgrading && rcode == 101) {
- IOException failed = new IOException(
- "Unable to handle 101 while waiting for 100");
- return MinimalFuture.failedFuture(failed);
- }
- return exchImpl.readBodyAsync(this::ignoreBody, false, parentExecutor)
- .thenApply(v -> r1);
- }
- });
- }
-
- // After sending the request headers, if no ProxyAuthorizationRequired
- // was raised and the expectContinue flag is off, we can immediately
- // send the request body and proceed.
- private CompletableFuture<Response> sendRequestBody(ExchangeImpl<T> ex) {
- assert !request.expectContinue();
- CompletableFuture<Response> cf = ex.sendBodyAsync()
- .thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
- cf = wrapForUpgrade(cf);
- cf = wrapForLog(cf);
- return cf;
- }
-
- CompletableFuture<Response> responseAsyncImpl0(HttpConnection connection) {
- Function<ExchangeImpl<T>, CompletableFuture<Response>> after407Check;
- bodyIgnored = null;
- if (request.expectContinue()) {
- request.addSystemHeader("Expect", "100-Continue");
- Log.logTrace("Sending Expect: 100-Continue");
- // wait for 100-Continue before sending body
- after407Check = this::expectContinue;
- } else {
- // send request body and proceed.
- after407Check = this::sendRequestBody;
- }
- // The ProxyAuthorizationRequired can be triggered either by
- // establishExchange (case of HTTP/2 SSL tunelling through HTTP/1.1 proxy
- // or by sendHeaderAsync (case of HTTP/1.1 SSL tunelling through HTTP/1.1 proxy
- // Therefore we handle it with a call to this checkFor407(...) after these
- // two places.
- Function<ExchangeImpl<T>, CompletableFuture<Response>> afterExch407Check =
- (ex) -> ex.sendHeadersAsync()
- .handle((r,t) -> this.checkFor407(r, t, after407Check))
- .thenCompose(Function.identity());
- return establishExchange(connection)
- .handle((r,t) -> this.checkFor407(r,t, afterExch407Check))
- .thenCompose(Function.identity());
- }
-
- private CompletableFuture<Response> wrapForUpgrade(CompletableFuture<Response> cf) {
- if (upgrading) {
- return cf.thenCompose(r -> checkForUpgradeAsync(r, exchImpl));
- }
- return cf;
- }
-
- private CompletableFuture<Response> wrapForLog(CompletableFuture<Response> cf) {
- if (Log.requests()) {
- return cf.thenApply(response -> {
- Log.logResponse(response::toString);
- return response;
- });
- }
- return cf;
- }
-
- HttpResponse.BodySubscriber<T> ignoreBody(int status, HttpHeaders hdrs) {
- return HttpResponse.BodySubscriber.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<Response>
- checkForUpgradeAsync(Response resp,
- ExchangeImpl<T> ex) {
-
- int rcode = resp.statusCode();
- if (upgrading && (rcode == 101)) {
- Http1Exchange<T> e = (Http1Exchange<T>)ex;
- // check for 101 switching protocols
- // 101 responses are not supposed to contain a body.
- // => should we fail if there is one?
- debug.log(Level.DEBUG, "Upgrading async %s", e.connection());
- return e.readBodyAsync(this::ignoreBody, false, parentExecutor)
- .thenCompose((T v) -> {// v is null
- debug.log(Level.DEBUG, "Ignored body");
- // we pass e::getBuffer to allow the ByteBuffers to accumulate
- // while we build the Http2Connection
- return Http2Connection.createAsync(e.connection(),
- client.client2(),
- this, e::drainLeftOverBytes)
- .thenCompose((Http2Connection c) -> {
- boolean cached = c.offerConnection();
- Stream<T> s = c.getStream(1);
-
- if (s == null) {
- // s can be null if an exception occurred
- // asynchronously while sending the preface.
- Throwable t = c.getRecordedCause();
- IOException ioe;
- if (t != null) {
- if (!cached)
- c.close();
- ioe = new IOException("Can't get stream 1: " + t, t);
- } else {
- ioe = new IOException("Can't get stream 1");
- }
- return MinimalFuture.failedFuture(ioe);
- }
- exchImpl.released();
- Throwable t;
- // There's a race condition window where an external
- // thread (SelectorManager) might complete the
- // exchange in timeout at the same time where we're
- // trying to switch the exchange impl.
- // 'failed' will be reset to null after
- // exchImpl.cancel() has completed, so either we
- // will observe failed != null here, or we will
- // observe e.getCancelCause() != null, or the
- // timeout exception will be routed to 's'.
- // Either way, we need to relay it to s.
- synchronized (this) {
- exchImpl = s;
- t = failed;
- }
- // Check whether the HTTP/1.1 was cancelled.
- if (t == null) t = e.getCancelCause();
- // if HTTP/1.1 exchange was timed out, don't
- // try to go further.
- if (t instanceof HttpTimeoutException) {
- s.cancelImpl(t);
- return MinimalFuture.failedFuture(t);
- }
- debug.log(Level.DEBUG, "Getting response async %s", s);
- return s.getResponseAsync(null);
- });}
- );
- }
- return MinimalFuture.completedFuture(resp);
- }
-
- private URI getURIForSecurityCheck() {
- URI u;
- String method = request.method();
- InetSocketAddress authority = request.authority();
- URI uri = request.uri();
-
- // CONNECT should be restricted at API level
- if (method.equalsIgnoreCase("CONNECT")) {
- try {
- u = new URI("socket",
- null,
- authority.getHostString(),
- authority.getPort(),
- null,
- null,
- null);
- } catch (URISyntaxException e) {
- throw new InternalError(e); // shouldn't happen
- }
- } else {
- u = uri;
- }
- return u;
- }
-
- /**
- * Returns the security permission required for the given details.
- * If method is CONNECT, then uri must be of form "scheme://host:port"
- */
- private static URLPermission permissionForServer(URI uri,
- String method,
- Map<String, List<String>> headers) {
- if (method.equals("CONNECT")) {
- return new URLPermission(uri.toString(), "CONNECT");
- } else {
- return Utils.permissionForServer(uri, method, headers.keySet().stream());
- }
- }
-
- /**
- * Performs the necessary security permission checks required to retrieve
- * the response. Returns a security exception representing the denied
- * permission, or null if all checks pass or there is no security manager.
- */
- private SecurityException checkPermissions() {
- String method = request.method();
- SecurityManager sm = System.getSecurityManager();
- if (sm == null || method.equals("CONNECT")) {
- // tunneling will have a null acc, which is fine. The proxy
- // permission check will have already been preformed.
- return null;
- }
-
- HttpHeaders userHeaders = request.getUserHeaders();
- URI u = getURIForSecurityCheck();
- URLPermission p = permissionForServer(u, method, userHeaders.map());
-
- try {
- assert acc != null;
- sm.checkPermission(p, acc);
- } catch (SecurityException e) {
- return e;
- }
- ProxySelector ps = client.proxySelector();
- if (ps != null) {
- if (!method.equals("CONNECT")) {
- // a non-tunneling HTTP proxy. Need to check access
- URLPermission proxyPerm = permissionForProxy(request.proxy());
- if (proxyPerm != null) {
- try {
- sm.checkPermission(proxyPerm, acc);
- } catch (SecurityException e) {
- return e;
- }
- }
- }
- }
- return null;
- }
-
- HttpClient.Version version() {
- return multi.version();
- }
-
- String dbgString() {
- return dbgTag;
- }
-}
--- a/src/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<T> {
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- private static final System.Logger DEBUG_LOGGER =
- Utils.getDebugLogger("ExchangeImpl"::toString, DEBUG);
-
- final Exchange<T> exchange;
-
- ExchangeImpl(Exchange<T> e) {
- // e == null means a http/2 pushed stream
- this.exchange = e;
- }
-
- final Exchange<T> getExchange() {
- return exchange;
- }
-
-
- /**
- * Returns the {@link HttpConnection} instance to which this exchange is
- * assigned.
- */
- abstract HttpConnection connection();
-
- /**
- * Initiates a new exchange and assigns it to a connection if one exists
- * already. connection usually null.
- */
- static <U> CompletableFuture<? extends ExchangeImpl<U>>
- get(Exchange<U> exchange, HttpConnection connection)
- {
- if (exchange.version() == HTTP_1_1) {
- DEBUG_LOGGER.log(Level.DEBUG, "get: HTTP/1.1: new Http1Exchange");
- return createHttp1Exchange(exchange, connection);
- } else {
- Http2ClientImpl c2 = exchange.client().client2(); // TODO: improve
- HttpRequestImpl request = exchange.request();
- CompletableFuture<Http2Connection> c2f = c2.getConnectionFor(request);
- DEBUG_LOGGER.log(Level.DEBUG, "get: Trying to get HTTP/2 connection");
- return c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection))
- .thenCompose(Function.identity());
- }
- }
-
- private static <U> CompletableFuture<? extends ExchangeImpl<U>>
- createExchangeImpl(Http2Connection c,
- Throwable t,
- Exchange<U> exchange,
- HttpConnection connection)
- {
- DEBUG_LOGGER.log(Level.DEBUG, "handling HTTP/2 connection creation result");
- boolean secure = exchange.request().secure();
- if (t != null) {
- DEBUG_LOGGER.log(Level.DEBUG,
- "handling HTTP/2 connection creation failed: %s",
- (Object)t);
- t = Utils.getCompletionCause(t);
- if (t instanceof Http2Connection.ALPNException) {
- Http2Connection.ALPNException ee = (Http2Connection.ALPNException)t;
- AbstractAsyncSSLConnection as = ee.getConnection();
- DEBUG_LOGGER.log(Level.DEBUG, "downgrading to HTTP/1.1 with: %s", as);
- CompletableFuture<? extends ExchangeImpl<U>> ex =
- createHttp1Exchange(exchange, as);
- return ex;
- } else {
- DEBUG_LOGGER.log(Level.DEBUG, "HTTP/2 connection creation failed "
- + "with unexpected exception: %s", (Object)t);
- return MinimalFuture.failedFuture(t);
- }
- }
- if (secure && c== null) {
- DEBUG_LOGGER.log(Level.DEBUG, "downgrading to HTTP/1.1 ");
- CompletableFuture<? extends ExchangeImpl<U>> ex =
- createHttp1Exchange(exchange, null);
- return ex;
- }
- if (c == null) {
- // no existing connection. Send request with HTTP 1 and then
- // upgrade if successful
- DEBUG_LOGGER.log(Level.DEBUG, "new Http1Exchange, try to upgrade");
- return createHttp1Exchange(exchange, connection)
- .thenApply((e) -> {
- exchange.h2Upgrade();
- return e;
- });
- } else {
- DEBUG_LOGGER.log(Level.DEBUG, "creating HTTP/2 streams");
- Stream<U> s = c.createStream(exchange);
- CompletableFuture<? extends ExchangeImpl<U>> ex = MinimalFuture.completedFuture(s);
- return ex;
- }
- }
-
- private static <T> CompletableFuture<Http1Exchange<T>>
- createHttp1Exchange(Exchange<T> ex, HttpConnection as)
- {
- try {
- return MinimalFuture.completedFuture(new Http1Exchange<>(ex, as));
- } catch (Throwable e) {
- return MinimalFuture.failedFuture(e);
- }
- }
-
- /* The following methods have separate HTTP/1.1 and HTTP/2 implementations */
-
- abstract CompletableFuture<ExchangeImpl<T>> sendHeadersAsync();
-
- /** Sends a request body, after request headers have been sent. */
- abstract CompletableFuture<ExchangeImpl<T>> sendBodyAsync();
-
- abstract CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
- boolean returnConnectionToPool,
- Executor executor);
-
- /**
- * Ignore/consume the body.
- */
- abstract CompletableFuture<Void> ignoreBody();
-
- /** Gets the response headers. Completes before body is read. */
- abstract CompletableFuture<Response> getResponseAsync(Executor executor);
-
-
- /** Cancels a request. Not currently exposed through API. */
- abstract void cancel();
-
- /**
- * Cancels a request with a cause. Not currently exposed through API.
- */
- abstract void cancel(IOException cause);
-
- /**
- * Called when the exchange is released, so that cleanup actions may be
- * performed - such as deregistering callbacks.
- * Typically released is called during upgrade, when an HTTP/2 stream
- * takes over from an Http1Exchange, or when a new exchange is created
- * during a multi exchange before the final response body was received.
- */
- abstract void released();
-
- /**
- * Called when the exchange is completed, so that cleanup actions may be
- * performed - such as deregistering callbacks.
- * Typically, completed is called at the end of the exchange, when the
- * final response body has been received (or an error has caused the
- * completion of the exchange).
- */
- abstract void completed();
-
- /**
- * Returns true if this exchange was canceled.
- * @return true if this exchange was canceled.
- */
- abstract boolean isCanceled();
-
- /**
- * Returns the cause for which this exchange was canceled, if available.
- * @return the cause for which this exchange was canceled, if available.
- */
- abstract Throwable getCancelCause();
-}
--- a/src/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<Class<? extends HeaderFilter>> filterClasses = new LinkedList<>();
-
- public void addFilter(Class<? extends HeaderFilter> type) {
- filterClasses.add(type);
- }
-
- List<HeaderFilter> getFilterChain() {
- List<HeaderFilter> l = new LinkedList<>();
- for (Class<? extends HeaderFilter> clazz : filterClasses) {
- try {
- // Requires a public no arg constructor.
- HeaderFilter headerFilter = clazz.getConstructor().newInstance();
- l.add(headerFilter);
- } catch (ReflectiveOperationException e) {
- throw new InternalError(e);
- }
- }
- return l;
- }
-}
--- a/src/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;
-}
--- 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<String> {
- int index;
- boolean returnsValue; // or key
-
- ParserIterator (boolean returnValue) {
- returnsValue = returnValue;
- }
- @Override
- public boolean hasNext () {
- return index<nkeys;
- }
- @Override
- public String next () {
- if (index >= nkeys) {
- throw new NoSuchElementException();
- }
- return tab[index++][returnsValue?1:0];
- }
- }
-
- public Iterator<String> keys () {
- return new ParserIterator (false);
- }
-
-// public Iterator<String> values () {
-// return new ParserIterator (true);
-// }
-
- @Override
- public String toString () {
- Iterator<String> k = keys();
- StringBuilder sb = new StringBuilder();
- sb.append("{size=").append(asize).append(" nkeys=").append(nkeys)
- .append(' ');
- for (int i=0; k.hasNext(); i++) {
- String key = k.next();
- String val = findValue (i);
- if (val != null && "".equals (val)) {
- val = null;
- }
- sb.append(" {").append(key).append(val == null ? "" : "," + val)
- .append('}');
- if (k.hasNext()) {
- sb.append (',');
- }
- }
- sb.append (" }");
- return sb.toString();
- }
-
-// public int findInt(String k, int Default) {
-// try {
-// return Integer.parseInt(findValue(k, String.valueOf(Default)));
-// } catch (Throwable t) {
-// return Default;
-// }
-// }
-}
--- a/src/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<ByteBuffer> queue
- = new ConcurrentLinkedDeque<>();
- private final SequentialScheduler scheduler =
- SequentialScheduler.synchronizedScheduler(this::flush);
- private final Executor executor;
- private final Http1TubeSubscriber subscriber = new Http1TubeSubscriber();
- private final AtomicReference<Http1AsyncDelegate> pendingDelegateRef;
- private final AtomicLong received = new AtomicLong();
- final AtomicBoolean canRequestMore = new AtomicBoolean();
-
- private volatile Throwable error;
- private volatile Http1AsyncDelegate delegate;
- // This reference is only used to prevent early GC of the exchange.
- private volatile Http1Exchange<?> owner;
- // Only used for checking whether we run on the selector manager thread.
- private final HttpClientImpl client;
- private boolean retry;
-
- public Http1AsyncReceiver(Executor executor, Http1Exchange<?> owner) {
- this.pendingDelegateRef = new AtomicReference<>();
- this.executor = executor;
- this.owner = owner;
- this.client = owner.client;
- }
-
- // This is the main loop called by the SequentialScheduler.
- // It attempts to empty the queue until the scheduler is stopped,
- // or the delegate is unregistered, or the delegate is unable to
- // process the data (because it's not ready or already done), which
- // it signals by returning 'true';
- private void flush() {
- ByteBuffer buf;
- try {
- assert !client.isSelectorThread() :
- "Http1AsyncReceiver::flush should not run in the selector: "
- + Thread.currentThread().getName();
-
- // First check whether we have a pending delegate that has
- // just subscribed, and if so, create a Subscription for it
- // and call onSubscribe.
- handlePendingDelegate();
-
- // Then start emptying the queue, if possible.
- while ((buf = queue.peek()) != null) {
- Http1AsyncDelegate delegate = this.delegate;
- debug.log(Level.DEBUG, "Got %s bytes for delegate %s",
- buf.remaining(), delegate);
- if (!hasDemand(delegate)) {
- // The scheduler will be invoked again later when the demand
- // becomes positive.
- return;
- }
-
- assert delegate != null;
- debug.log(Level.DEBUG, "Forwarding %s bytes to delegate %s",
- buf.remaining(), delegate);
- // The delegate has demand: feed it the next buffer.
- if (!delegate.tryAsyncReceive(buf)) {
- final long remaining = buf.remaining();
- debug.log(Level.DEBUG, () -> {
- // If the scheduler is stopped, the queue may already
- // be empty and the reference may already be released.
- String remstr = scheduler.isStopped() ? "" :
- " remaining in ref: "
- + remaining;
- remstr = remstr
- + " total remaining: " + remaining();
- return "Delegate done: " + remaining;
- });
- canRequestMore.set(false);
- // The last buffer parsed may have remaining unparsed bytes.
- // Don't take it out of the queue.
- return; // done.
- }
-
- // removed parsed buffer from queue, and continue with next
- // if available
- ByteBuffer parsed = queue.remove();
- canRequestMore.set(queue.isEmpty());
- assert parsed == buf;
- }
-
- // queue is empty: let's see if we should request more
- checkRequestMore();
-
- } catch (Throwable t) {
- Throwable x = error;
- if (x == null) error = t; // will be handled in the finally block
- debug.log(Level.DEBUG, "Unexpected error caught in flush()", t);
- } finally {
- // Handles any pending error.
- // The most recently subscribed delegate will get the error.
- checkForErrors();
- }
- }
-
- /**
- * Must be called from within the scheduler main loop.
- * Handles any pending errors by calling delegate.onReadError().
- * If the error can be forwarded to the delegate, stops the scheduler.
- */
- private void checkForErrors() {
- // Handles any pending error.
- // The most recently subscribed delegate will get the error.
- // If the delegate is null, the error will be handled by the next
- // delegate that subscribes.
- // If the queue is not empty, wait until it it is empty before
- // handling the error.
- Http1AsyncDelegate delegate = pendingDelegateRef.get();
- if (delegate == null) delegate = this.delegate;
- Throwable x = error;
- if (delegate != null && x != null && queue.isEmpty()) {
- // forward error only after emptying the queue.
- final Object captured = delegate;
- debug.log(Level.DEBUG, () -> "flushing " + x
- + "\n\t delegate: " + captured
- + "\t\t queue.isEmpty: " + queue.isEmpty());
- scheduler.stop();
- delegate.onReadError(x);
- }
- }
-
- /**
- * Must be called from within the scheduler main loop.
- * Figure out whether more data should be requested from the
- * Http1TubeSubscriber.
- */
- private void checkRequestMore() {
- Http1AsyncDelegate delegate = this.delegate;
- boolean more = this.canRequestMore.get();
- boolean hasDemand = hasDemand(delegate);
- debug.log(Level.DEBUG, () -> "checkRequestMore: "
- + "canRequestMore=" + more + ", hasDemand=" + hasDemand
- + (delegate == null ? ", delegate=null" : ""));
- if (hasDemand) {
- subscriber.requestMore();
- }
- }
-
- /**
- * Must be called from within the scheduler main loop.
- * Return true if the delegate is not null and has some demand.
- * @param delegate The Http1AsyncDelegate delegate
- * @return true if the delegate is not null and has some demand
- */
- private boolean hasDemand(Http1AsyncDelegate delegate) {
- if (delegate == null) return false;
- AbstractSubscription subscription = delegate.subscription();
- long demand = subscription.demand().get();
- debug.log(Level.DEBUG, "downstream subscription demand is %s", demand);
- return demand > 0;
- }
-
- /**
- * Must be called from within the scheduler main loop.
- * Handles pending delegate subscription.
- * Return true if there was some pending delegate subscription and a new
- * delegate was subscribed, false otherwise.
- *
- * @return true if there was some pending delegate subscription and a new
- * delegate was subscribed, false otherwise.
- */
- private boolean handlePendingDelegate() {
- Http1AsyncDelegate pending = pendingDelegateRef.get();
- if (pending != null && pendingDelegateRef.compareAndSet(pending, null)) {
- Http1AsyncDelegate delegate = this.delegate;
- if (delegate != null) unsubscribe(delegate);
- Runnable cancel = () -> {
- debug.log(Level.DEBUG, "Downstream subscription cancelled by %s", pending);
- // The connection should be closed, as some data may
- // be left over in the stream.
- try {
- setRetryOnError(false);
- onReadError(new IOException("subscription cancelled"));
- unsubscribe(pending);
- } finally {
- Http1Exchange<?> exchg = owner;
- stop();
- if (exchg != null) exchg.connection().close();
- }
- };
- // The subscription created by a delegate is only loosely
- // coupled with the upstream subscription. This is partly because
- // the header/body parser work with a flow of ByteBuffer, whereas
- // we have a flow List<ByteBuffer> upstream.
- Http1AsyncDelegateSubscription subscription =
- new Http1AsyncDelegateSubscription(scheduler, cancel);
- pending.onSubscribe(subscription);
- this.delegate = delegate = pending;
- final Object captured = delegate;
- debug.log(Level.DEBUG, () -> "delegate is now " + captured
- + ", demand=" + subscription.demand().get()
- + ", canRequestMore=" + canRequestMore.get()
- + ", queue.isEmpty=" + queue.isEmpty());
- return true;
- }
- return false;
- }
-
- synchronized void setRetryOnError(boolean retry) {
- this.retry = retry;
- }
-
- void clear() {
- debug.log(Level.DEBUG, "cleared");
- this.pendingDelegateRef.set(null);
- this.delegate = null;
- this.owner = null;
- }
-
- void subscribe(Http1AsyncDelegate delegate) {
- synchronized(this) {
- pendingDelegateRef.set(delegate);
- }
- if (queue.isEmpty()) {
- canRequestMore.set(true);
- }
- debug.log(Level.DEBUG, () ->
- "Subscribed pending " + delegate + " queue.isEmpty: "
- + queue.isEmpty());
- // Everything may have been received already. Make sure
- // we parse it.
- if (client.isSelectorThread()) {
- scheduler.runOrSchedule(executor);
- } else {
- scheduler.runOrSchedule();
- }
- }
-
- // Used for debugging only!
- long remaining() {
- return Utils.remaining(queue.toArray(Utils.EMPTY_BB_ARRAY));
- }
-
- void unsubscribe(Http1AsyncDelegate delegate) {
- synchronized(this) {
- if (this.delegate == delegate) {
- debug.log(Level.DEBUG, "Unsubscribed %s", delegate);
- this.delegate = null;
- }
- }
- }
-
- // Callback: Consumer of ByteBuffer
- private void asyncReceive(ByteBuffer buf) {
- debug.log(Level.DEBUG, "Putting %s bytes into the queue", buf.remaining());
- received.addAndGet(buf.remaining());
- queue.offer(buf);
-
- // This callback is called from within the selector thread.
- // Use an executor here to avoid doing the heavy lifting in the
- // selector.
- scheduler.runOrSchedule(executor);
- }
-
- // Callback: Consumer of Throwable
- void onReadError(Throwable ex) {
- Http1AsyncDelegate delegate;
- Throwable recorded;
- debug.log(Level.DEBUG, "onError: %s", (Object) ex);
- synchronized (this) {
- delegate = this.delegate;
- recorded = error;
- if (recorded == null) {
- // retry is set to true by HttpExchange when the connection is
- // already connected, which means it's been retrieved from
- // the pool.
- if (retry && (ex instanceof IOException)) {
- // could be either EOFException, or
- // IOException("connection reset by peer), or
- // SSLHandshakeException resulting from the server having
- // closed the SSL session.
- if (received.get() == 0) {
- // If we receive such an exception before having
- // received any byte, then in this case, we will
- // throw ConnectionExpiredException
- // to try & force a retry of the request.
- retry = false;
- ex = new ConnectionExpiredException(
- "subscription is finished", ex);
- }
- }
- error = ex;
- }
- final Throwable t = (recorded == null ? ex : recorded);
- debug.log(Level.DEBUG, () -> "recorded " + t
- + "\n\t delegate: " + delegate
- + "\t\t queue.isEmpty: " + queue.isEmpty(), ex);
- }
- if (queue.isEmpty() || pendingDelegateRef.get() != null) {
- // This callback is called from within the selector thread.
- // Use an executor here to avoid doing the heavy lifting in the
- // selector.
- scheduler.runOrSchedule(executor);
- }
- }
-
- void stop() {
- debug.log(Level.DEBUG, "stopping");
- scheduler.stop();
- delegate = null;
- owner = null;
- }
-
- /**
- * Returns the TubeSubscriber for reading from the connection flow.
- * @return the TubeSubscriber for reading from the connection flow.
- */
- TubeSubscriber subscriber() {
- return subscriber;
- }
-
- /**
- * A simple tube subscriber for reading from the connection flow.
- */
- final class Http1TubeSubscriber implements TubeSubscriber {
- volatile Flow.Subscription subscription;
- volatile boolean completed;
- volatile boolean dropped;
-
- public void onSubscribe(Flow.Subscription subscription) {
- // supports being called multiple time.
- // doesn't cancel the previous subscription, since that is
- // most probably the same as the new subscription.
- assert this.subscription == null || dropped == false;
- this.subscription = subscription;
- dropped = false;
- canRequestMore.set(true);
- if (delegate != null) {
- scheduler.runOrSchedule(executor);
- }
- }
-
- void requestMore() {
- Flow.Subscription s = subscription;
- if (s == null) return;
- if (canRequestMore.compareAndSet(true, false)) {
- if (!completed && !dropped) {
- debug.log(Level.DEBUG,
- "Http1TubeSubscriber: requesting one more from upstream");
- s.request(1);
- return;
- }
- }
- debug.log(Level.DEBUG, "Http1TubeSubscriber: no need to request more");
- }
-
- @Override
- public void onNext(List<ByteBuffer> item) {
- canRequestMore.set(item.isEmpty());
- for (ByteBuffer buffer : item) {
- asyncReceive(buffer);
- }
- }
-
- @Override
- public void onError(Throwable throwable) {
- onReadError(throwable);
- completed = true;
- }
-
- @Override
- public void onComplete() {
- onReadError(new EOFException("EOF reached while reading"));
- completed = true;
- }
-
- public void dropSubscription() {
- debug.log(Level.DEBUG, "Http1TubeSubscriber: dropSubscription");
- // we could probably set subscription to null here...
- // then we might not need the 'dropped' boolean?
- dropped = true;
- }
-
- }
-
- // Drains the content of the queue into a single ByteBuffer.
- // The scheduler must be permanently stopped before calling drain().
- ByteBuffer drain(ByteBuffer initial) {
- // Revisit: need to clean that up.
- //
- ByteBuffer b = initial = (initial == null ? Utils.EMPTY_BYTEBUFFER : initial);
- assert scheduler.isStopped();
-
- if (queue.isEmpty()) return b;
-
- // sanity check: we shouldn't have queued the same
- // buffer twice.
- ByteBuffer[] qbb = queue.toArray(new ByteBuffer[queue.size()]);
- assert java.util.stream.Stream.of(qbb)
- .collect(Collectors.toSet())
- .size() == qbb.length : debugQBB(qbb);
-
- // compute the number of bytes in the queue, the number of bytes
- // in the initial buffer
- // TODO: will need revisiting - as it is not guaranteed that all
- // data will fit in single BB!
- int size = Utils.remaining(qbb, Integer.MAX_VALUE);
- int remaining = b.remaining();
- int free = b.capacity() - b.position() - remaining;
- debug.log(Level.DEBUG,
- "Flushing %s bytes from queue into initial buffer (remaining=%s, free=%s)",
- size, remaining, free);
-
- // check whether the initial buffer has enough space
- if (size > free) {
- debug.log(Level.DEBUG,
- "Allocating new buffer for initial: %s", (size + remaining));
- // allocates a new buffer and copy initial to it
- b = ByteBuffer.allocate(size + remaining);
- Utils.copy(initial, b);
- assert b.position() == remaining;
- b.flip();
- assert b.position() == 0;
- assert b.limit() == remaining;
- assert b.remaining() == remaining;
- }
-
- // store position and limit
- int pos = b.position();
- int limit = b.limit();
- assert limit - pos == remaining;
- assert b.capacity() >= remaining + size
- : "capacity: " + b.capacity()
- + ", remaining: " + b.remaining()
- + ", size: " + size;
-
- // prepare to copy the content of the queue
- b.position(limit);
- b.limit(pos + remaining + size);
- assert b.remaining() >= size :
- "remaining: " + b.remaining() + ", size: " + size;
-
- // copy the content of the queue
- int count = 0;
- for (int i=0; i<qbb.length; i++) {
- ByteBuffer b2 = qbb[i];
- int r = b2.remaining();
- assert b.remaining() >= r : "need at least " + r + " only "
- + b.remaining() + " available";
- int copied = Utils.copy(b2, b);
- assert copied == r : "copied="+copied+" available="+r;
- assert b2.remaining() == 0;
- count += copied;
- }
- assert count == size;
- assert b.position() == pos + remaining + size :
- "b.position="+b.position()+" != "+pos+"+"+remaining+"+"+size;
-
- // reset limit and position
- b.limit(limit+size);
- b.position(pos);
-
- // we can clear the refs
- queue.clear();
- final ByteBuffer bb = b;
- debug.log(Level.DEBUG, () -> "Initial buffer now has " + bb.remaining()
- + " pos=" + bb.position() + " limit=" + bb.limit());
-
- return b;
- }
-
- private String debugQBB(ByteBuffer[] qbb) {
- StringBuilder msg = new StringBuilder();
- List<ByteBuffer> lbb = Arrays.asList(qbb);
- Set<ByteBuffer> sbb = new HashSet<>(Arrays.asList(qbb));
-
- int uniquebb = sbb.size();
- msg.append("qbb: ").append(lbb.size())
- .append(" (unique: ").append(uniquebb).append("), ")
- .append("duplicates: ");
- String sep = "";
- for (ByteBuffer b : lbb) {
- if (!sbb.remove(b)) {
- msg.append(sep)
- .append(String.valueOf(b))
- .append("[remaining=")
- .append(b.remaining())
- .append(", position=")
- .append(b.position())
- .append(", capacity=")
- .append(b.capacity())
- .append("]");
- sep = ", ";
- }
- }
- return msg.toString();
- }
-
- volatile String dbgTag;
- String dbgString() {
- String tag = dbgTag;
- if (tag == null) {
- String flowTag = null;
- Http1Exchange<?> exchg = owner;
- Object flow = (exchg != null)
- ? exchg.connection().getConnectionFlow()
- : null;
- flowTag = tag = flow == null ? null: (String.valueOf(flow));
- if (flowTag != null) {
- dbgTag = tag = flowTag + " Http1AsyncReceiver";
- } else {
- tag = "Http1AsyncReceiver";
- }
- }
- return tag;
- }
-}
--- a/src/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<T> extends ExchangeImpl<T> {
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
- private static final System.Logger DEBUG_LOGGER =
- Utils.getDebugLogger("Http1Exchange"::toString, DEBUG);
-
- final HttpRequestImpl request; // main request
- final Http1Request requestAction;
- private volatile Http1Response<T> response;
- final HttpConnection connection;
- final HttpClientImpl client;
- final Executor executor;
- private final Http1AsyncReceiver asyncReceiver;
-
- /** Records a possible cancellation raised before any operation
- * has been initiated, or an error received while sending the request. */
- private Throwable failed;
- private final List<CompletableFuture<?>> operations; // used for cancel
-
- /** Must be held when operating on any internal state or data. */
- private final Object lock = new Object();
-
- /** Holds the outgoing data, either the headers or a request body part. Or
- * an error from the request body publisher. At most there can be ~2 pieces
- * of outgoing data ( onComplete|onError can be invoked without demand ).*/
- final ConcurrentLinkedDeque<DataPair> outgoing = new ConcurrentLinkedDeque<>();
-
- /** The write publisher, responsible for writing the complete request ( both
- * headers and body ( if any ). */
- private final Http1Publisher writePublisher = new Http1Publisher();
-
- /** Completed when the header have been published, or there is an error */
- private final CompletableFuture<ExchangeImpl<T>> headersSentCF = new MinimalFuture<>();
- /** Completed when the body has been published, or there is an error */
- private final CompletableFuture<ExchangeImpl<T>> bodySentCF = new MinimalFuture<>();
-
- /** The subscriber to the request's body published. Maybe null. */
- private volatile Http1BodySubscriber bodySubscriber;
-
- enum State { INITIAL,
- HEADERS,
- BODY,
- ERROR, // terminal state
- COMPLETING,
- COMPLETED } // terminal state
-
- private State state = State.INITIAL;
-
- /** A carrier for either data or an error. Used to carry data, and communicate
- * errors from the request ( both headers and body ) to the exchange. */
- static class DataPair {
- Throwable throwable;
- List<ByteBuffer> data;
- DataPair(List<ByteBuffer> data, Throwable throwable){
- this.data = data;
- this.throwable = throwable;
- }
- @Override
- public String toString() {
- return "DataPair [data=" + data + ", throwable=" + throwable + "]";
- }
- }
-
- /** An abstract supertype for HTTP/1.1 body subscribers. There are two
- * concrete implementations: {@link Http1Request.StreamSubscriber}, and
- * {@link Http1Request.FixedContentSubscriber}, for receiving chunked and
- * fixed length bodies, respectively. */
- static abstract class Http1BodySubscriber implements Flow.Subscriber<ByteBuffer> {
- protected volatile Flow.Subscription subscription;
- protected volatile boolean complete;
-
- /** Final sentinel in the stream of request body. */
- static final List<ByteBuffer> COMPLETED = List.of(ByteBuffer.allocate(0));
-
- void request(long n) {
- DEBUG_LOGGER.log(Level.DEBUG, () ->
- "Http1BodySubscriber requesting " + n + ", from " + subscription);
- subscription.request(n);
- }
-
- static Http1BodySubscriber completeSubscriber() {
- return new Http1BodySubscriber() {
- @Override public void onSubscribe(Flow.Subscription subscription) { error(); }
- @Override public void onNext(ByteBuffer item) { error(); }
- @Override public void onError(Throwable throwable) { error(); }
- @Override public void onComplete() { error(); }
- private void error() {
- throw new InternalError("should not reach here");
- }
- };
- }
- }
-
- @Override
- public String toString() {
- return "HTTP/1.1 " + request.toString();
- }
-
- HttpRequestImpl request() {
- return request;
- }
-
- Http1Exchange(Exchange<T> exchange, HttpConnection connection)
- throws IOException
- {
- super(exchange);
- this.request = exchange.request();
- this.client = exchange.client();
- this.executor = exchange.executor();
- this.operations = new LinkedList<>();
- operations.add(headersSentCF);
- operations.add(bodySentCF);
- if (connection != null) {
- this.connection = connection;
- } else {
- InetSocketAddress addr = request.getAddress();
- this.connection = HttpConnection.getConnection(addr, client, request, HTTP_1_1);
- }
- this.requestAction = new Http1Request(request, this);
- this.asyncReceiver = new Http1AsyncReceiver(executor, this);
- asyncReceiver.subscribe(new InitialErrorReceiver());
- }
-
- /** An initial receiver that handles no data, but cancels the request if
- * it receives an error. Will be replaced when reading response body. */
- final class InitialErrorReceiver implements Http1AsyncReceiver.Http1AsyncDelegate {
- volatile AbstractSubscription s;
- @Override
- public boolean tryAsyncReceive(ByteBuffer ref) {
- return false; // no data has been processed, leave it in the queue
- }
-
- @Override
- public void onReadError(Throwable ex) {
- cancelImpl(ex);
- }
-
- @Override
- public void onSubscribe(AbstractSubscription s) {
- this.s = s;
- }
-
- public AbstractSubscription subscription() {
- return s;
- }
- }
-
- @Override
- HttpConnection connection() {
- return connection;
- }
-
- private void connectFlows(HttpConnection connection) {
- FlowTube tube = connection.getConnectionFlow();
- debug.log(Level.DEBUG, "%s connecting flows", tube);
-
- // Connect the flow to our Http1TubeSubscriber:
- // asyncReceiver.subscriber().
- tube.connectFlows(writePublisher,
- asyncReceiver.subscriber());
- }
-
- @Override
- CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
- // create the response before sending the request headers, so that
- // the response can set the appropriate receivers.
- debug.log(Level.DEBUG, "Sending headers only");
- if (response == null) {
- response = new Http1Response<>(connection, this, asyncReceiver);
- }
-
- debug.log(Level.DEBUG, "response created in advance");
- // If the first attempt to read something triggers EOF, or
- // IOException("channel reset by peer"), we're going to retry.
- // Instruct the asyncReceiver to throw ConnectionExpiredException
- // to force a retry.
- asyncReceiver.setRetryOnError(true);
-
- CompletableFuture<Void> connectCF;
- if (!connection.connected()) {
- debug.log(Level.DEBUG, "initiating connect async");
- connectCF = connection.connectAsync();
- synchronized (lock) {
- operations.add(connectCF);
- }
- } else {
- connectCF = new MinimalFuture<>();
- connectCF.complete(null);
- }
-
- return connectCF
- .thenCompose(unused -> {
- CompletableFuture<Void> cf = new MinimalFuture<>();
- try {
- connectFlows(connection);
-
- debug.log(Level.DEBUG, "requestAction.headers");
- List<ByteBuffer> data = requestAction.headers();
- synchronized (lock) {
- state = State.HEADERS;
- }
- debug.log(Level.DEBUG, "setting outgoing with headers");
- assert outgoing.isEmpty() : "Unexpected outgoing:" + outgoing;
- appendToOutgoing(data);
- cf.complete(null);
- return cf;
- } catch (Throwable t) {
- debug.log(Level.DEBUG, "Failed to send headers: %s", t);
- connection.close();
- cf.completeExceptionally(t);
- return cf;
- } })
- .thenCompose(unused -> headersSentCF);
- }
-
- @Override
- CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
- assert headersSentCF.isDone();
- try {
- bodySubscriber = requestAction.continueRequest();
- if (bodySubscriber == null) {
- bodySubscriber = Http1BodySubscriber.completeSubscriber();
- appendToOutgoing(Http1BodySubscriber.COMPLETED);
- } else {
- bodySubscriber.request(1); // start
- }
- } catch (Throwable t) {
- connection.close();
- bodySentCF.completeExceptionally(t);
- }
- return bodySentCF;
- }
-
- @Override
- CompletableFuture<Response> getResponseAsync(Executor executor) {
- CompletableFuture<Response> cf = response.readHeadersAsync(executor);
- Throwable cause;
- synchronized (lock) {
- operations.add(cf);
- cause = failed;
- failed = null;
- }
-
- if (cause != null) {
- Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms]"
- + "\n\tCompleting exceptionally with {2}\n",
- request.uri(),
- request.timeout().isPresent() ?
- // calling duration.toMillis() can throw an exception.
- // this is just debugging, we don't care if it overflows.
- (request.timeout().get().getSeconds() * 1000
- + request.timeout().get().getNano() / 1000000) : -1,
- cause);
- boolean acknowledged = cf.completeExceptionally(cause);
- debug.log(Level.DEBUG,
- () -> acknowledged
- ? ("completed response with " + cause)
- : ("response already completed, ignoring " + cause));
- }
- return cf;
- }
-
- @Override
- CompletableFuture<T> readBodyAsync(BodyHandler<T> handler,
- boolean returnConnectionToPool,
- Executor executor)
- {
- BodySubscriber<T> bs = handler.apply(response.responseCode(),
- response.responseHeaders());
- CompletableFuture<T> bodyCF = response.readBody(bs,
- returnConnectionToPool,
- executor);
- return bodyCF;
- }
-
- @Override
- CompletableFuture<Void> ignoreBody() {
- return response.ignoreBody(executor);
- }
-
- ByteBuffer drainLeftOverBytes() {
- synchronized (lock) {
- asyncReceiver.stop();
- return asyncReceiver.drain(Utils.EMPTY_BYTEBUFFER);
- }
- }
-
- void released() {
- Http1Response<T> resp = this.response;
- if (resp != null) resp.completed();
- asyncReceiver.clear();
- }
-
- void completed() {
- Http1Response<T> resp = this.response;
- if (resp != null) resp.completed();
- }
-
- /**
- * Cancel checks to see if request and responseAsync finished already.
- * If not it closes the connection and completes all pending operations
- */
- @Override
- void cancel() {
- cancelImpl(new IOException("Request cancelled"));
- }
-
- /**
- * Cancel checks to see if request and responseAsync finished already.
- * If not it closes the connection and completes all pending operations
- */
- @Override
- void cancel(IOException cause) {
- cancelImpl(cause);
- }
-
- private void cancelImpl(Throwable cause) {
- LinkedList<CompletableFuture<?>> toComplete = null;
- int count = 0;
- synchronized (lock) {
- if (failed == null)
- failed = cause;
- if (requestAction != null && requestAction.finished()
- && response != null && response.finished()) {
- return;
- }
- connection.close(); // TODO: ensure non-blocking if holding the lock
- writePublisher.writeScheduler.stop();
- if (operations.isEmpty()) {
- Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms] no pending operation."
- + "\n\tCan''t cancel yet with {2}",
- request.uri(),
- request.timeout().isPresent() ?
- // calling duration.toMillis() can throw an exception.
- // this is just debugging, we don't care if it overflows.
- (request.timeout().get().getSeconds() * 1000
- + request.timeout().get().getNano() / 1000000) : -1,
- cause);
- } else {
- for (CompletableFuture<?> cf : operations) {
- if (!cf.isDone()) {
- if (toComplete == null) toComplete = new LinkedList<>();
- toComplete.add(cf);
- count++;
- }
- }
- operations.clear();
- }
- }
- Log.logError("Http1Exchange.cancel: count=" + count);
- if (toComplete != null) {
- // We might be in the selector thread in case of timeout, when
- // the SelectorManager calls purgeTimeoutsAndReturnNextDeadline()
- // There may or may not be other places that reach here
- // from the SelectorManager thread, so just make sure we
- // don't complete any CF from within the selector manager
- // thread.
- Executor exec = client.isSelectorThread()
- ? executor
- : this::runInline;
- while (!toComplete.isEmpty()) {
- CompletableFuture<?> cf = toComplete.poll();
- exec.execute(() -> {
- if (cf.completeExceptionally(cause)) {
- debug.log(Level.DEBUG, "completed cf with %s",
- (Object) cause);
- }
- });
- }
- }
- }
-
- private void runInline(Runnable run) {
- assert !client.isSelectorThread();
- run.run();
- }
-
- /** Returns true if this exchange was canceled. */
- boolean isCanceled() {
- synchronized (lock) {
- return failed != null;
- }
- }
-
- /** Returns the cause for which this exchange was canceled, if available. */
- Throwable getCancelCause() {
- synchronized (lock) {
- return failed;
- }
- }
-
- /** Convenience for {@link #appendToOutgoing(DataPair)}, with just a Throwable. */
- void appendToOutgoing(Throwable throwable) {
- appendToOutgoing(new DataPair(null, throwable));
- }
-
- /** Convenience for {@link #appendToOutgoing(DataPair)}, with just data. */
- void appendToOutgoing(List<ByteBuffer> item) {
- appendToOutgoing(new DataPair(item, null));
- }
-
- private void appendToOutgoing(DataPair dp) {
- debug.log(Level.DEBUG, "appending to outgoing " + dp);
- outgoing.add(dp);
- writePublisher.writeScheduler.runOrSchedule();
- }
-
- /** Tells whether, or not, there is any outgoing data that can be published,
- * or if there is an error. */
- private boolean hasOutgoing() {
- return !outgoing.isEmpty();
- }
-
- // Invoked only by the publisher
- // ALL tasks should execute off the Selector-Manager thread
- /** Returns the next portion of the HTTP request, or the error. */
- private DataPair getOutgoing() {
- final Executor exec = client.theExecutor();
- final DataPair dp = outgoing.pollFirst();
-
- if (dp == null) // publisher has not published anything yet
- return null;
-
- synchronized (lock) {
- if (dp.throwable != null) {
- state = State.ERROR;
- exec.execute(() -> {
- connection.close();
- headersSentCF.completeExceptionally(dp.throwable);
- bodySentCF.completeExceptionally(dp.throwable);
- });
- return dp;
- }
-
- switch (state) {
- case HEADERS:
- state = State.BODY;
- // completeAsync, since dependent tasks should run in another thread
- debug.log(Level.DEBUG, "initiating completion of headersSentCF");
- headersSentCF.completeAsync(() -> this, exec);
- break;
- case BODY:
- if (dp.data == Http1BodySubscriber.COMPLETED) {
- state = State.COMPLETING;
- debug.log(Level.DEBUG, "initiating completion of bodySentCF");
- bodySentCF.completeAsync(() -> this, exec);
- } else {
- debug.log(Level.DEBUG, "requesting more body from the subscriber");
- exec.execute(() -> bodySubscriber.request(1));
- }
- break;
- case INITIAL:
- case ERROR:
- case COMPLETING:
- case COMPLETED:
- default:
- assert false : "Unexpected state:" + state;
- }
-
- return dp;
- }
- }
-
- /** A Publisher of HTTP/1.1 headers and request body. */
- final class Http1Publisher implements FlowTube.TubePublisher {
-
- final System.Logger debug = Utils.getDebugLogger(this::dbgString);
- volatile Flow.Subscriber<? super List<ByteBuffer>> subscriber;
- volatile boolean cancelled;
- final Http1WriteSubscription subscription = new Http1WriteSubscription();
- final Demand demand = new Demand();
- final SequentialScheduler writeScheduler =
- SequentialScheduler.synchronizedScheduler(new WriteTask());
-
- @Override
- public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
- assert state == State.INITIAL;
- Objects.requireNonNull(s);
- assert subscriber == null;
-
- subscriber = s;
- debug.log(Level.DEBUG, "got subscriber: %s", s);
- s.onSubscribe(subscription);
- }
-
- volatile String dbgTag;
- String dbgString() {
- String tag = dbgTag;
- Object flow = connection.getConnectionFlow();
- if (tag == null && flow != null) {
- dbgTag = tag = "Http1Publisher(" + flow + ")";
- } else if (tag == null) {
- tag = "Http1Publisher(?)";
- }
- return tag;
- }
-
- final class WriteTask implements Runnable {
- @Override
- public void run() {
- assert state != State.COMPLETED : "Unexpected state:" + state;
- debug.log(Level.DEBUG, "WriteTask");
- if (subscriber == null) {
- debug.log(Level.DEBUG, "no subscriber yet");
- return;
- }
- debug.log(Level.DEBUG, () -> "hasOutgoing = " + hasOutgoing());
- while (hasOutgoing() && demand.tryDecrement()) {
- DataPair dp = getOutgoing();
-
- if (dp.throwable != null) {
- debug.log(Level.DEBUG, "onError");
- // Do not call the subscriber's onError, it is not required.
- writeScheduler.stop();
- } else {
- List<ByteBuffer> data = dp.data;
- if (data == Http1BodySubscriber.COMPLETED) {
- synchronized (lock) {
- assert state == State.COMPLETING : "Unexpected state:" + state;
- state = State.COMPLETED;
- }
- debug.log(Level.DEBUG,
- "completed, stopping %s", writeScheduler);
- writeScheduler.stop();
- // Do nothing more. Just do not publish anything further.
- // The next Subscriber will eventually take over.
-
- } else {
- debug.log(Level.DEBUG, () ->
- "onNext with " + Utils.remaining(data) + " bytes");
- subscriber.onNext(data);
- }
- }
- }
- }
- }
-
- final class Http1WriteSubscription implements Flow.Subscription {
-
- @Override
- public void request(long n) {
- if (cancelled)
- return; //no-op
- demand.increase(n);
- debug.log(Level.DEBUG,
- "subscription request(%d), demand=%s", n, demand);
- writeScheduler.runOrSchedule(client.theExecutor());
- }
-
- @Override
- public void cancel() {
- debug.log(Level.DEBUG, "subscription cancelled");
- if (cancelled)
- return; //no-op
- cancelled = true;
- writeScheduler.stop();
- }
- }
- }
-
- String dbgString() {
- return "Http1Exchange";
- }
-}
--- a/src/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<String,List<String>> privateMap = new HashMap<>();
-
- enum State { STATUS_LINE,
- STATUS_LINE_FOUND_CR,
- STATUS_LINE_END,
- STATUS_LINE_END_CR,
- HEADER,
- HEADER_FOUND_CR,
- HEADER_FOUND_LF,
- HEADER_FOUND_CR_LF,
- HEADER_FOUND_CR_LF_CR,
- FINISHED }
-
- private State state = State.STATUS_LINE;
-
- /** Returns the status-line. */
- String statusLine() { return statusLine; }
-
- /** Returns the response code. */
- int responseCode() { return responseCode; }
-
- /** Returns the headers, possibly empty. */
- HttpHeaders headers() { assert state == State.FINISHED; return headers; }
-
- /**
- * Parses HTTP/1.X status-line and headers from the given bytes. Must be
- * called successive times, with additional data, until returns true.
- *
- * All given ByteBuffers will be consumed, until ( possibly ) the last one
- * ( when true is returned ), which may not be fully consumed.
- *
- * @param input the ( partial ) header data
- * @return true iff the end of the headers block has been reached
- */
- boolean parse(ByteBuffer input) throws ProtocolException {
- requireNonNull(input, "null input");
-
- while (input.hasRemaining() && state != State.FINISHED) {
- switch (state) {
- case STATUS_LINE:
- readResumeStatusLine(input);
- break;
- case STATUS_LINE_FOUND_CR:
- readStatusLineFeed(input);
- break;
- case STATUS_LINE_END:
- maybeStartHeaders(input);
- break;
- case STATUS_LINE_END_CR:
- maybeEndHeaders(input);
- break;
- case HEADER:
- readResumeHeader(input);
- break;
- // fallthrough
- case HEADER_FOUND_CR:
- case HEADER_FOUND_LF:
- resumeOrLF(input);
- break;
- case HEADER_FOUND_CR_LF:
- resumeOrSecondCR(input);
- break;
- case HEADER_FOUND_CR_LF_CR:
- resumeOrEndHeaders(input);
- break;
- default:
- throw new InternalError(
- "Unexpected state: " + String.valueOf(state));
- }
- }
-
- return state == State.FINISHED;
- }
-
- private void readResumeStatusLine(ByteBuffer input) {
- char c = 0;
- while (input.hasRemaining() && (c =(char)input.get()) != CR) {
- sb.append(c);
- }
-
- if (c == CR) {
- state = State.STATUS_LINE_FOUND_CR;
- }
- }
-
- private void readStatusLineFeed(ByteBuffer input) throws ProtocolException {
- char c = (char)input.get();
- if (c != LF) {
- throw protocolException("Bad trailing char, \"%s\", when parsing status-line, \"%s\"",
- c, sb.toString());
- }
-
- statusLine = sb.toString();
- sb = new StringBuilder();
- if (!statusLine.startsWith("HTTP/1.")) {
- throw protocolException("Invalid status line: \"%s\"", statusLine);
- }
- if (statusLine.length() < 12) {
- throw protocolException("Invalid status line: \"%s\"", statusLine);
- }
- responseCode = Integer.parseInt(statusLine.substring(9, 12));
-
- state = State.STATUS_LINE_END;
- }
-
- private void maybeStartHeaders(ByteBuffer input) {
- assert state == State.STATUS_LINE_END;
- assert sb.length() == 0;
- char c = (char)input.get();
- if (c == CR) {
- state = State.STATUS_LINE_END_CR;
- } else {
- sb.append(c);
- state = State.HEADER;
- }
- }
-
- private void maybeEndHeaders(ByteBuffer input) throws ProtocolException {
- assert state == State.STATUS_LINE_END_CR;
- assert sb.length() == 0;
- char c = (char)input.get();
- if (c == LF) {
- headers = ImmutableHeaders.of(privateMap);
- privateMap = null;
- state = State.FINISHED; // no headers
- } else {
- throw protocolException("Unexpected \"%s\", after status-line CR", c);
- }
- }
-
- private void readResumeHeader(ByteBuffer input) {
- assert state == State.HEADER;
- assert input.hasRemaining();
- while (input.hasRemaining()) {
- char c = (char)input.get();
- if (c == CR) {
- state = State.HEADER_FOUND_CR;
- break;
- } else if (c == LF) {
- state = State.HEADER_FOUND_LF;
- break;
- }
-
- if (c == HT)
- c = SP;
- sb.append(c);
- }
- }
-
- private void addHeaderFromString(String headerString) {
- assert sb.length() == 0;
- int idx = headerString.indexOf(':');
- if (idx == -1)
- return;
- String name = headerString.substring(0, idx).trim();
- if (name.isEmpty())
- return;
- String value = headerString.substring(idx + 1, headerString.length()).trim();
-
- privateMap.computeIfAbsent(name.toLowerCase(Locale.US),
- k -> new ArrayList<>()).add(value);
- }
-
- private void resumeOrLF(ByteBuffer input) {
- assert state == State.HEADER_FOUND_CR || state == State.HEADER_FOUND_LF;
- char c = (char)input.get();
- if (c == LF && state == State.HEADER_FOUND_CR) {
- // header value will be flushed by
- // resumeOrSecondCR if next line does not
- // begin by SP or HT
- state = State.HEADER_FOUND_CR_LF;
- } else if (c == SP || c == HT) {
- sb.append(SP); // parity with MessageHeaders
- state = State.HEADER;
- } else {
- sb = new StringBuilder();
- sb.append(c);
- state = State.HEADER;
- }
- }
-
- private void resumeOrSecondCR(ByteBuffer input) {
- assert state == State.HEADER_FOUND_CR_LF;
- char c = (char)input.get();
- if (c == CR) {
- if (sb.length() > 0) {
- // no continuation line - flush
- // previous header value.
- String headerString = sb.toString();
- sb = new StringBuilder();
- addHeaderFromString(headerString);
- }
- state = State.HEADER_FOUND_CR_LF_CR;
- } else if (c == SP || c == HT) {
- assert sb.length() != 0;
- sb.append(SP); // continuation line
- state = State.HEADER;
- } else {
- if (sb.length() > 0) {
- // no continuation line - flush
- // previous header value.
- String headerString = sb.toString();
- sb = new StringBuilder();
- addHeaderFromString(headerString);
- }
- sb.append(c);
- state = State.HEADER;
- }
- }
-
- private void resumeOrEndHeaders(ByteBuffer input) throws ProtocolException {
- assert state == State.HEADER_FOUND_CR_LF_CR;
- char c = (char)input.get();
- if (c == LF) {
- state = State.FINISHED;
- headers = ImmutableHeaders.of(privateMap);
- privateMap = null;
- } else {
- throw protocolException("Unexpected \"%s\", after CR LF CR", c);
- }
- }
-
- private ProtocolException protocolException(String format, Object... args) {
- return new ProtocolException(format(format, args));
- }
-}
--- a/src/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<String,List<String>> filter =
- connection.headerFilter(request);
-
- // If we're sending this request through a tunnel,
- // then don't send any preemptive proxy-* headers that
- // the authentication filter may have saved in its
- // cache.
- collectHeaders1(sb, systemHeaders, filter);
-
- // If we're sending this request through a tunnel,
- // don't send any user-supplied proxy-* headers
- // to the target server.
- collectHeaders1(sb, userHeaders, filter);
- sb.append("\r\n");
- }
-
- private void collectHeaders1(StringBuilder sb, HttpHeaders headers,
- BiPredicate<String, List<String>> filter) {
- for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) {
- String key = entry.getKey();
- List<String> values = entry.getValue();
- if (!filter.test(key, values)) continue;
- for (String value : values) {
- sb.append(key).append(": ").append(value).append("\r\n");
- }
- }
- }
-
- private String getPathAndQuery(URI uri) {
- String path = uri.getPath();
- String query = uri.getQuery();
- if (path == null || path.equals("")) {
- path = "/";
- }
- if (query == null) {
- query = "";
- }
- if (query.equals("")) {
- return path;
- } else {
- return path + "?" + query;
- }
- }
-
- private String authorityString(InetSocketAddress addr) {
- return addr.getHostString() + ":" + addr.getPort();
- }
-
- private String hostString() {
- URI uri = request.uri();
- int port = uri.getPort();
- String host = uri.getHost();
-
- boolean defaultPort;
- if (port == -1) {
- defaultPort = true;
- } else if (request.secure()) {
- defaultPort = port == 443;
- } else {
- defaultPort = port == 80;
- }
-
- if (defaultPort) {
- return host;
- } else {
- return host + ":" + Integer.toString(port);
- }
- }
-
- private String requestURI() {
- URI uri = request.uri();
- String method = request.method();
-
- if ((request.proxy() == null && !method.equals("CONNECT"))
- || request.isWebSocket()) {
- return getPathAndQuery(uri);
- }
- if (request.secure()) {
- if (request.method().equals("CONNECT")) {
- // use authority for connect itself
- return authorityString(request.authority());
- } else {
- // requests over tunnel do not require full URL
- return getPathAndQuery(uri);
- }
- }
- if (request.method().equals("CONNECT")) {
- // use authority for connect itself
- return authorityString(request.authority());
- }
-
- return uri == null? authorityString(request.authority()) : uri.toString();
- }
-
- private boolean finished;
-
- synchronized boolean finished() {
- return finished;
- }
-
- synchronized void setFinished() {
- finished = true;
- }
-
- List<ByteBuffer> headers() {
- if (Log.requests() && request != null) {
- Log.logRequest(request.toString());
- }
- String uriString = requestURI();
- StringBuilder sb = new StringBuilder(64);
- sb.append(request.method())
- .append(' ')
- .append(uriString)
- .append(" HTTP/1.1\r\n");
-
- URI uri = request.uri();
- if (uri != null) {
- systemHeaders.setHeader("Host", hostString());
- }
- if (requestPublisher == null) {
- // Not a user request, or maybe a method, e.g. GET, with no body.
- contentLength = 0;
- } else {
- contentLength = requestPublisher.contentLength();
- }
-
- if (contentLength == 0) {
- systemHeaders.setHeader("Content-Length", "0");
- } else if (contentLength > 0) {
- systemHeaders.setHeader("Content-Length", Long.toString(contentLength));
- streaming = false;
- } else {
- streaming = true;
- systemHeaders.setHeader("Transfer-encoding", "chunked");
- }
- collectHeaders0(sb);
- String hs = sb.toString();
- logHeaders(hs);
- ByteBuffer b = ByteBuffer.wrap(hs.getBytes(US_ASCII));
- return List.of(b);
- }
-
- Http1BodySubscriber continueRequest() {
- Http1BodySubscriber subscriber;
- if (streaming) {
- subscriber = new StreamSubscriber();
- requestPublisher.subscribe(subscriber);
- } else {
- if (contentLength == 0)
- return null;
-
- subscriber = new FixedContentSubscriber();
- requestPublisher.subscribe(subscriber);
- }
- return subscriber;
- }
-
- class StreamSubscriber extends Http1BodySubscriber {
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- if (this.subscription != null) {
- Throwable t = new IllegalStateException("already subscribed");
- http1Exchange.appendToOutgoing(t);
- } else {
- this.subscription = subscription;
- }
- }
-
- @Override
- public void onNext(ByteBuffer item) {
- Objects.requireNonNull(item);
- if (complete) {
- Throwable t = new IllegalStateException("subscription already completed");
- http1Exchange.appendToOutgoing(t);
- } else {
- int chunklen = item.remaining();
- ArrayList<ByteBuffer> l = new ArrayList<>(3);
- l.add(getHeader(chunklen));
- l.add(item);
- l.add(ByteBuffer.wrap(CRLF));
- http1Exchange.appendToOutgoing(l);
- }
- }
-
- @Override
- public void onError(Throwable throwable) {
- if (complete)
- return;
-
- subscription.cancel();
- http1Exchange.appendToOutgoing(throwable);
- }
-
- @Override
- public void onComplete() {
- if (complete) {
- Throwable t = new IllegalStateException("subscription already completed");
- http1Exchange.appendToOutgoing(t);
- } else {
- ArrayList<ByteBuffer> l = new ArrayList<>(2);
- l.add(ByteBuffer.wrap(EMPTY_CHUNK_BYTES));
- l.add(ByteBuffer.wrap(CRLF));
- complete = true;
- //setFinished();
- http1Exchange.appendToOutgoing(l);
- http1Exchange.appendToOutgoing(COMPLETED);
- setFinished(); // TODO: before or after,? does it matter?
-
- }
- }
- }
-
- class FixedContentSubscriber extends Http1BodySubscriber {
-
- private volatile long contentWritten;
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- if (this.subscription != null) {
- Throwable t = new IllegalStateException("already subscribed");
- http1Exchange.appendToOutgoing(t);
- } else {
- this.subscription = subscription;
- }
- }
-
- @Override
- public void onNext(ByteBuffer item) {
- debug.log(Level.DEBUG, "onNext");
- Objects.requireNonNull(item);
- if (complete) {
- Throwable t = new IllegalStateException("subscription already completed");
- http1Exchange.appendToOutgoing(t);
- } else {
- long writing = item.remaining();
- long written = (contentWritten += writing);
-
- if (written > contentLength) {
- subscription.cancel();
- String msg = connection.getConnectionFlow()
- + " [" + Thread.currentThread().getName() +"] "
- + "Too many bytes in request body. Expected: "
- + contentLength + ", got: " + written;
- http1Exchange.appendToOutgoing(new IOException(msg));
- } else {
- http1Exchange.appendToOutgoing(List.of(item));
- }
- }
- }
-
- @Override
- public void onError(Throwable throwable) {
- debug.log(Level.DEBUG, "onError");
- if (complete) // TODO: error?
- return;
-
- subscription.cancel();
- http1Exchange.appendToOutgoing(throwable);
- }
-
- @Override
- public void onComplete() {
- debug.log(Level.DEBUG, "onComplete");
- if (complete) {
- Throwable t = new IllegalStateException("subscription already completed");
- http1Exchange.appendToOutgoing(t);
- } else {
- complete = true;
- long written = contentWritten;
- if (contentLength > written) {
- subscription.cancel();
- Throwable t = new IOException(connection.getConnectionFlow()
- + " [" + Thread.currentThread().getName() +"] "
- + "Too few bytes returned by the publisher ("
- + written + "/"
- + contentLength + ")");
- http1Exchange.appendToOutgoing(t);
- } else {
- http1Exchange.appendToOutgoing(COMPLETED);
- }
- }
- }
- }
-
- private static final byte[] CRLF = {'\r', '\n'};
- private static final byte[] EMPTY_CHUNK_BYTES = {'0', '\r', '\n'};
-
- /** Returns a header for a particular chunk size */
- private static ByteBuffer getHeader(int size) {
- String hexStr = Integer.toHexString(size);
- byte[] hexBytes = hexStr.getBytes(US_ASCII);
- byte[] header = new byte[hexStr.length()+2];
- System.arraycopy(hexBytes, 0, header, 0, hexBytes.length);
- header[hexBytes.length] = CRLF[0];
- header[hexBytes.length+1] = CRLF[1];
- return ByteBuffer.wrap(header);
- }
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- final System.Logger debug = Utils.getDebugLogger(this::toString, DEBUG);
-
-}
--- a/src/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<T> {
-
- private volatile ResponseContent content;
- private final HttpRequestImpl request;
- private Response response;
- private final HttpConnection connection;
- private HttpHeaders headers;
- private int responseCode;
- private final Http1Exchange<T> exchange;
- private boolean return2Cache; // return connection to cache when finished
- private final HeadersReader headersReader; // used to read the headers
- private final BodyReader bodyReader; // used to read the body
- private final Http1AsyncReceiver asyncReceiver;
- private volatile EOFException eof;
- // max number of bytes of (fixed length) body to ignore on redirect
- private final static int MAX_IGNORE = 1024;
-
- // Revisit: can we get rid of this?
- static enum State {INITIAL, READING_HEADERS, READING_BODY, DONE}
- private volatile State readProgress = State.INITIAL;
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- final System.Logger debug = Utils.getDebugLogger(this.getClass()::getSimpleName, DEBUG);
-
-
- Http1Response(HttpConnection conn,
- Http1Exchange<T> exchange,
- Http1AsyncReceiver asyncReceiver) {
- this.readProgress = State.INITIAL;
- this.request = exchange.request();
- this.exchange = exchange;
- this.connection = conn;
- this.asyncReceiver = asyncReceiver;
- headersReader = new HeadersReader(this::advance);
- bodyReader = new BodyReader(this::advance);
- }
-
- public CompletableFuture<Response> readHeadersAsync(Executor executor) {
- debug.log(Level.DEBUG, () -> "Reading Headers: (remaining: "
- + asyncReceiver.remaining() +") " + readProgress);
- // with expect continue we will resume reading headers + body.
- asyncReceiver.unsubscribe(bodyReader);
- bodyReader.reset();
- Http1HeaderParser hd = new Http1HeaderParser();
- readProgress = State.READING_HEADERS;
- headersReader.start(hd);
- asyncReceiver.subscribe(headersReader);
- CompletableFuture<State> cf = headersReader.completion();
- assert cf != null : "parsing not started";
-
- Function<State, Response> lambda = (State completed) -> {
- assert completed == State.READING_HEADERS;
- debug.log(Level.DEBUG, () ->
- "Reading Headers: creating Response object;"
- + " state is now " + readProgress);
- asyncReceiver.unsubscribe(headersReader);
- responseCode = hd.responseCode();
- headers = hd.headers();
-
- response = new Response(request,
- exchange.getExchange(),
- headers,
- responseCode,
- HTTP_1_1);
- return response;
- };
-
- if (executor != null) {
- return cf.thenApplyAsync(lambda, executor);
- } else {
- return cf.thenApply(lambda);
- }
- }
-
- private boolean finished;
-
- synchronized void completed() {
- finished = true;
- }
-
- synchronized boolean finished() {
- return finished;
- }
-
- int fixupContentLen(int clen) {
- if (request.method().equalsIgnoreCase("HEAD")) {
- return 0;
- }
- if (clen == -1) {
- if (headers.firstValue("Transfer-encoding").orElse("")
- .equalsIgnoreCase("chunked")) {
- return -1;
- }
- return 0;
- }
- return clen;
- }
-
- /**
- * Read up to MAX_IGNORE bytes discarding
- */
- public CompletableFuture<Void> ignoreBody(Executor executor) {
- int clen = (int)headers.firstValueAsLong("Content-Length").orElse(-1);
- if (clen == -1 || clen > MAX_IGNORE) {
- connection.close();
- return MinimalFuture.completedFuture(null); // not treating as error
- } else {
- return readBody(HttpResponse.BodySubscriber.discard(), true, executor);
- }
- }
-
- public <U> CompletableFuture<U> readBody(HttpResponse.BodySubscriber<U> p,
- boolean return2Cache,
- Executor executor) {
- this.return2Cache = return2Cache;
- final HttpResponse.BodySubscriber<U> pusher = p;
-
- final CompletableFuture<U> cf = new MinimalFuture<>();
-
- int clen0 = (int)headers.firstValueAsLong("Content-Length").orElse(-1);
-
- final int clen = fixupContentLen(clen0);
-
- // expect-continue reads headers and body twice.
- // if we reach here, we must reset the headersReader state.
- asyncReceiver.unsubscribe(headersReader);
- headersReader.reset();
-
- executor.execute(() -> {
- try {
- HttpClientImpl client = connection.client();
- content = new ResponseContent(
- connection, clen, headers, pusher,
- this::onFinished
- );
- if (cf.isCompletedExceptionally()) {
- // if an error occurs during subscription
- connection.close();
- return;
- }
- // increment the reference count on the HttpClientImpl
- // to prevent the SelectorManager thread from exiting until
- // the body is fully read.
- client.reference();
- bodyReader.start(content.getBodyParser(
- (t) -> {
- try {
- if (t != null) {
- pusher.onError(t);
- connection.close();
- if (!cf.isDone())
- cf.completeExceptionally(t);
- }
- } finally {
- // decrement the reference count on the HttpClientImpl
- // to allow the SelectorManager thread to exit if no
- // other operation is pending and the facade is no
- // longer referenced.
- client.unreference();
- bodyReader.onComplete(t);
- }
- }));
- CompletableFuture<State> bodyReaderCF = bodyReader.completion();
- asyncReceiver.subscribe(bodyReader);
- assert bodyReaderCF != null : "parsing not started";
- // Make sure to keep a reference to asyncReceiver from
- // within this
- CompletableFuture<?> trailingOp = bodyReaderCF.whenComplete((s,t) -> {
- t = Utils.getCompletionCause(t);
- try {
- if (t != null) {
- debug.log(Level.DEBUG, () ->
- "Finished reading body: " + s);
- assert s == State.READING_BODY;
- }
- if (t != null && !cf.isDone()) {
- pusher.onError(t);
- cf.completeExceptionally(t);
- }
- } catch (Throwable x) {
- // not supposed to happen
- asyncReceiver.onReadError(x);
- }
- });
- connection.addTrailingOperation(trailingOp);
- } catch (Throwable t) {
- debug.log(Level.DEBUG, () -> "Failed reading body: " + t);
- try {
- if (!cf.isDone()) {
- pusher.onError(t);
- cf.completeExceptionally(t);
- }
- } finally {
- asyncReceiver.onReadError(t);
- }
- }
- });
- 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<T>
- implements Http1AsyncReceiver.Http1AsyncDelegate {
- abstract void start(T parser);
- abstract CompletableFuture<State> completion();
- // accepts a buffer from upstream.
- // this should be implemented as a simple call to
- // accept(ref, parser, cf)
- public abstract boolean tryAsyncReceive(ByteBuffer buffer);
- public abstract void onReadError(Throwable t);
- // handle a byte buffer received from upstream.
- // this method should set the value of Http1Response.buffer
- // to ref.get() before beginning parsing.
- abstract void handle(ByteBuffer buf, T parser,
- CompletableFuture<State> cf);
- // resets this objects state so that it can be reused later on
- // typically puts the reference to parser and completion to null
- abstract void reset();
-
- // accepts a byte buffer received from upstream
- // returns true if the buffer is fully parsed and more data can
- // be accepted, false otherwise.
- final boolean accept(ByteBuffer buf, T parser,
- CompletableFuture<State> cf) {
- if (cf == null || parser == null || cf.isDone()) return false;
- handle(buf, parser, cf);
- return !cf.isDone();
- }
- public abstract void onSubscribe(AbstractSubscription s);
- public abstract AbstractSubscription subscription();
-
- }
-
- // Invoked with each new ByteBuffer when reading headers...
- final class HeadersReader extends Receiver<Http1HeaderParser> {
- final Consumer<State> onComplete;
- volatile Http1HeaderParser parser;
- volatile CompletableFuture<State> cf;
- volatile long count; // bytes parsed (for debug)
- volatile AbstractSubscription subscription;
-
- HeadersReader(Consumer<State> onComplete) {
- this.onComplete = onComplete;
- }
-
- @Override
- public AbstractSubscription subscription() {
- return subscription;
- }
-
- @Override
- public void onSubscribe(AbstractSubscription s) {
- this.subscription = s;
- s.request(1);
- }
-
- @Override
- void reset() {
- cf = null;
- parser = null;
- count = 0;
- subscription = null;
- }
-
- // Revisit: do we need to support restarting?
- @Override
- final void start(Http1HeaderParser hp) {
- count = 0;
- cf = new MinimalFuture<>();
- parser = hp;
- }
-
- @Override
- CompletableFuture<State> completion() {
- return cf;
- }
-
- @Override
- public final boolean tryAsyncReceive(ByteBuffer ref) {
- boolean hasDemand = subscription.demand().tryDecrement();
- assert hasDemand;
- boolean needsMore = accept(ref, parser, cf);
- if (needsMore) subscription.request(1);
- return needsMore;
- }
-
- @Override
- public final void onReadError(Throwable t) {
- Http1Response.this.onReadError(t);
- }
-
- @Override
- final void handle(ByteBuffer b,
- Http1HeaderParser parser,
- CompletableFuture<State> cf) {
- assert cf != null : "parsing not started";
- assert parser != null : "no parser";
- try {
- count += b.remaining();
- debug.log(Level.DEBUG, () -> "Sending " + b.remaining()
- + "/" + b.capacity() + " bytes to header parser");
- if (parser.parse(b)) {
- count -= b.remaining();
- debug.log(Level.DEBUG, () ->
- "Parsing headers completed. bytes=" + count);
- onComplete.accept(State.READING_HEADERS);
- cf.complete(State.READING_HEADERS);
- }
- } catch (Throwable t) {
- debug.log(Level.DEBUG,
- () -> "Header parser failed to handle buffer: " + t);
- cf.completeExceptionally(t);
- }
- }
- }
-
- // Invoked with each new ByteBuffer when reading bodies...
- final class BodyReader extends Receiver<BodyParser> {
- final Consumer<State> onComplete;
- volatile BodyParser parser;
- volatile CompletableFuture<State> cf;
- volatile AbstractSubscription subscription;
- BodyReader(Consumer<State> onComplete) {
- this.onComplete = onComplete;
- }
-
- @Override
- void reset() {
- parser = null;
- cf = null;
- subscription = null;
- }
-
- // Revisit: do we need to support restarting?
- @Override
- final void start(BodyParser parser) {
- cf = new MinimalFuture<>();
- this.parser = parser;
- }
-
- @Override
- CompletableFuture<State> completion() {
- return cf;
- }
-
- @Override
- public final boolean tryAsyncReceive(ByteBuffer b) {
- return accept(b, parser, cf);
- }
-
- @Override
- public final void onReadError(Throwable t) {
- Http1Response.this.onReadError(t);
- }
-
- @Override
- public AbstractSubscription subscription() {
- return subscription;
- }
-
- @Override
- public void onSubscribe(AbstractSubscription s) {
- this.subscription = s;
- parser.onSubscribe(s);
- }
-
- @Override
- final void handle(ByteBuffer b,
- BodyParser parser,
- CompletableFuture<State> cf) {
- assert cf != null : "parsing not started";
- assert parser != null : "no parser";
- try {
- debug.log(Level.DEBUG, () -> "Sending " + b.remaining()
- + "/" + b.capacity() + " bytes to body parser");
- parser.accept(b);
- } catch (Throwable t) {
- debug.log(Level.DEBUG,
- () -> "Body parser failed to handle buffer: " + t);
- if (!cf.isDone()) {
- cf.completeExceptionally(t);
- }
- }
- }
-
- final void onComplete(Throwable closedExceptionally) {
- if (cf.isDone()) return;
- if (closedExceptionally != null) {
- cf.completeExceptionally(closedExceptionally);
- } else {
- onComplete.accept(State.READING_BODY);
- cf.complete(State.READING_BODY);
- }
- }
-
- @Override
- public String toString() {
- return super.toString() + "/parser=" + String.valueOf(parser);
- }
-
- }
-}
--- a/src/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<String,Http2Connection> connections = new ConcurrentHashMap<>();
-
- private final Set<String> failures = Collections.synchronizedSet(new HashSet<>());
-
- /**
- * When HTTP/2 requested only. The following describes the aggregate behavior including the
- * calling code. In all cases, the HTTP2 connection cache
- * is checked first for a suitable connection and that is returned if available.
- * If not, a new connection is opened, except in https case when a previous negotiate failed.
- * In that case, we want to continue using http/1.1. When a connection is to be opened and
- * if multiple requests are sent in parallel then each will open a new connection.
- *
- * If negotiation/upgrade succeeds then
- * one connection will be put in the cache and the others will be closed
- * after the initial request completes (not strictly necessary for h2, only for h2c)
- *
- * If negotiate/upgrade fails, then any opened connections remain open (as http/1.1)
- * and will be used and cached in the http/1 cache. Note, this method handles the
- * https failure case only (by completing the CF with an ALPN exception, handled externally)
- * The h2c upgrade is handled externally also.
- *
- * Specific CF behavior of this method.
- * 1. completes with ALPN exception: h2 negotiate failed for first time. failure recorded.
- * 2. completes with other exception: failure not recorded. Caller must handle
- * 3. completes normally with null: no connection in cache for h2c or h2 failed previously
- * 4. completes normally with connection: h2 or h2c connection in cache. Use it.
- */
- CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req) {
- URI uri = req.uri();
- InetSocketAddress proxy = req.proxy();
- String key = Http2Connection.keyFor(uri, proxy);
-
- synchronized (this) {
- Http2Connection connection = connections.get(key);
- if (connection != null) { // fast path if connection already exists
- return MinimalFuture.completedFuture(connection);
- }
-
- if (!req.secure() || failures.contains(key)) {
- // secure: negotiate failed before. Use http/1.1
- // !secure: no connection available in cache. Attempt upgrade
- return MinimalFuture.completedFuture(null);
- }
- }
- return Http2Connection
- .createAsync(req, this)
- .whenComplete((conn, t) -> {
- synchronized (Http2ClientImpl.this) {
- if (conn != null) {
- offerConnection(conn);
- } else {
- Throwable cause = Utils.getCompletionCause(t);
- if (cause instanceof Http2Connection.ALPNException)
- failures.add(key);
- }
- }
- });
- }
-
- /*
- * Cache the given connection, if no connection to the same
- * destination exists. If one exists, then we let the initial stream
- * complete but allow it to close itself upon completion.
- * This situation should not arise with https because the request
- * has not been sent as part of the initial alpn negotiation
- */
- boolean offerConnection(Http2Connection c) {
- String key = c.key();
- Http2Connection c1 = connections.putIfAbsent(key, c);
- if (c1 != null) {
- c.setSingleStream(true);
- return false;
- }
- return true;
- }
-
- void deleteConnection(Http2Connection c) {
- connections.remove(c.key());
- }
-
- void stop() {
- debug.log(Level.DEBUG, "stopping");
- connections.values().forEach(this::close);
- connections.clear();
- }
-
- private void close(Http2Connection h2c) {
- try { h2c.close(); } catch (Throwable t) {}
- }
-
- HttpClientImpl client() {
- return client;
- }
-
- /** Returns the client settings as a base64 (url) encoded string */
- String getSettingsString() {
- SettingsFrame sf = getClientSettings();
- byte[] settings = sf.toByteArray(); // without the header
- Base64.Encoder encoder = Base64.getUrlEncoder()
- .withoutPadding();
- return encoder.encodeToString(settings);
- }
-
- private static final int K = 1024;
-
- private static int getParameter(String property, int min, int max, int defaultValue) {
- int value = Utils.getIntegerNetProperty(property, defaultValue);
- // use default value if misconfigured
- if (value < min || value > max) {
- Log.logError("Property value for {0}={1} not in [{2}..{3}]: " +
- "using default={4}", property, value, min, max, defaultValue);
- value = defaultValue;
- }
- return value;
- }
-
- // used for the connection window, to have a connection window size
- // bigger than the initial stream window size.
- int getConnectionWindowSize(SettingsFrame clientSettings) {
- // Maximum size is 2^31-1. Don't allow window size to be less
- // than the stream window size. HTTP/2 specify a default of 64 * K -1,
- // but we use 2^26 by default for better performance.
- int streamWindow = clientSettings.getParameter(INITIAL_WINDOW_SIZE);
-
- // The default is the max between the stream window size
- // and the connection window size.
- int defaultValue = Math.min(Integer.MAX_VALUE,
- Math.max(streamWindow, K*K*32));
-
- return getParameter(
- "jdk.httpclient.connectionWindowSize",
- streamWindow, Integer.MAX_VALUE, defaultValue);
- }
-
- SettingsFrame getClientSettings() {
- SettingsFrame frame = new SettingsFrame();
- // default defined for HTTP/2 is 4 K, we use 16 K.
- frame.setParameter(HEADER_TABLE_SIZE, getParameter(
- "jdk.httpclient.hpack.maxheadertablesize",
- 0, Integer.MAX_VALUE, 16 * K));
- // O: does not accept push streams. 1: accepts push streams.
- frame.setParameter(ENABLE_PUSH, getParameter(
- "jdk.httpclient.enablepush",
- 0, 1, 1));
- // HTTP/2 recommends to set the number of concurrent streams
- // no lower than 100. We use 100. 0 means no stream would be
- // accepted. That would render the client to be non functional,
- // so we won't let 0 be configured for our Http2ClientImpl.
- frame.setParameter(MAX_CONCURRENT_STREAMS, getParameter(
- "jdk.httpclient.maxstreams",
- 1, Integer.MAX_VALUE, 100));
- // Maximum size is 2^31-1. Don't allow window size to be less
- // than the minimum frame size as this is likely to be a
- // configuration error. HTTP/2 specify a default of 64 * K -1,
- // but we use 16 M for better performance.
- frame.setParameter(INITIAL_WINDOW_SIZE, getParameter(
- "jdk.httpclient.windowsize",
- 16 * K, Integer.MAX_VALUE, 16*K*K));
- // HTTP/2 specify a minimum size of 16 K, a maximum size of 2^24-1,
- // and a default of 16 K. We use 16 K as default.
- frame.setParameter(MAX_FRAME_SIZE, getParameter(
- "jdk.httpclient.maxframesize",
- 16 * K, 16 * K * K -1, 16 * K));
- return frame;
- }
-}
--- a/src/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<ByteBuffer> pending;
-
- boolean processReceivedData(FramesDecoder decoder, ByteBuffer buf)
- throws IOException
- {
- // if preface is not sent, buffers data in the pending list
- if (!prefaceSent) {
- debug.log(Level.DEBUG, "Preface is not sent: buffering %d",
- buf.remaining());
- synchronized (this) {
- if (!prefaceSent) {
- if (pending == null) pending = new ArrayList<>();
- pending.add(buf);
- debug.log(Level.DEBUG, () -> "there are now "
- + Utils.remaining(pending)
- + " bytes buffered waiting for preface to be sent");
- return false;
- }
- }
- }
-
- // Preface is sent. Checks for pending data and flush it.
- // We rely on this method being called from within the Http2TubeSubscriber
- // scheduler, so we know that no other thread could execute this method
- // concurrently while we're here.
- // This ensures that later incoming buffers will not
- // be processed before we have flushed the pending queue.
- // No additional synchronization is therefore necessary here.
- List<ByteBuffer> pending = this.pending;
- this.pending = null;
- if (pending != null) {
- // flush pending data
- debug.log(Level.DEBUG, () -> "Processing buffered data: "
- + Utils.remaining(pending));
- for (ByteBuffer b : pending) {
- decoder.decode(b);
- }
- }
- // push the received buffer to the frames decoder.
- if (buf != EMPTY_TRIGGER) {
- debug.log(Level.DEBUG, "Processing %d", buf.remaining());
- decoder.decode(buf);
- }
- return true;
- }
-
- // Mark that the connection preface is sent
- void markPrefaceSent() {
- assert !prefaceSent;
- synchronized (this) {
- prefaceSent = true;
- }
- }
- }
-
- volatile boolean closed;
-
- //-------------------------------------
- final HttpConnection connection;
- private final Http2ClientImpl client2;
- private final Map<Integer,Stream<?>> streams = new ConcurrentHashMap<>();
- private int nextstreamid;
- private int nextPushStream = 2;
- private final Encoder hpackOut;
- private final Decoder hpackIn;
- final SettingsFrame clientSettings;
- private volatile SettingsFrame serverSettings;
- private final String key; // for HttpClientImpl.connections map
- private final FramesDecoder framesDecoder;
- private final FramesEncoder framesEncoder = new FramesEncoder();
-
- /**
- * Send Window controller for both connection and stream windows.
- * Each of this connection's Streams MUST use this controller.
- */
- private final WindowController windowController = new WindowController();
- private final FramesController framesController = new FramesController();
- private final Http2TubeSubscriber subscriber = new Http2TubeSubscriber();
- final ConnectionWindowUpdateSender windowUpdater;
- private volatile Throwable cause;
- private volatile Supplier<ByteBuffer> initial;
-
- static final int DEFAULT_FRAME_SIZE = 16 * 1024;
-
-
- // TODO: need list of control frames from other threads
- // that need to be sent
-
- private Http2Connection(HttpConnection connection,
- Http2ClientImpl client2,
- int nextstreamid,
- String key) {
- this.connection = connection;
- this.client2 = client2;
- this.nextstreamid = nextstreamid;
- this.key = key;
- this.clientSettings = this.client2.getClientSettings();
- this.framesDecoder = new FramesDecoder(this::processFrame,
- clientSettings.getParameter(SettingsFrame.MAX_FRAME_SIZE));
- // serverSettings will be updated by server
- this.serverSettings = SettingsFrame.getDefaultSettings();
- this.hpackOut = new Encoder(serverSettings.getParameter(HEADER_TABLE_SIZE));
- this.hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE));
- debugHpack.log(Level.DEBUG, () -> "For the record:" + super.toString());
- debugHpack.log(Level.DEBUG, "Decoder created: %s", hpackIn);
- debugHpack.log(Level.DEBUG, "Encoder created: %s", hpackOut);
- this.windowUpdater = new ConnectionWindowUpdateSender(this,
- client2.getConnectionWindowSize(clientSettings));
- }
-
- /**
- * Case 1) Create from upgraded HTTP/1.1 connection.
- * Is ready to use. Can be SSL. exchange is the Exchange
- * that initiated the connection, whose response will be delivered
- * on a Stream.
- */
- private Http2Connection(HttpConnection connection,
- Http2ClientImpl client2,
- Exchange<?> exchange,
- Supplier<ByteBuffer> initial)
- throws IOException, InterruptedException
- {
- this(connection,
- client2,
- 3, // stream 1 is registered during the upgrade
- keyFor(connection));
- Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
-
- Stream<?> initialStream = createStream(exchange);
- initialStream.registerStream(1);
- windowController.registerStream(1, getInitialSendWindowSize());
- initialStream.requestSent();
- // Upgrading:
- // set callbacks before sending preface - makes sure anything that
- // might be sent by the server will come our way.
- this.initial = initial;
- connectFlows(connection);
- sendConnectionPreface();
- }
-
- // Used when upgrading an HTTP/1.1 connection to HTTP/2 after receiving
- // agreement from the server. Async style but completes immediately, because
- // the connection is already connected.
- static CompletableFuture<Http2Connection> createAsync(HttpConnection connection,
- Http2ClientImpl client2,
- Exchange<?> exchange,
- Supplier<ByteBuffer> initial)
- {
- return MinimalFuture.supply(() -> new Http2Connection(connection, client2, exchange, initial));
- }
-
- // Requires TLS handshake. So, is really async
- static CompletableFuture<Http2Connection> createAsync(HttpRequestImpl request,
- Http2ClientImpl h2client) {
- assert request.secure();
- AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection)
- HttpConnection.getConnection(request.getAddress(),
- h2client.client(),
- request,
- HttpClient.Version.HTTP_2);
-
- return connection.connectAsync()
- .thenCompose(unused -> checkSSLConfig(connection))
- .thenCompose(notused-> {
- CompletableFuture<Http2Connection> cf = new MinimalFuture<>();
- try {
- Http2Connection hc = new Http2Connection(request, h2client, connection);
- cf.complete(hc);
- } catch (IOException e) {
- cf.completeExceptionally(e);
- }
- return cf; } );
- }
-
- /**
- * Cases 2) 3)
- *
- * request is request to be sent.
- */
- private Http2Connection(HttpRequestImpl request,
- Http2ClientImpl h2client,
- HttpConnection connection)
- throws IOException
- {
- this(connection,
- h2client,
- 1,
- keyFor(request.uri(), request.proxy()));
-
- Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
-
- // safe to resume async reading now.
- connectFlows(connection);
- sendConnectionPreface();
- }
-
- private void connectFlows(HttpConnection connection) {
- FlowTube tube = connection.getConnectionFlow();
- // Connect the flow to our Http2TubeSubscriber:
- tube.connectFlows(connection.publisher(), subscriber);
- }
-
- final HttpClientImpl client() {
- return client2.client();
- }
-
- /**
- * Throws an IOException if h2 was not negotiated
- */
- private static CompletableFuture<?> checkSSLConfig(AbstractAsyncSSLConnection aconn) {
- assert aconn.isSecure();
-
- Function<String, CompletableFuture<Void>> checkAlpnCF = (alpn) -> {
- CompletableFuture<Void> cf = new MinimalFuture<>();
- SSLEngine engine = aconn.getEngine();
- assert Objects.equals(alpn, engine.getApplicationProtocol());
-
- DEBUG_LOGGER.log(Level.DEBUG, "checkSSLConfig: alpn: %s", alpn );
-
- if (alpn == null || !alpn.equals("h2")) {
- String msg;
- if (alpn == null) {
- Log.logSSL("ALPN not supported");
- msg = "ALPN not supported";
- } else {
- switch (alpn) {
- case "":
- Log.logSSL(msg = "No ALPN negotiated");
- break;
- case "http/1.1":
- Log.logSSL( msg = "HTTP/1.1 ALPN returned");
- break;
- default:
- Log.logSSL(msg = "Unexpected ALPN: " + alpn);
- cf.completeExceptionally(new IOException(msg));
- }
- }
- cf.completeExceptionally(new ALPNException(msg, aconn));
- return cf;
- }
- cf.complete(null);
- return cf;
- };
-
- return aconn.getALPN()
- .whenComplete((r,t) -> {
- if (t != null && t instanceof SSLException) {
- // something went wrong during the initial handshake
- // close the connection
- aconn.close();
- }
- })
- .thenCompose(checkAlpnCF);
- }
-
- synchronized boolean singleStream() {
- return singleStream;
- }
-
- synchronized void setSingleStream(boolean use) {
- singleStream = use;
- }
-
- static String keyFor(HttpConnection connection) {
- boolean isProxy = connection.isProxied();
- boolean isSecure = connection.isSecure();
- InetSocketAddress addr = connection.address();
-
- return keyString(isSecure, isProxy, addr.getHostString(), addr.getPort());
- }
-
- static String keyFor(URI uri, InetSocketAddress proxy) {
- boolean isSecure = uri.getScheme().equalsIgnoreCase("https");
- boolean isProxy = proxy != null;
-
- String host;
- int port;
-
- if (proxy != null) {
- host = proxy.getHostString();
- port = proxy.getPort();
- } else {
- host = uri.getHost();
- port = uri.getPort();
- }
- return keyString(isSecure, isProxy, host, port);
- }
-
- // {C,S}:{H:P}:host:port
- // C indicates clear text connection "http"
- // S indicates secure "https"
- // H indicates host (direct) connection
- // P indicates proxy
- // Eg: "S:H:foo.com:80"
- static String keyString(boolean secure, boolean proxy, String host, int port) {
- if (secure && port == -1)
- port = 443;
- else if (!secure && port == -1)
- port = 80;
- return (secure ? "S:" : "C:") + (proxy ? "P:" : "H:") + host + ":" + port;
- }
-
- String key() {
- return this.key;
- }
-
- boolean offerConnection() {
- return client2.offerConnection(this);
- }
-
- private HttpPublisher publisher() {
- return connection.publisher();
- }
-
- private void decodeHeaders(HeaderFrame frame, DecodingCallback decoder)
- throws IOException
- {
- debugHpack.log(Level.DEBUG, "decodeHeaders(%s)", decoder);
-
- boolean endOfHeaders = frame.getFlag(HeaderFrame.END_HEADERS);
-
- List<ByteBuffer> buffers = frame.getHeaderBlock();
- int len = buffers.size();
- for (int i = 0; i < len; i++) {
- ByteBuffer b = buffers.get(i);
- hpackIn.decode(b, endOfHeaders && (i == len - 1), decoder);
- }
- }
-
- final int getInitialSendWindowSize() {
- return serverSettings.getParameter(INITIAL_WINDOW_SIZE);
- }
-
- void close() {
- Log.logTrace("Closing HTTP/2 connection: to {0}", connection.address());
- GoAwayFrame f = new GoAwayFrame(0,
- ErrorFrame.NO_ERROR,
- "Requested by user".getBytes(UTF_8));
- // TODO: set last stream. For now zero ok.
- sendFrame(f);
- }
-
- long count;
- final void asyncReceive(ByteBuffer buffer) {
- // We don't need to read anything and
- // we don't want to send anything back to the server
- // until the connection preface has been sent.
- // Therefore we're going to wait if needed before reading
- // (and thus replying) to anything.
- // Starting to reply to something (e.g send an ACK to a
- // SettingsFrame sent by the server) before the connection
- // preface is fully sent might result in the server
- // sending a GOAWAY frame with 'invalid_preface'.
- //
- // Note: asyncReceive is only called from the Http2TubeSubscriber
- // sequential scheduler.
- try {
- Supplier<ByteBuffer> bs = initial;
- // ensure that we always handle the initial buffer first,
- // if any.
- if (bs != null) {
- initial = null;
- ByteBuffer b = bs.get();
- if (b.hasRemaining()) {
- long c = ++count;
- debug.log(Level.DEBUG, () -> "H2 Receiving Initial("
- + c +"): " + b.remaining());
- framesController.processReceivedData(framesDecoder, b);
- }
- }
- ByteBuffer b = buffer;
- // the Http2TubeSubscriber scheduler ensures that the order of incoming
- // buffers is preserved.
- if (b == EMPTY_TRIGGER) {
- debug.log(Level.DEBUG, "H2 Received EMPTY_TRIGGER");
- boolean prefaceSent = framesController.prefaceSent;
- assert prefaceSent;
- // call framesController.processReceivedData to potentially
- // trigger the processing of all the data buffered there.
- framesController.processReceivedData(framesDecoder, buffer);
- debug.log(Level.DEBUG, "H2 processed buffered data");
- } else {
- long c = ++count;
- debug.log(Level.DEBUG, "H2 Receiving(%d): %d", c, b.remaining());
- framesController.processReceivedData(framesDecoder, buffer);
- debug.log(Level.DEBUG, "H2 processed(%d)", c);
- }
- } catch (Throwable e) {
- String msg = Utils.stackTrace(e);
- Log.logTrace(msg);
- shutdown(e);
- }
- }
-
- Throwable getRecordedCause() {
- return cause;
- }
-
- void shutdown(Throwable t) {
- debug.log(Level.DEBUG, () -> "Shutting down h2c (closed="+closed+"): " + t);
- if (closed == true) return;
- synchronized (this) {
- if (closed == true) return;
- closed = true;
- }
- Log.logError(t);
- Throwable initialCause = this.cause;
- if (initialCause == null) this.cause = t;
- client2.deleteConnection(this);
- List<Stream<?>> c = new LinkedList<>(streams.values());
- for (Stream<?> s : c) {
- s.cancelImpl(t);
- }
- connection.close();
- }
-
- /**
- * Streams initiated by a client MUST use odd-numbered stream
- * identifiers; those initiated by the server MUST use even-numbered
- * stream identifiers.
- */
- private static final boolean isSeverInitiatedStream(int streamid) {
- return (streamid & 0x1) == 0;
- }
-
- /**
- * Handles stream 0 (common) frames that apply to whole connection and passes
- * other stream specific frames to that Stream object.
- *
- * Invokes Stream.incoming() which is expected to process frame without
- * blocking.
- */
- void processFrame(Http2Frame frame) throws IOException {
- Log.logFrames(frame, "IN");
- int streamid = frame.streamid();
- if (frame instanceof MalformedFrame) {
- Log.logError(((MalformedFrame) frame).getMessage());
- if (streamid == 0) {
- framesDecoder.close("Malformed frame on stream 0");
- protocolError(((MalformedFrame) frame).getErrorCode(),
- ((MalformedFrame) frame).getMessage());
- } else {
- debug.log(Level.DEBUG, () -> "Reset stream: "
- + ((MalformedFrame) frame).getMessage());
- resetStream(streamid, ((MalformedFrame) frame).getErrorCode());
- }
- return;
- }
- if (streamid == 0) {
- handleConnectionFrame(frame);
- } else {
- if (frame instanceof SettingsFrame) {
- // The stream identifier for a SETTINGS frame MUST be zero
- framesDecoder.close(
- "The stream identifier for a SETTINGS frame MUST be zero");
- protocolError(GoAwayFrame.PROTOCOL_ERROR);
- return;
- }
-
- Stream<?> stream = getStream(streamid);
- if (stream == null) {
- // Should never receive a frame with unknown stream id
-
- if (frame instanceof HeaderFrame) {
- // always decode the headers as they may affect
- // connection-level HPACK decoding state
- HeaderDecoder decoder = new LoggingHeaderDecoder(new HeaderDecoder());
- decodeHeaders((HeaderFrame) frame, decoder);
- }
-
- if (!(frame instanceof ResetFrame)) {
- if (isSeverInitiatedStream(streamid)) {
- if (streamid < nextPushStream) {
- // trailing data on a cancelled push promise stream,
- // reset will already have been sent, ignore
- Log.logTrace("Ignoring cancelled push promise frame " + frame);
- } else {
- resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
- }
- } else if (streamid >= nextstreamid) {
- // otherwise the stream has already been reset/closed
- resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
- }
- }
- return;
- }
- if (frame instanceof PushPromiseFrame) {
- PushPromiseFrame pp = (PushPromiseFrame)frame;
- handlePushPromise(stream, pp);
- } else if (frame instanceof HeaderFrame) {
- // decode headers (or continuation)
- decodeHeaders((HeaderFrame) frame, stream.rspHeadersConsumer());
- stream.incoming(frame);
- } else {
- stream.incoming(frame);
- }
- }
- }
-
- private <T> void handlePushPromise(Stream<T> parent, PushPromiseFrame pp)
- throws IOException
- {
- // always decode the headers as they may affect connection-level HPACK
- // decoding state
- HeaderDecoder decoder = new LoggingHeaderDecoder(new HeaderDecoder());
- decodeHeaders(pp, decoder);
-
- HttpRequestImpl parentReq = parent.request;
- int promisedStreamid = pp.getPromisedStream();
- if (promisedStreamid != nextPushStream) {
- resetStream(promisedStreamid, ResetFrame.PROTOCOL_ERROR);
- return;
- } else {
- nextPushStream += 2;
- }
-
- HttpHeadersImpl headers = decoder.headers();
- HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest(parentReq, headers);
- Exchange<T> pushExch = new Exchange<>(pushReq, parent.exchange.multi);
- Stream.PushedStream<T> pushStream = createPushStream(parent, pushExch);
- pushExch.exchImpl = pushStream;
- pushStream.registerStream(promisedStreamid);
- parent.incoming_pushPromise(pushReq, pushStream);
- }
-
- private void handleConnectionFrame(Http2Frame frame)
- throws IOException
- {
- switch (frame.type()) {
- case SettingsFrame.TYPE:
- handleSettings((SettingsFrame)frame);
- break;
- case PingFrame.TYPE:
- handlePing((PingFrame)frame);
- break;
- case GoAwayFrame.TYPE:
- handleGoAway((GoAwayFrame)frame);
- break;
- case WindowUpdateFrame.TYPE:
- handleWindowUpdate((WindowUpdateFrame)frame);
- break;
- default:
- protocolError(ErrorFrame.PROTOCOL_ERROR);
- }
- }
-
- void resetStream(int streamid, int code) throws IOException {
- Log.logError(
- "Resetting stream {0,number,integer} with error code {1,number,integer}",
- streamid, code);
- ResetFrame frame = new ResetFrame(streamid, code);
- sendFrame(frame);
- closeStream(streamid);
- }
-
- void closeStream(int streamid) {
- debug.log(Level.DEBUG, "Closed stream %d", streamid);
- Stream<?> s = streams.remove(streamid);
- if (s != null) {
- // decrement the reference count on the HttpClientImpl
- // to allow the SelectorManager thread to exit if no
- // other operation is pending and the facade is no
- // longer referenced.
- client().unreference();
- }
- // ## Remove s != null. It is a hack for delayed cancellation,reset
- if (s != null && !(s instanceof Stream.PushedStream)) {
- // Since PushStreams have no request body, then they have no
- // corresponding entry in the window controller.
- windowController.removeStream(streamid);
- }
- if (singleStream() && streams.isEmpty()) {
- // should be only 1 stream, but there might be more if server push
- close();
- }
- }
-
- /**
- * Increments this connection's send Window by the amount in the given frame.
- */
- private void handleWindowUpdate(WindowUpdateFrame f)
- throws IOException
- {
- int amount = f.getUpdate();
- if (amount <= 0) {
- // ## temporarily disable to workaround a bug in Jetty where it
- // ## sends Window updates with a 0 update value.
- //protocolError(ErrorFrame.PROTOCOL_ERROR);
- } else {
- boolean success = windowController.increaseConnectionWindow(amount);
- if (!success) {
- protocolError(ErrorFrame.FLOW_CONTROL_ERROR); // overflow
- }
- }
- }
-
- private void protocolError(int errorCode)
- throws IOException
- {
- protocolError(errorCode, null);
- }
-
- private void protocolError(int errorCode, String msg)
- throws IOException
- {
- GoAwayFrame frame = new GoAwayFrame(0, errorCode);
- sendFrame(frame);
- shutdown(new IOException("protocol error" + (msg == null?"":(": " + msg))));
- }
-
- private void handleSettings(SettingsFrame frame)
- throws IOException
- {
- assert frame.streamid() == 0;
- if (!frame.getFlag(SettingsFrame.ACK)) {
- int oldWindowSize = serverSettings.getParameter(INITIAL_WINDOW_SIZE);
- int newWindowSize = frame.getParameter(INITIAL_WINDOW_SIZE);
- int diff = newWindowSize - oldWindowSize;
- if (diff != 0) {
- windowController.adjustActiveStreams(diff);
- }
- serverSettings = frame;
- sendFrame(new SettingsFrame(SettingsFrame.ACK));
- }
- }
-
- private void handlePing(PingFrame frame)
- throws IOException
- {
- frame.setFlag(PingFrame.ACK);
- sendUnorderedFrame(frame);
- }
-
- private void handleGoAway(GoAwayFrame frame)
- throws IOException
- {
- shutdown(new IOException(
- String.valueOf(connection.channel().getLocalAddress())
- +": GOAWAY received"));
- }
-
- /**
- * Max frame size we are allowed to send
- */
- public int getMaxSendFrameSize() {
- int param = serverSettings.getParameter(MAX_FRAME_SIZE);
- if (param == -1) {
- param = DEFAULT_FRAME_SIZE;
- }
- return param;
- }
-
- /**
- * Max frame size we will receive
- */
- public int getMaxReceiveFrameSize() {
- return clientSettings.getParameter(MAX_FRAME_SIZE);
- }
-
- private static final String CLIENT_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
-
- private static final byte[] PREFACE_BYTES =
- CLIENT_PREFACE.getBytes(StandardCharsets.ISO_8859_1);
-
- /**
- * Sends Connection preface and Settings frame with current preferred
- * values
- */
- private void sendConnectionPreface() throws IOException {
- Log.logTrace("{0}: start sending connection preface to {1}",
- connection.channel().getLocalAddress(),
- connection.address());
- SettingsFrame sf = new SettingsFrame(clientSettings);
- int initialWindowSize = sf.getParameter(INITIAL_WINDOW_SIZE);
- ByteBuffer buf = framesEncoder.encodeConnectionPreface(PREFACE_BYTES, sf);
- Log.logFrames(sf, "OUT");
- // send preface bytes and SettingsFrame together
- HttpPublisher publisher = publisher();
- publisher.enqueue(List.of(buf));
- publisher.signalEnqueued();
- // mark preface sent.
- framesController.markPrefaceSent();
- Log.logTrace("PREFACE_BYTES sent");
- Log.logTrace("Settings Frame sent");
-
- // send a Window update for the receive buffer we are using
- // minus the initial 64 K specified in protocol
- final int len = windowUpdater.initialWindowSize - initialWindowSize;
- if (len > 0) {
- windowUpdater.sendWindowUpdate(len);
- }
- // there will be an ACK to the windows update - which should
- // cause any pending data stored before the preface was sent to be
- // flushed (see PrefaceController).
- Log.logTrace("finished sending connection preface");
- debug.log(Level.DEBUG, "Triggering processing of buffered data"
- + " after sending connection preface");
- subscriber.onNext(List.of(EMPTY_TRIGGER));
- }
-
- /**
- * Returns an existing Stream with given id, or null if doesn't exist
- */
- @SuppressWarnings("unchecked")
- <T> Stream<T> getStream(int streamid) {
- return (Stream<T>)streams.get(streamid);
- }
-
- /**
- * Creates Stream with given id.
- */
- final <T> Stream<T> createStream(Exchange<T> exchange) {
- Stream<T> stream = new Stream<>(this, exchange, windowController);
- return stream;
- }
-
- <T> Stream.PushedStream<T> createPushStream(Stream<T> parent, Exchange<T> pushEx) {
- PushGroup<T> pg = parent.exchange.getPushGroup();
- return new Stream.PushedStream<>(pg, this, pushEx);
- }
-
- <T> void putStream(Stream<T> stream, int streamid) {
- // increment the reference count on the HttpClientImpl
- // to prevent the SelectorManager thread from exiting until
- // the stream is closed.
- client().reference();
- streams.put(streamid, stream);
- }
-
- /**
- * Encode the headers into a List<ByteBuffer> and then create HEADERS
- * and CONTINUATION frames from the list and return the List<Http2Frame>.
- */
- private List<HeaderFrame> encodeHeaders(OutgoingHeaders<Stream<?>> frame) {
- List<ByteBuffer> buffers = encodeHeadersImpl(
- getMaxSendFrameSize(),
- frame.getAttachment().getRequestPseudoHeaders(),
- frame.getUserHeaders(),
- frame.getSystemHeaders());
-
- List<HeaderFrame> frames = new ArrayList<>(buffers.size());
- Iterator<ByteBuffer> bufIterator = buffers.iterator();
- HeaderFrame oframe = new HeadersFrame(frame.streamid(), frame.getFlags(), bufIterator.next());
- frames.add(oframe);
- while(bufIterator.hasNext()) {
- oframe = new ContinuationFrame(frame.streamid(), bufIterator.next());
- frames.add(oframe);
- }
- oframe.setFlag(HeaderFrame.END_HEADERS);
- return frames;
- }
-
- // Dedicated cache for headers encoding ByteBuffer.
- // There can be no concurrent access to this buffer as all access to this buffer
- // and its content happen within a single critical code block section protected
- // by the sendLock. / (see sendFrame())
- // private final ByteBufferPool headerEncodingPool = new ByteBufferPool();
-
- private ByteBuffer getHeaderBuffer(int maxFrameSize) {
- ByteBuffer buf = ByteBuffer.allocate(maxFrameSize);
- buf.limit(maxFrameSize);
- return buf;
- }
-
- /*
- * Encodes all the headers from the given HttpHeaders into the given List
- * of buffers.
- *
- * From https://tools.ietf.org/html/rfc7540#section-8.1.2 :
- *
- * ...Just as in HTTP/1.x, header field names are strings of ASCII
- * characters that are compared in a case-insensitive fashion. However,
- * header field names MUST be converted to lowercase prior to their
- * encoding in HTTP/2...
- */
- private List<ByteBuffer> encodeHeadersImpl(int maxFrameSize, HttpHeaders... headers) {
- ByteBuffer buffer = getHeaderBuffer(maxFrameSize);
- List<ByteBuffer> buffers = new ArrayList<>();
- for(HttpHeaders header : headers) {
- for (Map.Entry<String, List<String>> e : header.map().entrySet()) {
- String lKey = e.getKey().toLowerCase();
- List<String> values = e.getValue();
- for (String value : values) {
- hpackOut.header(lKey, value);
- while (!hpackOut.encode(buffer)) {
- buffer.flip();
- buffers.add(buffer);
- buffer = getHeaderBuffer(maxFrameSize);
- }
- }
- }
- }
- buffer.flip();
- buffers.add(buffer);
- return buffers;
- }
-
- private List<ByteBuffer> encodeHeaders(OutgoingHeaders<Stream<?>> oh, Stream<?> stream) {
- oh.streamid(stream.streamid);
- if (Log.headers()) {
- StringBuilder sb = new StringBuilder("HEADERS FRAME (stream=");
- sb.append(stream.streamid).append(")\n");
- Log.dumpHeaders(sb, " ", oh.getAttachment().getRequestPseudoHeaders());
- Log.dumpHeaders(sb, " ", oh.getSystemHeaders());
- Log.dumpHeaders(sb, " ", oh.getUserHeaders());
- Log.logHeaders(sb.toString());
- }
- List<HeaderFrame> frames = encodeHeaders(oh);
- return encodeFrames(frames);
- }
-
- private List<ByteBuffer> encodeFrames(List<HeaderFrame> frames) {
- if (Log.frames()) {
- frames.forEach(f -> Log.logFrames(f, "OUT"));
- }
- return framesEncoder.encodeFrames(frames);
- }
-
- private Stream<?> registerNewStream(OutgoingHeaders<Stream<?>> oh) {
- Stream<?> stream = oh.getAttachment();
- int streamid = nextstreamid;
- nextstreamid += 2;
- stream.registerStream(streamid);
- // set outgoing window here. This allows thread sending
- // body to proceed.
- windowController.registerStream(streamid, getInitialSendWindowSize());
- return stream;
- }
-
- private final Object sendlock = new Object();
-
- void sendFrame(Http2Frame frame) {
- try {
- HttpPublisher publisher = publisher();
- synchronized (sendlock) {
- if (frame instanceof OutgoingHeaders) {
- @SuppressWarnings("unchecked")
- OutgoingHeaders<Stream<?>> oh = (OutgoingHeaders<Stream<?>>) frame;
- Stream<?> stream = registerNewStream(oh);
- // provide protection from inserting unordered frames between Headers and Continuation
- publisher.enqueue(encodeHeaders(oh, stream));
- } else {
- publisher.enqueue(encodeFrame(frame));
- }
- }
- publisher.signalEnqueued();
- } catch (IOException e) {
- if (!closed) {
- Log.logError(e);
- shutdown(e);
- }
- }
- }
-
- private List<ByteBuffer> encodeFrame(Http2Frame frame) {
- Log.logFrames(frame, "OUT");
- return framesEncoder.encodeFrame(frame);
- }
-
- void sendDataFrame(DataFrame frame) {
- try {
- HttpPublisher publisher = publisher();
- publisher.enqueue(encodeFrame(frame));
- publisher.signalEnqueued();
- } catch (IOException e) {
- if (!closed) {
- Log.logError(e);
- shutdown(e);
- }
- }
- }
-
- /*
- * Direct call of the method bypasses synchronization on "sendlock" and
- * allowed only of control frames: WindowUpdateFrame, PingFrame and etc.
- * prohibited for such frames as DataFrame, HeadersFrame, ContinuationFrame.
- */
- void sendUnorderedFrame(Http2Frame frame) {
- try {
- HttpPublisher publisher = publisher();
- publisher.enqueueUnordered(encodeFrame(frame));
- publisher.signalEnqueued();
- } catch (IOException e) {
- if (!closed) {
- Log.logError(e);
- shutdown(e);
- }
- }
- }
-
- /**
- * A simple tube subscriber for reading from the connection flow.
- */
- final class Http2TubeSubscriber implements TubeSubscriber {
- volatile Flow.Subscription subscription;
- volatile boolean completed;
- volatile boolean dropped;
- volatile Throwable error;
- final ConcurrentLinkedQueue<ByteBuffer> queue
- = new ConcurrentLinkedQueue<>();
- final SequentialScheduler scheduler =
- SequentialScheduler.synchronizedScheduler(this::processQueue);
-
- final void processQueue() {
- try {
- while (!queue.isEmpty() && !scheduler.isStopped()) {
- ByteBuffer buffer = queue.poll();
- debug.log(Level.DEBUG,
- "sending %d to Http2Connection.asyncReceive",
- buffer.remaining());
- asyncReceive(buffer);
- }
- } catch (Throwable t) {
- Throwable x = error;
- if (x == null) error = t;
- } finally {
- Throwable x = error;
- if (x != null) {
- debug.log(Level.DEBUG, "Stopping scheduler", x);
- scheduler.stop();
- Http2Connection.this.shutdown(x);
- }
- }
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- // supports being called multiple time.
- // doesn't cancel the previous subscription, since that is
- // most probably the same as the new subscription.
- assert this.subscription == null || dropped == false;
- this.subscription = subscription;
- dropped = false;
- // TODO FIXME: request(1) should be done by the delegate.
- if (!completed) {
- debug.log(Level.DEBUG, "onSubscribe: requesting Long.MAX_VALUE for reading");
- subscription.request(Long.MAX_VALUE);
- } else {
- debug.log(Level.DEBUG, "onSubscribe: already completed");
- }
- }
-
- @Override
- public void onNext(List<ByteBuffer> item) {
- debug.log(Level.DEBUG, () -> "onNext: got " + Utils.remaining(item)
- + " bytes in " + item.size() + " buffers");
- queue.addAll(item);
- scheduler.runOrSchedule(client().theExecutor());
- }
-
- @Override
- public void onError(Throwable throwable) {
- debug.log(Level.DEBUG, () -> "onError: " + throwable);
- error = throwable;
- completed = true;
- scheduler.runOrSchedule(client().theExecutor());
- }
-
- @Override
- public void onComplete() {
- debug.log(Level.DEBUG, "EOF");
- error = new EOFException("EOF reached while reading");
- completed = true;
- scheduler.runOrSchedule(client().theExecutor());
- }
-
- @Override
- public void dropSubscription() {
- debug.log(Level.DEBUG, "dropSubscription");
- // we could probably set subscription to null here...
- // then we might not need the 'dropped' boolean?
- dropped = true;
- }
- }
-
- @Override
- public final String toString() {
- return dbgString();
- }
-
- final String dbgString() {
- return "Http2Connection("
- + connection.getConnectionFlow() + ")";
- }
-
- final class LoggingHeaderDecoder extends HeaderDecoder {
-
- private final HeaderDecoder delegate;
- private final System.Logger debugHpack =
- Utils.getHpackLogger(this::dbgString, DEBUG_HPACK);
-
- LoggingHeaderDecoder(HeaderDecoder delegate) {
- this.delegate = delegate;
- }
-
- String dbgString() {
- return Http2Connection.this.dbgString() + "/LoggingHeaderDecoder";
- }
-
- @Override
- public void onDecoded(CharSequence name, CharSequence value) {
- delegate.onDecoded(name, value);
- }
-
- @Override
- public void onIndexed(int index,
- CharSequence name,
- CharSequence value) {
- debugHpack.log(Level.DEBUG, "onIndexed(%s, %s, %s)%n",
- index, name, value);
- delegate.onIndexed(index, name, value);
- }
-
- @Override
- public void onLiteral(int index,
- CharSequence name,
- CharSequence value,
- boolean valueHuffman) {
- debugHpack.log(Level.DEBUG, "onLiteral(%s, %s, %s, %s)%n",
- index, name, value, valueHuffman);
- delegate.onLiteral(index, name, value, valueHuffman);
- }
-
- @Override
- public void onLiteral(CharSequence name,
- boolean nameHuffman,
- CharSequence value,
- boolean valueHuffman) {
- debugHpack.log(Level.DEBUG, "onLiteral(%s, %s, %s, %s)%n",
- name, nameHuffman, value, valueHuffman);
- delegate.onLiteral(name, nameHuffman, value, valueHuffman);
- }
-
- @Override
- public void onLiteralNeverIndexed(int index,
- CharSequence name,
- CharSequence value,
- boolean valueHuffman) {
- debugHpack.log(Level.DEBUG, "onLiteralNeverIndexed(%s, %s, %s, %s)%n",
- index, name, value, valueHuffman);
- delegate.onLiteralNeverIndexed(index, name, value, valueHuffman);
- }
-
- @Override
- public void onLiteralNeverIndexed(CharSequence name,
- boolean nameHuffman,
- CharSequence value,
- boolean valueHuffman) {
- debugHpack.log(Level.DEBUG, "onLiteralNeverIndexed(%s, %s, %s, %s)%n",
- name, nameHuffman, value, valueHuffman);
- delegate.onLiteralNeverIndexed(name, nameHuffman, value, valueHuffman);
- }
-
- @Override
- public void onLiteralWithIndexing(int index,
- CharSequence name,
- CharSequence value,
- boolean valueHuffman) {
- debugHpack.log(Level.DEBUG, "onLiteralWithIndexing(%s, %s, %s, %s)%n",
- index, name, value, valueHuffman);
- delegate.onLiteralWithIndexing(index, name, value, valueHuffman);
- }
-
- @Override
- public void onLiteralWithIndexing(CharSequence name,
- boolean nameHuffman,
- CharSequence value,
- boolean valueHuffman) {
- debugHpack.log(Level.DEBUG, "onLiteralWithIndexing(%s, %s, %s, %s)%n",
- name, nameHuffman, value, valueHuffman);
- delegate.onLiteralWithIndexing(name, nameHuffman, value, valueHuffman);
- }
-
- @Override
- public void onSizeUpdate(int capacity) {
- debugHpack.log(Level.DEBUG, "onSizeUpdate(%s)%n", capacity);
- delegate.onSizeUpdate(capacity);
- }
-
- @Override
- HttpHeadersImpl headers() {
- return delegate.headers();
- }
- }
-
- static class HeaderDecoder implements DecodingCallback {
- HttpHeadersImpl headers;
-
- HeaderDecoder() {
- this.headers = new HttpHeadersImpl();
- }
-
- @Override
- public void onDecoded(CharSequence name, CharSequence value) {
- headers.addHeader(name.toString(), value.toString());
- }
-
- HttpHeadersImpl headers() {
- return headers;
- }
- }
-
- static final class ConnectionWindowUpdateSender extends WindowUpdateSender {
-
- final int initialWindowSize;
- public ConnectionWindowUpdateSender(Http2Connection connection,
- int initialWindowSize) {
- super(connection, initialWindowSize);
- this.initialWindowSize = initialWindowSize;
- }
-
- @Override
- int getStreamId() {
- return 0;
- }
- }
-
- /**
- * Thrown when https handshake negotiates http/1.1 alpn instead of h2
- */
- static final class ALPNException extends IOException {
- private static final long serialVersionUID = 0L;
- final transient AbstractAsyncSSLConnection connection;
-
- ALPNException(String msg, AbstractAsyncSSLConnection connection) {
- super(msg);
- this.connection = connection;
- }
-
- AbstractAsyncSSLConnection getConnection() {
- return connection;
- }
- }
-}
--- a/src/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);
- }
-}
--- 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> cookieHandler() {
- return impl.cookieHandler();
- }
-
- @Override
- public Redirect followRedirects() {
- return impl.followRedirects();
- }
-
- @Override
- public Optional<ProxySelector> proxy() {
- return impl.proxy();
- }
-
- @Override
- public SSLContext sslContext() {
- return impl.sslContext();
- }
-
- @Override
- public SSLParameters sslParameters() {
- return impl.sslParameters();
- }
-
- @Override
- public Optional<Authenticator> authenticator() {
- return impl.authenticator();
- }
-
- @Override
- public HttpClient.Version version() {
- return impl.version();
- }
-
- @Override
- public Optional<Executor> executor() {
- return impl.executor();
- }
-
- @Override
- public <T> HttpResponse<T>
- send(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)
- throws IOException, InterruptedException
- {
- try {
- return impl.send(req, responseBodyHandler);
- } finally {
- Reference.reachabilityFence(this);
- }
- }
-
- @Override
- public <T> CompletableFuture<HttpResponse<T>>
- sendAsync(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler) {
- try {
- return impl.sendAsync(req, responseBodyHandler);
- } finally {
- Reference.reachabilityFence(this);
- }
- }
-
- @Override
- public <T> CompletableFuture<HttpResponse<T>>
- sendAsync(HttpRequest req,
- BodyHandler<T> responseBodyHandler,
- PushPromiseHandler<T> pushPromiseHandler){
- try {
- return impl.sendAsync(req, responseBodyHandler, pushPromiseHandler);
- } finally {
- Reference.reachabilityFence(this);
- }
- }
-
- @Override
- public WebSocket.Builder newWebSocketBuilder() {
- try {
- return impl.newWebSocketBuilder();
- } finally {
- Reference.reachabilityFence(this);
- }
- }
-
- @Override
- public String toString() {
- // Used by tests to get the client's id.
- return impl.toString();
- }
-}
--- a/src/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<ProxySelector> userProxySelector;
- private final ProxySelector proxySelector;
- private final Authenticator authenticator;
- private final Version version;
- private final ConnectionPool connections;
- private final Executor executor;
- private final boolean isDefaultExecutor;
- // Security parameters
- private final SSLContext sslContext;
- private final SSLParameters sslParams;
- private final SelectorManager selmgr;
- private final FilterFactory filters;
- private final Http2ClientImpl client2;
- private final long id;
- private final String dbgTag;
-
- // This reference is used to keep track of the facade HttpClient
- // that was returned to the application code.
- // It makes it possible to know when the application no longer
- // holds any reference to the HttpClient.
- // Unfortunately, this information is not enough to know when
- // to exit the SelectorManager thread. Because of the asynchronous
- // nature of the API, we also need to wait until all pending operations
- // have completed.
- private final WeakReference<HttpClientFacade> facadeRef;
-
- // This counter keeps track of the number of operations pending
- // on the HttpClient. The SelectorManager thread will wait
- // until there are no longer any pending operations and the
- // facadeRef is cleared before exiting.
- //
- // The pendingOperationCount is incremented every time a send/sendAsync
- // operation is invoked on the HttpClient, and is decremented when
- // the HttpResponse<T> object is returned to the user.
- // However, at this point, the body may not have been fully read yet.
- // This is the case when the response T is implemented as a streaming
- // subscriber (such as an InputStream).
- //
- // To take care of this issue the pendingOperationCount will additionally
- // be incremented/decremented in the following cases:
- //
- // 1. For HTTP/2 it is incremented when a stream is added to the
- // Http2Connection streams map, and decreased when the stream is removed
- // from the map. This should also take care of push promises.
- // 2. For WebSocket the count is increased when creating a
- // DetachedConnectionChannel for the socket, and decreased
- // when the the channel is closed.
- // In addition, the HttpClient facade is passed to the WebSocket builder,
- // (instead of the client implementation delegate).
- // 3. For HTTP/1.1 the count is incremented before starting to parse the body
- // response, and decremented when the parser has reached the end of the
- // response body flow.
- //
- // This should ensure that the selector manager thread remains alive until
- // the response has been fully received or the web socket is closed.
- private final AtomicLong pendingOperationCount = new AtomicLong();
- private final AtomicLong pendingWebSocketCount = new AtomicLong();
- private final AtomicLong pendingHttpRequestCount = new AtomicLong();
-
- /** A Set of, deadline first, ordered timeout events. */
- private final TreeSet<TimeoutEvent> timeouts;
-
- /**
- * This is a bit tricky:
- * 1. an HttpClientFacade has a final HttpClientImpl field.
- * 2. an HttpClientImpl has a final WeakReference<HttpClientFacade> field,
- * where the referent is the facade created for that instance.
- * 3. We cannot just create the HttpClientFacade in the HttpClientImpl
- * constructor, because it would be only weakly referenced and could
- * be GC'ed before we can return it.
- * The solution is to use an instance of SingleFacadeFactory which will
- * allow the caller of new HttpClientImpl(...) to retrieve the facade
- * after the HttpClientImpl has been created.
- */
- private static final class SingleFacadeFactory {
- HttpClientFacade facade;
- HttpClientFacade createFacade(HttpClientImpl impl) {
- assert facade == null;
- return (facade = new HttpClientFacade(impl));
- }
- }
-
- static HttpClientFacade create(HttpClientBuilderImpl builder) {
- SingleFacadeFactory facadeFactory = new SingleFacadeFactory();
- HttpClientImpl impl = new HttpClientImpl(builder, facadeFactory);
- impl.start();
- assert facadeFactory.facade != null;
- assert impl.facadeRef.get() == facadeFactory.facade;
- return facadeFactory.facade;
- }
-
- private HttpClientImpl(HttpClientBuilderImpl builder,
- SingleFacadeFactory facadeFactory) {
- id = CLIENT_IDS.incrementAndGet();
- dbgTag = "HttpClientImpl(" + id +")";
- if (builder.sslContext == null) {
- try {
- sslContext = SSLContext.getDefault();
- } catch (NoSuchAlgorithmException ex) {
- throw new InternalError(ex);
- }
- } else {
- sslContext = builder.sslContext;
- }
- Executor ex = builder.executor;
- if (ex == null) {
- ex = Executors.newCachedThreadPool(new DefaultThreadFactory(id));
- isDefaultExecutor = true;
- } else {
- ex = builder.executor;
- isDefaultExecutor = false;
- }
- facadeRef = new WeakReference<>(facadeFactory.createFacade(this));
- client2 = new Http2ClientImpl(this);
- executor = ex;
- cookieHandler = builder.cookieHandler;
- followRedirects = builder.followRedirects == null ?
- Redirect.NEVER : builder.followRedirects;
- this.userProxySelector = Optional.ofNullable(builder.proxy);
- this.proxySelector = userProxySelector
- .orElseGet(HttpClientImpl::getDefaultProxySelector);
- debug.log(Level.DEBUG, "proxySelector is %s (user-supplied=%s)",
- this.proxySelector, userProxySelector.isPresent());
- authenticator = builder.authenticator;
- if (builder.version == null) {
- version = HttpClient.Version.HTTP_2;
- } else {
- version = builder.version;
- }
- if (builder.sslParams == null) {
- sslParams = getDefaultParams(sslContext);
- } else {
- sslParams = builder.sslParams;
- }
- connections = new ConnectionPool(id);
- connections.start();
- timeouts = new TreeSet<>();
- try {
- selmgr = new SelectorManager(this);
- } catch (IOException e) {
- // unlikely
- throw new InternalError(e);
- }
- selmgr.setDaemon(true);
- filters = new FilterFactory();
- initFilters();
- assert facadeRef.get() != null;
- }
-
- private void start() {
- selmgr.start();
- }
-
- // Called from the SelectorManager thread, just before exiting.
- // Clears the HTTP/1.1 and HTTP/2 cache, ensuring that the connections
- // that may be still lingering there are properly closed (and their
- // possibly still opened SocketChannel released).
- private void stop() {
- // Clears HTTP/1.1 cache and close its connections
- connections.stop();
- // Clears HTTP/2 cache and close its connections.
- client2.stop();
- }
-
- private static SSLParameters getDefaultParams(SSLContext ctx) {
- SSLParameters params = ctx.getSupportedSSLParameters();
- params.setProtocols(new String[]{"TLSv1.2"});
- return params;
- }
-
- private static ProxySelector getDefaultProxySelector() {
- PrivilegedAction<ProxySelector> action = ProxySelector::getDefault;
- return AccessController.doPrivileged(action);
- }
-
- // Returns the facade that was returned to the application code.
- // May be null if that facade is no longer referenced.
- final HttpClientFacade facade() {
- return facadeRef.get();
- }
-
- // Increments the pendingOperationCount.
- final long reference() {
- pendingHttpRequestCount.incrementAndGet();
- return pendingOperationCount.incrementAndGet();
- }
-
- // Decrements the pendingOperationCount.
- final long unreference() {
- final long count = pendingOperationCount.decrementAndGet();
- final long httpCount = pendingHttpRequestCount.decrementAndGet();
- final long webSocketCount = pendingWebSocketCount.get();
- if (count == 0 && facade() == null) {
- selmgr.wakeupSelector();
- }
- assert httpCount >= 0 : "count of HTTP operations < 0";
- assert webSocketCount >= 0 : "count of WS operations < 0";
- assert count >= 0 : "count of pending operations < 0";
- return count;
- }
-
- // Increments the pendingOperationCount.
- final long webSocketOpen() {
- pendingWebSocketCount.incrementAndGet();
- return pendingOperationCount.incrementAndGet();
- }
-
- // Decrements the pendingOperationCount.
- final long webSocketClose() {
- final long count = pendingOperationCount.decrementAndGet();
- final long webSocketCount = pendingWebSocketCount.decrementAndGet();
- final long httpCount = pendingHttpRequestCount.get();
- if (count == 0 && facade() == null) {
- selmgr.wakeupSelector();
- }
- assert httpCount >= 0 : "count of HTTP operations < 0";
- assert webSocketCount >= 0 : "count of WS operations < 0";
- assert count >= 0 : "count of pending operations < 0";
- return count;
- }
-
- // Returns the pendingOperationCount.
- final long referenceCount() {
- return pendingOperationCount.get();
- }
-
- // Called by the SelectorManager thread to figure out whether it's time
- // to terminate.
- final boolean isReferenced() {
- HttpClient facade = facade();
- return facade != null || referenceCount() > 0;
- }
-
- /**
- * Wait for activity on given exchange.
- * The following occurs in the SelectorManager thread.
- *
- * 1) add to selector
- * 2) If selector fires for this exchange then
- * call AsyncEvent.handle()
- *
- * If exchange needs to change interest ops, then call registerEvent() again.
- */
- void registerEvent(AsyncEvent exchange) throws IOException {
- selmgr.register(exchange);
- }
-
- /**
- * Only used from RawChannel to disconnect the channel from
- * the selector
- */
- void cancelRegistration(SocketChannel s) {
- selmgr.cancel(s);
- }
-
- /**
- * Allows an AsyncEvent to modify its interestOps.
- * @param event The modified event.
- */
- void eventUpdated(AsyncEvent event) throws ClosedChannelException {
- assert !(event instanceof AsyncTriggerEvent);
- selmgr.eventUpdated(event);
- }
-
- boolean isSelectorThread() {
- return Thread.currentThread() == selmgr;
- }
-
- Http2ClientImpl client2() {
- return client2;
- }
-
- private void debugCompleted(String tag, long startNanos, HttpRequest req) {
- if (debugelapsed.isLoggable(Level.DEBUG)) {
- debugelapsed.log(Level.DEBUG, () -> tag + " elapsed "
- + (System.nanoTime() - startNanos)/1000_000L
- + " millis for " + req.method()
- + " to " + req.uri());
- }
- }
-
- @Override
- public <T> HttpResponse<T>
- send(HttpRequest req, BodyHandler<T> responseHandler)
- throws IOException, InterruptedException
- {
- try {
- return sendAsync(req, responseHandler).get();
- } catch (ExecutionException e) {
- Throwable t = e.getCause();
- if (t instanceof Error)
- throw (Error)t;
- if (t instanceof RuntimeException)
- throw (RuntimeException)t;
- else if (t instanceof IOException)
- throw Utils.getIOException(t);
- else
- throw new InternalError("Unexpected exception", t);
- }
- }
-
- @Override
- public <T> CompletableFuture<HttpResponse<T>>
- sendAsync(HttpRequest userRequest, BodyHandler<T> responseHandler)
- {
- return sendAsync(userRequest, responseHandler, null);
- }
-
-
- @Override
- public <T> CompletableFuture<HttpResponse<T>>
- sendAsync(HttpRequest userRequest,
- BodyHandler<T> responseHandler,
- PushPromiseHandler<T> pushPromiseHandler)
- {
- AccessControlContext acc = null;
- if (System.getSecurityManager() != null)
- acc = AccessController.getContext();
-
- // Clone the, possibly untrusted, HttpRequest
- HttpRequestImpl requestImpl = new HttpRequestImpl(userRequest, proxySelector, acc);
- if (requestImpl.method().equals("CONNECT"))
- throw new IllegalArgumentException("Unsupported method CONNECT");
-
- long start = DEBUGELAPSED ? System.nanoTime() : 0;
- reference();
- try {
- debugelapsed.log(Level.DEBUG, "ClientImpl (async) send %s", userRequest);
-
- MultiExchange<T> mex = new MultiExchange<>(userRequest,
- requestImpl,
- this,
- responseHandler,
- pushPromiseHandler,
- acc);
- CompletableFuture<HttpResponse<T>> res =
- mex.responseAsync().whenComplete((b,t) -> unreference());
- if (DEBUGELAPSED) {
- res = res.whenComplete(
- (b,t) -> debugCompleted("ClientImpl (async)", start, userRequest));
- }
- // makes sure that any dependent actions happen in the executor
- if (acc != null) {
- res.whenCompleteAsync((r, t) -> { /* do nothing */},
- new PrivilegedExecutor(executor, acc));
- }
-
- return res;
- } catch(Throwable t) {
- unreference();
- debugCompleted("ClientImpl (async)", start, userRequest);
- throw t;
- }
- }
-
- // Main loop for this client's selector
- private final static class SelectorManager extends Thread {
-
- // For testing purposes we have an internal System property that
- // can control the frequency at which the selector manager will wake
- // up when there are no pending operations.
- // Increasing the frequency (shorter delays) might allow the selector
- // to observe that the facade is no longer referenced and might allow
- // the selector thread to terminate more timely - for when nothing is
- // ongoing it will only check for that condition every NODEADLINE ms.
- // To avoid misuse of the property, the delay that can be specified
- // is comprised between [MIN_NODEADLINE, MAX_NODEADLINE], and its default
- // value if unspecified (or <= 0) is DEF_NODEADLINE = 3000ms
- // The property is -Djdk.httpclient.internal.selector.timeout=<millis>
- private static final int MIN_NODEADLINE = 1000; // ms
- private static final int MAX_NODEADLINE = 1000 * 1200; // ms
- private static final int DEF_NODEADLINE = 3000; // ms
- private static final long NODEADLINE; // default is DEF_NODEADLINE ms
- static {
- // ensure NODEADLINE is initialized with some valid value.
- long deadline = Utils.getIntegerNetProperty(
- "jdk.httpclient.internal.selector.timeout",
- DEF_NODEADLINE); // millis
- if (deadline <= 0) deadline = DEF_NODEADLINE;
- deadline = Math.max(deadline, MIN_NODEADLINE);
- NODEADLINE = Math.min(deadline, MAX_NODEADLINE);
- }
-
- private final Selector selector;
- private volatile boolean closed;
- private final List<AsyncEvent> registrations;
- private final System.Logger debug;
- private final System.Logger debugtimeout;
- HttpClientImpl owner;
- ConnectionPool pool;
-
- SelectorManager(HttpClientImpl ref) throws IOException {
- super(null, null, "HttpClient-" + ref.id + "-SelectorManager", 0, false);
- owner = ref;
- debug = ref.debug;
- debugtimeout = ref.debugtimeout;
- pool = ref.connectionPool();
- registrations = new ArrayList<>();
- selector = Selector.open();
- }
-
- void eventUpdated(AsyncEvent e) throws ClosedChannelException {
- if (Thread.currentThread() == this) {
- SelectionKey key = e.channel().keyFor(selector);
- 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<Pair<AsyncEvent,IOException>> errorList = new ArrayList<>();
- List<AsyncEvent> readyList = new ArrayList<>();
- try {
- while (!Thread.currentThread().isInterrupted()) {
- synchronized (this) {
- assert errorList.isEmpty();
- assert readyList.isEmpty();
- for (AsyncEvent event : registrations) {
- if (event instanceof AsyncTriggerEvent) {
- readyList.add(event);
- continue;
- }
- SelectableChannel chan = event.channel();
- SelectionKey key = null;
- try {
- key = chan.keyFor(selector);
- SelectorAttachment sa;
- if (key == null || !key.isValid()) {
- if (key != null) {
- // key is canceled.
- // invoke selectNow() to purge it
- // before registering the new event.
- selector.selectNow();
- }
- sa = new SelectorAttachment(chan, selector);
- } else {
- sa = (SelectorAttachment) key.attachment();
- }
- // may throw IOE if channel closed: that's OK
- sa.register(event);
- if (!chan.isOpen()) {
- throw new IOException("Channel closed");
- }
- } catch (IOException e) {
- Log.logTrace("HttpClientImpl: " + e);
- debug.log(Level.DEBUG, () ->
- "Got " + e.getClass().getName()
- + " while handling"
- + " registration events");
- chan.close();
- // let the event abort deal with it
- errorList.add(new Pair<>(event, e));
- if (key != null) {
- key.cancel();
- selector.selectNow();
- }
- }
- }
- registrations.clear();
- selector.selectedKeys().clear();
- }
-
- for (AsyncEvent event : readyList) {
- assert event instanceof AsyncTriggerEvent;
- event.handle();
- }
- readyList.clear();
-
- for (Pair<AsyncEvent,IOException> error : errorList) {
- // an IOException was raised and the channel closed.
- handleEvent(error.first, error.second);
- }
- errorList.clear();
-
- // Check whether client is still alive, and if not,
- // gracefully stop this thread
- if (!owner.isReferenced()) {
- Log.logTrace("HttpClient no longer referenced. Exiting...");
- return;
- }
-
- // Timeouts will have milliseconds granularity. It is important
- // to handle them in a timely fashion.
- long nextTimeout = owner.purgeTimeoutsAndReturnNextDeadline();
- debugtimeout.log(Level.DEBUG, "next timeout: %d", nextTimeout);
-
- // Keep-alive have seconds granularity. It's not really an
- // issue if we keep connections linger a bit more in the keep
- // alive cache.
- long nextExpiry = pool.purgeExpiredConnectionsAndReturnNextDeadline();
- debugtimeout.log(Level.DEBUG, "next expired: %d", nextExpiry);
-
- assert nextTimeout >= 0;
- assert nextExpiry >= 0;
-
- // Don't wait for ever as it might prevent the thread to
- // stop gracefully. millis will be 0 if no deadline was found.
- if (nextTimeout <= 0) nextTimeout = NODEADLINE;
-
- // Clip nextExpiry at NODEADLINE limit. The default
- // keep alive is 1200 seconds (half an hour) - we don't
- // want to wait that long.
- if (nextExpiry <= 0) nextExpiry = NODEADLINE;
- else nextExpiry = Math.min(NODEADLINE, nextExpiry);
-
- // takes the least of the two.
- long millis = Math.min(nextExpiry, nextTimeout);
-
- debugtimeout.log(Level.DEBUG, "Next deadline is %d",
- (millis == 0 ? NODEADLINE : millis));
- //debugPrint(selector);
- int n = selector.select(millis == 0 ? NODEADLINE : millis);
- if (n == 0) {
- // Check whether client is still alive, and if not,
- // gracefully stop this thread
- if (!owner.isReferenced()) {
- Log.logTrace("HttpClient no longer referenced. Exiting...");
- return;
- }
- owner.purgeTimeoutsAndReturnNextDeadline();
- continue;
- }
- Set<SelectionKey> keys = selector.selectedKeys();
-
- assert errorList.isEmpty();
- for (SelectionKey key : keys) {
- SelectorAttachment sa = (SelectorAttachment) key.attachment();
- if (!key.isValid()) {
- IOException ex = sa.chan.isOpen()
- ? new IOException("Invalid key")
- : new ClosedChannelException();
- sa.pending.forEach(e -> errorList.add(new Pair<>(e,ex)));
- sa.pending.clear();
- continue;
- }
-
- int eventsOccurred;
- try {
- eventsOccurred = key.readyOps();
- } catch (CancelledKeyException ex) {
- IOException io = Utils.getIOException(ex);
- sa.pending.forEach(e -> errorList.add(new Pair<>(e,io)));
- sa.pending.clear();
- continue;
- }
- sa.events(eventsOccurred).forEach(readyList::add);
- sa.resetInterestOps(eventsOccurred);
- }
- selector.selectNow(); // complete cancellation
- selector.selectedKeys().clear();
-
- for (AsyncEvent event : readyList) {
- handleEvent(event, null); // will be delegated to executor
- }
- readyList.clear();
- errorList.forEach((p) -> handleEvent(p.first, p.second));
- errorList.clear();
- }
- } catch (Throwable e) {
- //e.printStackTrace();
- if (!closed) {
- // This terminates thread. So, better just print stack trace
- String err = Utils.stackTrace(e);
- Log.logError("HttpClientImpl: fatal error: " + err);
- }
- debug.log(Level.DEBUG, "shutting down", e);
- if (Utils.ASSERTIONSENABLED && !debug.isLoggable(Level.DEBUG)) {
- e.printStackTrace(System.err); // always print the stack
- }
- } finally {
- shutdown();
- }
- }
-
-// void debugPrint(Selector selector) {
-// System.err.println("Selector: debugprint start");
-// Set<SelectionKey> keys = selector.keys();
-// for (SelectionKey key : keys) {
-// SelectableChannel c = key.channel();
-// int ops = key.interestOps();
-// System.err.printf("selector chan:%s ops:%d\n", c, ops);
-// }
-// System.err.println("Selector: debugprint end");
-// }
-
- /** Handles the given event. The given ioe may be null. */
- void handleEvent(AsyncEvent event, IOException ioe) {
- if (closed || ioe != null) {
- event.abort(ioe);
- } else {
- event.handle();
- }
- }
- }
-
- /**
- * Tracks multiple user level registrations associated with one NIO
- * registration (SelectionKey). In this implementation, registrations
- * are one-off and when an event is posted the registration is cancelled
- * until explicitly registered again.
- *
- * <p> No external synchronization required as this class is only used
- * by the SelectorManager thread. One of these objects required per
- * connection.
- */
- private static class SelectorAttachment {
- private final SelectableChannel chan;
- private final Selector selector;
- private final Set<AsyncEvent> pending;
- private final static System.Logger debug =
- Utils.getDebugLogger("SelectorAttachment"::toString, DEBUG);
- private int interestOps;
-
- SelectorAttachment(SelectableChannel chan, Selector selector) {
- this.pending = new HashSet<>();
- this.chan = chan;
- this.selector = selector;
- }
-
- void register(AsyncEvent e) throws ClosedChannelException {
- int newOps = e.interestOps();
- boolean reRegister = (interestOps & newOps) != newOps;
- interestOps |= newOps;
- pending.add(e);
- if (reRegister) {
- // first time registration happens here also
- try {
- chan.register(selector, interestOps, this);
- } catch (CancelledKeyException x) {
- abortPending(x);
- }
- }
- }
-
- /**
- * Returns a Stream<AsyncEvents> containing only events that are
- * registered with the given {@code interestOps}.
- */
- Stream<AsyncEvent> events(int interestOps) {
- return pending.stream()
- .filter(ev -> (ev.interestOps() & interestOps) != 0);
- }
-
- /**
- * Removes any events with the given {@code interestOps}, and if no
- * events remaining, cancels the associated SelectionKey.
- */
- void resetInterestOps(int interestOps) {
- int newOps = 0;
-
- Iterator<AsyncEvent> itr = pending.iterator();
- while (itr.hasNext()) {
- AsyncEvent event = itr.next();
- int evops = event.interestOps();
- if (event.repeating()) {
- newOps |= evops;
- continue;
- }
- if ((evops & interestOps) != 0) {
- itr.remove();
- } else {
- newOps |= evops;
- }
- }
-
- this.interestOps = newOps;
- SelectionKey key = chan.keyFor(selector);
- if (newOps == 0 && pending.isEmpty()) {
- key.cancel();
- } else {
- try {
- key.interestOps(newOps);
- } catch (CancelledKeyException x) {
- // channel may have been closed
- debug.log(Level.DEBUG, "key cancelled for " + chan);
- abortPending(x);
- }
- }
- }
-
- void abortPending(Throwable x) {
- if (!pending.isEmpty()) {
- AsyncEvent[] evts = pending.toArray(new AsyncEvent[0]);
- pending.clear();
- IOException io = Utils.getIOException(x);
- for (AsyncEvent event : evts) {
- event.abort(io);
- }
- }
- }
- }
-
- /*package-private*/ SSLContext theSSLContext() {
- return sslContext;
- }
-
- @Override
- public SSLContext sslContext() {
- return sslContext;
- }
-
- @Override
- public SSLParameters sslParameters() {
- return Utils.copySSLParameters(sslParams);
- }
-
- @Override
- public Optional<Authenticator> authenticator() {
- return Optional.ofNullable(authenticator);
- }
-
- /*package-private*/ final Executor theExecutor() {
- return executor;
- }
-
- @Override
- public final Optional<Executor> executor() {
- return isDefaultExecutor ? Optional.empty() : Optional.of(executor);
- }
-
- ConnectionPool connectionPool() {
- return connections;
- }
-
- @Override
- public Redirect followRedirects() {
- return followRedirects;
- }
-
-
- @Override
- public Optional<CookieHandler> cookieHandler() {
- return Optional.ofNullable(cookieHandler);
- }
-
- @Override
- public Optional<ProxySelector> proxy() {
- return this.userProxySelector;
- }
-
- // Return the effective proxy that this client uses.
- ProxySelector proxySelector() {
- return proxySelector;
- }
-
- @Override
- public WebSocket.Builder newWebSocketBuilder() {
- // Make sure to pass the HttpClientFacade to the WebSocket builder.
- // This will ensure that the facade is not released before the
- // WebSocket has been created, at which point the pendingOperationCount
- // will have been incremented by the DetachedConnectionChannel
- // (see PlainHttpConnection.detachChannel())
- return new BuilderImpl(this.facade(), proxySelector);
- }
-
- @Override
- public Version version() {
- return version;
- }
-
- String dbgString() {
- return dbgTag;
- }
-
- @Override
- public String toString() {
- // Used by tests to get the client's id and compute the
- // name of the SelectorManager thread.
- return super.toString() + ("(" + id + ")");
- }
-
- private void initFilters() {
- addFilter(AuthenticationFilter.class);
- addFilter(RedirectFilter.class);
- if (this.cookieHandler != null) {
- addFilter(CookieFilter.class);
- }
- }
-
- private void addFilter(Class<? extends HeaderFilter> f) {
- filters.addFilter(f);
- }
-
- final List<HeaderFilter> filterChain() {
- return filters.getFilterChain();
- }
-
- // Timer controls.
- // Timers are implemented through timed Selector.select() calls.
-
- synchronized void registerTimer(TimeoutEvent event) {
- Log.logTrace("Registering timer {0}", event);
- timeouts.add(event);
- selmgr.wakeupSelector();
- }
-
- synchronized void cancelTimer(TimeoutEvent event) {
- Log.logTrace("Canceling timer {0}", event);
- timeouts.remove(event);
- }
-
- /**
- * Purges ( handles ) timer events that have passed their deadline, and
- * returns the amount of time, in milliseconds, until the next earliest
- * event. A return value of 0 means that there are no events.
- */
- private long purgeTimeoutsAndReturnNextDeadline() {
- long diff = 0L;
- List<TimeoutEvent> toHandle = null;
- int remaining = 0;
- // enter critical section to retrieve the timeout event to handle
- synchronized(this) {
- if (timeouts.isEmpty()) return 0L;
-
- Instant now = Instant.now();
- Iterator<TimeoutEvent> itr = timeouts.iterator();
- while (itr.hasNext()) {
- TimeoutEvent event = itr.next();
- diff = now.until(event.deadline(), ChronoUnit.MILLIS);
- if (diff <= 0) {
- itr.remove();
- toHandle = (toHandle == null) ? new ArrayList<>() : toHandle;
- toHandle.add(event);
- } else {
- break;
- }
- }
- remaining = timeouts.size();
- }
-
- // can be useful for debugging
- if (toHandle != null && Log.trace()) {
- Log.logTrace("purgeTimeoutsAndReturnNextDeadline: handling "
- + toHandle.size() + " events, "
- + "remaining " + remaining
- + ", next deadline: " + (diff < 0 ? 0L : diff));
- }
-
- // handle timeout events out of critical section
- if (toHandle != null) {
- Throwable failed = null;
- for (TimeoutEvent event : toHandle) {
- try {
- Log.logTrace("Firing timer {0}", event);
- event.handle();
- } catch (Error | RuntimeException e) {
- // Not expected. Handle remaining events then throw...
- // If e is an OOME or SOE it might simply trigger a new
- // error from here - but in this case there's not much we
- // could do anyway. Just let it flow...
- if (failed == null) failed = e;
- else failed.addSuppressed(e);
- Log.logTrace("Failed to handle event {0}: {1}", event, e);
- }
- }
- if (failed instanceof Error) throw (Error) failed;
- if (failed instanceof RuntimeException) throw (RuntimeException) failed;
- }
-
- // return time to wait until next event. 0L if there's no more events.
- return diff < 0 ? 0L : diff;
- }
-
- // used for the connection window
- int getReceiveBufferSize() {
- return Utils.getIntegerNetProperty(
- "jdk.httpclient.receiveBufferSize", 2 * 1024 * 1024
- );
- }
-}
--- a/src/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<CompletionStage<?>, Boolean> operations =
- new IdentityHashMap<>();
- void add(CompletionStage<?> cf) {
- synchronized(operations) {
- cf.whenComplete((r,t)-> remove(cf));
- operations.put(cf, Boolean.TRUE);
- }
- }
- boolean remove(CompletionStage<?> cf) {
- synchronized(operations) {
- return operations.remove(cf);
- }
- }
- }
-
- final void addTrailingOperation(CompletionStage<?> cf) {
- trailingOperations.add(cf);
- }
-
-// final void removeTrailingOperation(CompletableFuture<?> cf) {
-// trailingOperations.remove(cf);
-// }
-
- final HttpClientImpl client() {
- return client;
- }
-
- //public abstract void connect() throws IOException, InterruptedException;
-
- public abstract CompletableFuture<Void> connectAsync();
-
- /** Tells whether, or not, this connection is connected to its destination. */
- abstract boolean connected();
-
- /** Tells whether, or not, this connection is secure ( over SSL ) */
- abstract boolean isSecure();
-
- /** Tells whether, or not, this connection is proxied. */
- abstract boolean isProxied();
-
- /** Tells whether, or not, this connection is open. */
- final boolean isOpen() {
- return channel().isOpen() &&
- (connected() ? !getConnectionFlow().isFinished() : true);
- }
-
- interface HttpPublisher extends FlowTube.TubePublisher {
- void enqueue(List<ByteBuffer> buffers) throws IOException;
- void enqueueUnordered(List<ByteBuffer> buffers) throws IOException;
- void signalEnqueued() throws IOException;
- }
-
- /**
- * Returns the HTTP publisher associated with this connection. May be null
- * if invoked before connecting.
- */
- abstract HttpPublisher publisher();
-
- // HTTP/2 MUST use TLS version 1.2 or higher for HTTP/2 over TLS
- private static final Predicate<String> testRequiredHTTP2TLSVersion = proto ->
- proto.equals("TLSv1.2") || proto.equals("TLSv1.3");
-
- /**
- * Returns true if the given client's SSL parameter protocols contains at
- * least one TLS version that HTTP/2 requires.
- */
- private static final boolean hasRequiredHTTP2TLSVersion(HttpClient client) {
- String[] protos = client.sslParameters().getProtocols();
- if (protos != null) {
- return Arrays.stream(protos).filter(testRequiredHTTP2TLSVersion).findAny().isPresent();
- } else {
- return false;
- }
- }
-
- /**
- * Factory for retrieving HttpConnections. A connection can be retrieved
- * from the connection pool, or a new one created if none available.
- *
- * The given {@code addr} is the ultimate destination. Any proxies,
- * etc, are determined from the request. Returns a concrete instance which
- * is one of the following:
- * {@link PlainHttpConnection}
- * {@link PlainTunnelingConnection}
- *
- * The returned connection, if not from the connection pool, must have its,
- * connect() or connectAsync() method invoked, which ( when it completes
- * successfully ) renders the connection usable for requests.
- */
- public static HttpConnection getConnection(InetSocketAddress addr,
- HttpClientImpl client,
- HttpRequestImpl request,
- Version version) {
- HttpConnection c = null;
- InetSocketAddress proxy = request.proxy();
- if (proxy != null && proxy.isUnresolved()) {
- // The default proxy selector may select a proxy whose address is
- // unresolved. We must resolve the address before connecting to it.
- proxy = new InetSocketAddress(proxy.getHostString(), proxy.getPort());
- }
- boolean secure = request.secure();
- ConnectionPool pool = client.connectionPool();
-
- if (!secure) {
- c = pool.getConnection(false, addr, proxy);
- if (c != null && c.isOpen() /* may have been eof/closed when in the pool */) {
- final HttpConnection conn = c;
- DEBUG_LOGGER.log(Level.DEBUG, () -> conn.getConnectionFlow()
- + ": plain connection retrieved from HTTP/1.1 pool");
- return c;
- } else {
- return getPlainConnection(addr, proxy, request, client);
- }
- } else { // secure
- if (version != HTTP_2) { // only HTTP/1.1 connections are in the pool
- c = pool.getConnection(true, addr, proxy);
- }
- if (c != null && c.isOpen()) {
- final HttpConnection conn = c;
- DEBUG_LOGGER.log(Level.DEBUG, () -> conn.getConnectionFlow()
- + ": SSL connection retrieved from HTTP/1.1 pool");
- return c;
- } else {
- String[] alpn = null;
- if (version == HTTP_2 && hasRequiredHTTP2TLSVersion(client)) {
- alpn = new String[] { "h2", "http/1.1" };
- }
- return getSSLConnection(addr, proxy, alpn, request, client);
- }
- }
- }
-
- private static HttpConnection getSSLConnection(InetSocketAddress addr,
- InetSocketAddress proxy,
- String[] alpn,
- HttpRequestImpl request,
- HttpClientImpl client) {
- if (proxy != null)
- return new AsyncSSLTunnelConnection(addr, client, alpn, proxy,
- proxyTunnelHeaders(request));
- else
- return new AsyncSSLConnection(addr, client, alpn);
- }
-
- /**
- * This method is used to build a filter that will accept or
- * veto (header-name, value) tuple for transmission on the
- * wire.
- * The filter is applied to the headers when sending the headers
- * to the remote party.
- * Which tuple is accepted/vetoed depends on:
- * <pre>
- * - whether the connection is a tunnel connection
- * [talking to a server through a proxy tunnel]
- * - whether the method is CONNECT
- * [establishing a CONNECT tunnel through a proxy]
- * - whether the request is using a proxy
- * (and the connection is not a tunnel)
- * [talking to a server through a proxy]
- * - whether the request is a direct connection to
- * a server (no tunnel, no proxy).
- * </pre>
- * @param request
- * @return
- */
- BiPredicate<String,List<String>> headerFilter(HttpRequestImpl request) {
- if (isTunnel()) {
- // talking to a server through a proxy tunnel
- // don't send proxy-* headers to a plain server
- assert !request.isConnect();
- return Utils.NO_PROXY_HEADERS_FILTER;
- } else if (request.isConnect()) {
- // establishing a proxy tunnel
- // check for proxy tunnel disabled schemes
- // assert !this.isTunnel();
- assert request.proxy() == null;
- return Utils.PROXY_TUNNEL_FILTER;
- } else if (request.proxy() != null) {
- // talking to a server through a proxy (no tunnel)
- // check for proxy disabled schemes
- // assert !isTunnel() && !request.isConnect();
- return Utils.PROXY_FILTER;
- } else {
- // talking to a server directly (no tunnel, no proxy)
- // don't send proxy-* headers to a plain server
- // assert request.proxy() == null && !request.isConnect();
- return Utils.NO_PROXY_HEADERS_FILTER;
- }
- }
-
- // Composes a new immutable HttpHeaders that combines the
- // user and system header but only keeps those headers that
- // start with "proxy-"
- private static HttpHeaders proxyTunnelHeaders(HttpRequestImpl request) {
- Map<String, List<String>> combined = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
- combined.putAll(request.getSystemHeaders().map());
- combined.putAll(request.headers().map()); // let user override system
-
- // keep only proxy-* - and also strip authorization headers
- // for disabled schemes
- return ImmutableHeaders.of(combined, Utils.PROXY_TUNNEL_FILTER);
- }
-
- /* Returns either a plain HTTP connection or a plain tunnelling connection
- * for proxied WebSocket */
- private static HttpConnection getPlainConnection(InetSocketAddress addr,
- InetSocketAddress proxy,
- HttpRequestImpl request,
- HttpClientImpl client) {
- if (request.isWebSocket() && proxy != null)
- return new PlainTunnelingConnection(addr, proxy, client,
- proxyTunnelHeaders(request));
-
- if (proxy == null)
- return new PlainHttpConnection(addr, client);
- else
- return new PlainProxyConnection(proxy, client);
- }
-
- void closeOrReturnToCache(HttpHeaders hdrs) {
- if (hdrs == null) {
- // the connection was closed by server, eof
- close();
- return;
- }
- if (!isOpen()) {
- return;
- }
- HttpClientImpl client = client();
- if (client == null) {
- close();
- return;
- }
- ConnectionPool pool = client.connectionPool();
- boolean keepAlive = hdrs.firstValue("Connection")
- .map((s) -> !s.equalsIgnoreCase("close"))
- .orElse(true);
-
- if (keepAlive) {
- Log.logTrace("Returning connection to the pool: {0}", this);
- pool.returnToPool(this);
- } else {
- close();
- }
- }
-
- /* Tells whether or not this connection is a tunnel through a proxy */
- boolean isTunnel() { return false; }
-
- abstract SocketChannel channel();
-
- final InetSocketAddress address() {
- return address;
- }
-
- abstract ConnectionPool.CacheKey cacheKey();
-
- /**
- * Closes this connection, by returning the socket to its connection pool.
- */
- @Override
- public abstract void close();
-
- abstract void shutdownInput() throws IOException;
-
- abstract void shutdownOutput() throws IOException;
-
- // Support for WebSocket/RawChannelImpl which unfortunately
- // still depends on synchronous read/writes.
- // It should be removed when RawChannelImpl moves to using asynchronous APIs.
- abstract static class DetachedConnectionChannel implements Closeable {
- DetachedConnectionChannel() {}
- abstract SocketChannel channel();
- abstract long write(ByteBuffer[] buffers, int start, int number)
- throws IOException;
- abstract void shutdownInput() throws IOException;
- abstract void shutdownOutput() throws IOException;
- abstract ByteBuffer read() throws IOException;
- @Override
- public abstract void close();
- @Override
- public String toString() {
- return this.getClass().getSimpleName() + ": " + channel().toString();
- }
- }
-
- // Support for WebSocket/RawChannelImpl which unfortunately
- // still depends on synchronous read/writes.
- // It should be removed when RawChannelImpl moves to using asynchronous APIs.
- abstract DetachedConnectionChannel detachChannel();
-
- abstract FlowTube getConnectionFlow();
-
- /**
- * A publisher that makes it possible to publish (write)
- * ordered (normal priority) and unordered (high priority)
- * buffers downstream.
- */
- final class PlainHttpPublisher implements HttpPublisher {
- final Object reading;
- PlainHttpPublisher() {
- this(new Object());
- }
- PlainHttpPublisher(Object readingLock) {
- this.reading = readingLock;
- }
- final ConcurrentLinkedDeque<List<ByteBuffer>> queue = new ConcurrentLinkedDeque<>();
- volatile Flow.Subscriber<? super List<ByteBuffer>> subscriber;
- volatile HttpWriteSubscription subscription;
- final SequentialScheduler writeScheduler =
- new SequentialScheduler(this::flushTask);
- @Override
- public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
- synchronized (reading) {
- //assert this.subscription == null;
- //assert this.subscriber == null;
- if (subscription == null) {
- subscription = new HttpWriteSubscription();
- }
- this.subscriber = subscriber;
- }
- // TODO: should we do this in the flow?
- subscriber.onSubscribe(subscription);
- signal();
- }
-
- void flushTask(DeferredCompleter completer) {
- try {
- HttpWriteSubscription sub = subscription;
- if (sub != null) sub.flush();
- } finally {
- completer.complete();
- }
- }
-
- void signal() {
- writeScheduler.runOrSchedule();
- }
-
- final class HttpWriteSubscription implements Flow.Subscription {
- final Demand demand = new Demand();
-
- @Override
- public void request(long n) {
- if (n <= 0) throw new IllegalArgumentException("non-positive request");
- demand.increase(n);
- debug.log(Level.DEBUG, () -> "HttpPublisher: got request of "
- + n + " from "
- + getConnectionFlow());
- writeScheduler.runOrSchedule();
- }
-
- @Override
- public void cancel() {
- debug.log(Level.DEBUG, () -> "HttpPublisher: cancelled by "
- + getConnectionFlow());
- }
-
- void flush() {
- while (!queue.isEmpty() && demand.tryDecrement()) {
- List<ByteBuffer> elem = queue.poll();
- debug.log(Level.DEBUG, () -> "HttpPublisher: sending "
- + Utils.remaining(elem) + " bytes ("
- + elem.size() + " buffers) to "
- + getConnectionFlow());
- subscriber.onNext(elem);
- }
- }
- }
-
- @Override
- public void enqueue(List<ByteBuffer> buffers) throws IOException {
- queue.add(buffers);
- int bytes = buffers.stream().mapToInt(ByteBuffer::remaining).sum();
- debug.log(Level.DEBUG, "added %d bytes to the write queue", bytes);
- }
-
- @Override
- public void enqueueUnordered(List<ByteBuffer> buffers) throws IOException {
- // Unordered frames are sent before existing frames.
- int bytes = buffers.stream().mapToInt(ByteBuffer::remaining).sum();
- queue.addFirst(buffers);
- debug.log(Level.DEBUG, "inserted %d bytes in the write queue", bytes);
- }
-
- @Override
- public void signalEnqueued() throws IOException {
- debug.log(Level.DEBUG, "signalling the publisher of the write queue");
- signal();
- }
- }
-
- String dbgTag = null;
- final String dbgString() {
- FlowTube flow = getConnectionFlow();
- String tag = dbgTag;
- if (tag == null && flow != null) {
- dbgTag = tag = this.getClass().getSimpleName() + "(" + flow + ")";
- } else if (tag == null) {
- tag = this.getClass().getSimpleName() + "(?)";
- }
- return tag;
- }
-
- @Override
- public String toString() {
- return "HttpConnection: " + channel().toString();
- }
-}
--- a/src/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<HttpClient.Version> version;
- private Duration duration;
-
- public HttpRequestBuilderImpl(URI uri) {
- requireNonNull(uri, "uri must be non-null");
- checkURI(uri);
- this.uri = uri;
- this.userHeaders = new HttpHeadersImpl();
- this.method = "GET"; // default, as per spec
- this.version = Optional.empty();
- }
-
- public HttpRequestBuilderImpl() {
- this.userHeaders = new HttpHeadersImpl();
- this.method = "GET"; // default, as per spec
- this.version = Optional.empty();
- }
-
- @Override
- public HttpRequestBuilderImpl uri(URI uri) {
- requireNonNull(uri, "uri must be non-null");
- checkURI(uri);
- this.uri = uri;
- return this;
- }
-
- private static IllegalArgumentException newIAE(String message, Object... args) {
- return new IllegalArgumentException(format(message, args));
- }
-
- private static void checkURI(URI uri) {
- String scheme = uri.getScheme();
- if (scheme == null)
- throw newIAE("URI with undefined scheme");
- scheme = scheme.toLowerCase();
- if (!(scheme.equals("https") || scheme.equals("http"))) {
- throw newIAE("invalid URI scheme %s", scheme);
- }
- if (uri.getHost() == null) {
- throw newIAE("unsupported URI %s", uri);
- }
- }
-
- @Override
- public HttpRequestBuilderImpl copy() {
- HttpRequestBuilderImpl b = new HttpRequestBuilderImpl(this.uri);
- b.userHeaders = this.userHeaders.deepCopy();
- b.method = this.method;
- b.expectContinue = this.expectContinue;
- b.bodyPublisher = bodyPublisher;
- b.uri = uri;
- b.duration = duration;
- b.version = version;
- return b;
- }
-
- private void checkNameAndValue(String name, String value) {
- requireNonNull(name, "name");
- requireNonNull(value, "value");
- if (!isValidName(name)) {
- throw newIAE("invalid header name: \"%s\"", name);
- }
- if (!Utils.ALLOWED_HEADERS.test(name)) {
- throw newIAE("restricted header name: \"%s\"", name);
- }
- if (!isValidValue(value)) {
- throw newIAE("invalid header value: \"%s\"", value);
- }
- }
-
- @Override
- public HttpRequestBuilderImpl setHeader(String name, String value) {
- checkNameAndValue(name, value);
- userHeaders.setHeader(name, value);
- return this;
- }
-
- @Override
- public HttpRequestBuilderImpl header(String name, String value) {
- checkNameAndValue(name, value);
- userHeaders.addHeader(name, value);
- return this;
- }
-
- @Override
- public HttpRequestBuilderImpl headers(String... params) {
- requireNonNull(params);
- if (params.length == 0 || params.length % 2 != 0) {
- throw newIAE("wrong number, %d, of parameters", params.length);
- }
- for (int i = 0; i < params.length; i += 2) {
- String name = params[i];
- String value = params[i + 1];
- header(name, value);
- }
- return this;
- }
-
- @Override
- public HttpRequestBuilderImpl expectContinue(boolean enable) {
- expectContinue = enable;
- return this;
- }
-
- @Override
- public HttpRequestBuilderImpl version(HttpClient.Version version) {
- requireNonNull(version);
- this.version = Optional.of(version);
- return this;
- }
-
- HttpHeadersImpl headers() { return userHeaders; }
-
- URI uri() { return uri; }
-
- String method() { return method; }
-
- boolean expectContinue() { return expectContinue; }
-
- BodyPublisher bodyPublisher() { return bodyPublisher; }
-
- Optional<HttpClient.Version> version() { return version; }
-
- @Override
- public HttpRequest.Builder GET() {
- return method0("GET", null);
- }
-
- @Override
- public HttpRequest.Builder POST(BodyPublisher body) {
- return method0("POST", requireNonNull(body));
- }
-
- @Override
- public HttpRequest.Builder DELETE(BodyPublisher body) {
- return method0("DELETE", requireNonNull(body));
- }
-
- @Override
- public HttpRequest.Builder PUT(BodyPublisher body) {
- return method0("PUT", requireNonNull(body));
- }
-
- @Override
- public HttpRequest.Builder method(String method, BodyPublisher body) {
- requireNonNull(method);
- if (method.equals(""))
- throw newIAE("illegal method <empty string>");
- if (method.equals("CONNECT"))
- throw newIAE("method CONNECT is not supported");
- return method0(method, requireNonNull(body));
- }
-
- private HttpRequest.Builder method0(String method, BodyPublisher body) {
- assert method != null;
- assert !method.equals("GET") ? body != null : true;
- assert !method.equals("");
- this.method = method;
- this.bodyPublisher = body;
- return this;
- }
-
- @Override
- public HttpRequest build() {
- if (uri == null)
- throw new IllegalStateException("uri is null");
- assert method != null;
- return new HttpRequestImpl(this);
- }
-
- @Override
- public HttpRequest.Builder timeout(Duration duration) {
- requireNonNull(duration);
- if (duration.isNegative() || Duration.ZERO.equals(duration))
- throw new IllegalArgumentException("Invalid duration: " + duration);
- this.duration = duration;
- return this;
- }
-
- Duration timeout() { return duration; }
-
-}
--- a/src/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<HttpClient.Version> version;
-
- private static String userAgent() {
- PrivilegedAction<String> pa = () -> System.getProperty("java.version");
- String version = AccessController.doPrivileged(pa);
- return "Java-http-client/" + version;
- }
-
- /** The value of the User-Agent header for all requests sent by the client. */
- public static final String USER_AGENT = userAgent();
-
- /**
- * Creates an HttpRequestImpl from the given builder.
- */
- public HttpRequestImpl(HttpRequestBuilderImpl builder) {
- String method = builder.method();
- this.method = method == null ? "GET" : method;
- this.userHeaders = ImmutableHeaders.of(builder.headers().map(), ALLOWED_HEADERS);
- this.systemHeaders = new HttpHeadersImpl();
- this.uri = builder.uri();
- assert uri != null;
- this.proxy = null;
- this.expectContinue = builder.expectContinue();
- this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
- this.requestPublisher = builder.bodyPublisher(); // may be null
- this.timeout = builder.timeout();
- this.version = builder.version();
- this.authority = null;
- }
-
- /**
- * Creates an HttpRequestImpl from the given request.
- */
- public HttpRequestImpl(HttpRequest request, ProxySelector ps, AccessControlContext acc) {
- String method = request.method();
- this.method = method == null ? "GET" : method;
- this.userHeaders = request.headers();
- if (request instanceof HttpRequestImpl) {
- this.systemHeaders = ((HttpRequestImpl) request).systemHeaders;
- this.isWebSocket = ((HttpRequestImpl) request).isWebSocket;
- } else {
- this.systemHeaders = new HttpHeadersImpl();
- }
- this.systemHeaders.setHeader("User-Agent", USER_AGENT);
- this.uri = request.uri();
- if (isWebSocket) {
- // WebSocket determines and sets the proxy itself
- this.proxy = ((HttpRequestImpl) request).proxy;
- } else {
- if (ps != null)
- this.proxy = retrieveProxy(ps, uri);
- else
- this.proxy = null;
- }
- this.expectContinue = request.expectContinue();
- this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
- this.requestPublisher = request.bodyPublisher().orElse(null);
- if (acc != null && requestPublisher instanceof RequestPublishers.FilePublisher) {
- // Restricts the file publisher with the senders ACC, if any
- ((RequestPublishers.FilePublisher)requestPublisher).setAccessControlContext(acc);
- }
- this.timeout = request.timeout().orElse(null);
- this.version = request.version();
- this.authority = null;
- }
-
- /** Creates a HttpRequestImpl using fields of an existing request impl. */
- public HttpRequestImpl(URI uri,
- String method,
- HttpRequestImpl other) {
- this.method = method == null? "GET" : method;
- this.userHeaders = other.userHeaders;
- this.isWebSocket = other.isWebSocket;
- this.systemHeaders = other.systemHeaders;
- this.uri = uri;
- this.proxy = other.proxy;
- this.expectContinue = other.expectContinue;
- this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
- this.requestPublisher = other.requestPublisher; // may be null
- this.acc = other.acc;
- this.timeout = other.timeout;
- this.version = other.version();
- this.authority = null;
- }
-
- /* used for creating CONNECT requests */
- HttpRequestImpl(String method, InetSocketAddress authority, HttpHeaders headers) {
- // TODO: isWebSocket flag is not specified, but the assumption is that
- // such a request will never be made on a connection that will be returned
- // to the connection pool (we might need to revisit this constructor later)
- assert "CONNECT".equalsIgnoreCase(method);
- this.method = method;
- this.systemHeaders = new HttpHeadersImpl();
- this.userHeaders = ImmutableHeaders.of(headers);
- this.uri = URI.create("socket://" + authority.getHostString() + ":"
- + Integer.toString(authority.getPort()) + "/");
- this.proxy = null;
- this.requestPublisher = null;
- this.authority = authority;
- this.secure = false;
- this.expectContinue = false;
- this.timeout = null;
- // The CONNECT request sent for tunneling is only used in two cases:
- // 1. websocket, which only supports HTTP/1.1
- // 2. SSL tunneling through a HTTP/1.1 proxy
- // In either case we do not want to upgrade the connection to the proxy.
- // What we want to possibly upgrade is the tunneled connection to the
- // target server (so not the CONNECT request itself)
- this.version = Optional.of(HttpClient.Version.HTTP_1_1);
- }
-
- final boolean isConnect() {
- return "CONNECT".equalsIgnoreCase(method);
- }
-
- /**
- * Creates a HttpRequestImpl from the given set of Headers and the associated
- * "parent" request. Fields not taken from the headers are taken from the
- * parent.
- */
- static HttpRequestImpl createPushRequest(HttpRequestImpl parent,
- HttpHeadersImpl headers)
- throws IOException
- {
- return new HttpRequestImpl(parent, headers);
- }
-
- // only used for push requests
- private HttpRequestImpl(HttpRequestImpl parent, HttpHeadersImpl headers)
- throws IOException
- {
- this.method = headers.firstValue(":method")
- .orElseThrow(() -> new IOException("No method in Push Promise"));
- String path = headers.firstValue(":path")
- .orElseThrow(() -> new IOException("No path in Push Promise"));
- String scheme = headers.firstValue(":scheme")
- .orElseThrow(() -> new IOException("No scheme in Push Promise"));
- String authority = headers.firstValue(":authority")
- .orElseThrow(() -> new IOException("No authority in Push Promise"));
- StringBuilder sb = new StringBuilder();
- sb.append(scheme).append("://").append(authority).append(path);
- this.uri = URI.create(sb.toString());
- this.proxy = null;
- this.userHeaders = ImmutableHeaders.of(headers.map(), ALLOWED_HEADERS);
- this.systemHeaders = parent.systemHeaders;
- this.expectContinue = parent.expectContinue;
- this.secure = parent.secure;
- this.requestPublisher = parent.requestPublisher;
- this.acc = parent.acc;
- this.timeout = parent.timeout;
- this.version = parent.version;
- this.authority = null;
- }
-
- @Override
- public String toString() {
- return (uri == null ? "" : uri.toString()) + " " + method;
- }
-
- @Override
- public HttpHeaders headers() {
- return userHeaders;
- }
-
- InetSocketAddress authority() { return authority; }
-
- void setH2Upgrade(Http2ClientImpl h2client) {
- systemHeaders.setHeader("Connection", "Upgrade, HTTP2-Settings");
- systemHeaders.setHeader("Upgrade", "h2c");
- systemHeaders.setHeader("HTTP2-Settings", h2client.getSettingsString());
- }
-
- @Override
- public boolean expectContinue() { return expectContinue; }
-
- /** Retrieves the proxy, from the given ProxySelector, if there is one. */
- private static Proxy retrieveProxy(ProxySelector ps, URI uri) {
- Proxy proxy = null;
- List<Proxy> pl = ps.select(uri);
- if (!pl.isEmpty()) {
- Proxy p = pl.get(0);
- if (p.type() == Proxy.Type.HTTP)
- proxy = p;
- }
- return proxy;
- }
-
- InetSocketAddress proxy() {
- if (proxy == null || proxy.type() != Proxy.Type.HTTP
- || method.equalsIgnoreCase("CONNECT")) {
- return null;
- }
- return (InetSocketAddress)proxy.address();
- }
-
- boolean secure() { return secure; }
-
- @Override
- public void setProxy(Proxy proxy) {
- assert isWebSocket;
- this.proxy = proxy;
- }
-
- @Override
- public void isWebSocket(boolean is) {
- isWebSocket = is;
- }
-
- boolean isWebSocket() {
- return isWebSocket;
- }
-
- @Override
- public Optional<BodyPublisher> bodyPublisher() {
- return requestPublisher == null ? Optional.empty()
- : Optional.of(requestPublisher);
- }
-
- /**
- * Returns the request method for this request. If not set explicitly,
- * the default method for any request is "GET".
- */
- @Override
- public String method() { return method; }
-
- @Override
- public URI uri() { return uri; }
-
- @Override
- public Optional<Duration> timeout() {
- return timeout == null ? Optional.empty() : Optional.of(timeout);
- }
-
- HttpHeaders getUserHeaders() { return userHeaders; }
-
- HttpHeadersImpl getSystemHeaders() { return systemHeaders; }
-
- @Override
- public Optional<HttpClient.Version> version() { return version; }
-
- void addSystemHeader(String name, String value) {
- systemHeaders.addHeader(name, value);
- }
-
- @Override
- public void setSystemHeader(String name, String value) {
- systemHeaders.setHeader(name, value);
- }
-
- InetSocketAddress getAddress() {
- URI uri = uri();
- if (uri == null) {
- return authority();
- }
- int p = uri.getPort();
- if (p == -1) {
- if (uri.getScheme().equalsIgnoreCase("https")) {
- p = 443;
- } else {
- p = 80;
- }
- }
- final String host = uri.getHost();
- final int port = p;
- if (proxy() == null) {
- PrivilegedAction<InetSocketAddress> pa = () -> new InetSocketAddress(host, port);
- return AccessController.doPrivileged(pa);
- } else {
- return InetSocketAddress.createUnresolved(host, port);
- }
- }
-}
--- a/src/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<T> extends HttpResponse<T> implements RawChannel.Provider {
-
- final int responseCode;
- final Exchange<T> exchange;
- final HttpRequest initialRequest;
- final Optional<HttpResponse<T>> previousResponse;
- final HttpHeaders headers;
- final SSLParameters sslParameters;
- final URI uri;
- final HttpClient.Version version;
- RawChannel rawchan;
- final HttpConnection connection;
- final Stream<T> stream;
- final T body;
-
- public HttpResponseImpl(HttpRequest initialRequest,
- Response response,
- HttpResponse<T> previousResponse,
- T body,
- Exchange<T> exch) {
- this.responseCode = response.statusCode();
- this.exchange = exch;
- this.initialRequest = initialRequest;
- this.previousResponse = Optional.ofNullable(previousResponse);
- this.headers = response.headers();
- //this.trailers = trailers;
- this.sslParameters = exch.client().sslParameters();
- this.uri = response.request().uri();
- this.version = response.version();
- this.connection = connection(exch);
- this.stream = null;
- this.body = body;
- }
-
- private HttpConnection connection(Exchange<?> exch) {
- if (exch == null || exch.exchImpl == null) {
- assert responseCode == 407;
- return null; // case of Proxy 407
- }
- return exch.exchImpl.connection();
- }
-
- private ExchangeImpl<?> exchangeImpl() {
- return exchange != null ? exchange.exchImpl : stream;
- }
-
- @Override
- public int statusCode() {
- return responseCode;
- }
-
- @Override
- public HttpRequest request() {
- return initialRequest;
- }
-
- @Override
- public Optional<HttpResponse<T>> previousResponse() {
- return previousResponse;
- }
-
- @Override
- public HttpHeaders headers() {
- return headers;
- }
-
- @Override
- public T body() {
- return body;
- }
-
- @Override
- public SSLParameters sslParameters() {
- return sslParameters;
- }
-
- @Override
- public URI uri() {
- return uri;
- }
-
- @Override
- public HttpClient.Version version() {
- return version;
- }
- // keepalive flag determines whether connection is closed or kept alive
- // by reading/skipping data
-
- /**
- * Returns a RawChannel that may be used for WebSocket protocol.
- * @implNote This implementation does not support RawChannel over
- * HTTP/2 connections.
- * @return a RawChannel that may be used for WebSocket protocol.
- * @throws UnsupportedOperationException if getting a RawChannel over
- * this connection is not supported.
- * @throws IOException if an I/O exception occurs while retrieving
- * the channel.
- */
- @Override
- public synchronized RawChannel rawChannel() throws IOException {
- if (rawchan == null) {
- ExchangeImpl<?> exchImpl = exchangeImpl();
- if (!(exchImpl instanceof Http1Exchange)) {
- // RawChannel is only used for WebSocket - and WebSocket
- // is not supported over HTTP/2 yet, so we should not come
- // here. Getting a RawChannel over HTTP/2 might be supported
- // in the future, but it would entail retrieving any left over
- // bytes that might have been read but not consumed by the
- // HTTP/2 connection.
- throw new UnsupportedOperationException("RawChannel is not supported over HTTP/2");
- }
- // Http1Exchange may have some remaining bytes in its
- // internal buffer.
- Supplier<ByteBuffer> initial = ((Http1Exchange<?>)exchImpl)::drainLeftOverBytes;
- rawchan = new RawChannelImpl(exchange.client(), connection, initial);
- }
- return rawchan;
- }
-
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- String method = request().method();
- URI uri = request().uri();
- String uristring = uri == null ? "" : uri.toString();
- sb.append('(')
- .append(method)
- .append(" ")
- .append(uristring)
- .append(") ")
- .append(statusCode());
- return sb.toString();
- }
-}
--- a/src/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<String, List<String>> map;
-
- public static ImmutableHeaders empty() {
- return of(emptyMap());
- }
-
- public static ImmutableHeaders of(Map<String, List<String>> src) {
- return of(src, x -> true);
- }
-
- public static ImmutableHeaders of(HttpHeaders headers) {
- return (headers instanceof ImmutableHeaders)
- ? (ImmutableHeaders)headers
- : of(headers.map());
- }
-
- public static ImmutableHeaders of(Map<String, List<String>> src,
- Predicate<? super String> keyAllowed) {
- requireNonNull(src, "src");
- requireNonNull(keyAllowed, "keyAllowed");
- return new ImmutableHeaders(src, headerAllowed(keyAllowed));
- }
-
- public static ImmutableHeaders of(Map<String, List<String>> src,
- BiPredicate<? super String, ? super List<String>> headerAllowed) {
- requireNonNull(src, "src");
- requireNonNull(headerAllowed, "headerAllowed");
- return new ImmutableHeaders(src, headerAllowed);
- }
-
- private ImmutableHeaders(Map<String, List<String>> src,
- BiPredicate<? super String, ? super List<String>> headerAllowed) {
- Map<String, List<String>> m = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
- src.entrySet().stream()
- .filter(e -> headerAllowed.test(e.getKey(), e.getValue()))
- .forEach(e ->
- {
- List<String> values = new ArrayList<>(e.getValue());
- m.put(e.getKey(), unmodifiableList(values));
- }
- );
- this.map = unmodifiableMap(m);
- }
-
- private static BiPredicate<String, List<String>> headerAllowed(Predicate<? super String> keyAllowed) {
- return (n,v) -> keyAllowed.test(n);
- }
-
- @Override
- public Map<String, List<String>> map() {
- return map;
- }
-}
--- a/src/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<String>}. */
-public final class LineSubscriberAdapter<S extends Subscriber<? super String>,R>
- implements BodySubscriber<R> {
- private final CompletableFuture<R> cf = new MinimalFuture<>();
- private final S subscriber;
- private final Function<S, R> finisher;
- private final Charset charset;
- private final String eol;
- private volatile LineSubscription downstream;
-
- private LineSubscriberAdapter(S subscriber,
- Function<S, R> finisher,
- Charset charset,
- String eol) {
- if (eol != null && eol.isEmpty())
- throw new IllegalArgumentException("empty line separator");
- this.subscriber = Objects.requireNonNull(subscriber);
- this.finisher = Objects.requireNonNull(finisher);
- this.charset = Objects.requireNonNull(charset);
- this.eol = eol;
- }
-
- @Override
- public void onSubscribe(Subscription subscription) {
- downstream = LineSubscription.create(subscription,
- charset,
- eol,
- subscriber,
- cf);
- subscriber.onSubscribe(downstream);
- }
-
- @Override
- public void onNext(List<ByteBuffer> item) {
- try {
- downstream.submit(item);
- } catch (Throwable t) {
- onError(t);
- }
- }
-
- @Override
- public void onError(Throwable throwable) {
- try {
- downstream.signalError(throwable);
- } finally {
- cf.completeExceptionally(throwable);
- }
- }
-
- @Override
- public void onComplete() {
- try {
- downstream.signalComplete();
- } finally {
- cf.complete(finisher.apply(subscriber));
- }
- }
-
- @Override
- public CompletionStage<R> getBody() {
- return cf;
- }
-
- public static <S extends Subscriber<? super String>, R> LineSubscriberAdapter<S, R>
- create(S subscriber, Function<S, R> finisher, Charset charset, String eol)
- {
- if (eol != null && eol.isEmpty())
- throw new IllegalArgumentException("empty line separator");
- return new LineSubscriberAdapter<>(Objects.requireNonNull(subscriber),
- Objects.requireNonNull(finisher),
- Objects.requireNonNull(charset),
- eol);
- }
-
- static final class LineSubscription implements Flow.Subscription {
- final Flow.Subscription upstreamSubscription;
- final CharsetDecoder decoder;
- final String newline;
- final Demand downstreamDemand;
- final ConcurrentLinkedDeque<ByteBuffer> queue;
- final SequentialScheduler scheduler;
- final Flow.Subscriber<? super String> upstream;
- final CompletableFuture<?> cf;
- private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
- private final AtomicLong demanded = new AtomicLong();
- private volatile boolean completed;
- private volatile boolean cancelled;
-
- private final char[] chars = new char[1024];
- private final ByteBuffer leftover = ByteBuffer.wrap(new byte[64]);
- private final CharBuffer buffer = CharBuffer.wrap(chars);
- private final StringBuilder builder = new StringBuilder();
- private int lineCount;
- private String nextLine;
-
- private LineSubscription(Flow.Subscription s,
- CharsetDecoder dec,
- String separator,
- Flow.Subscriber<? super String> subscriber,
- CompletableFuture<?> completion) {
- downstreamDemand = new Demand();
- queue = new ConcurrentLinkedDeque<>();
- upstreamSubscription = Objects.requireNonNull(s);
- decoder = Objects.requireNonNull(dec);
- newline = separator;
- upstream = Objects.requireNonNull(subscriber);
- cf = Objects.requireNonNull(completion);
- scheduler = SequentialScheduler.synchronizedScheduler(this::loop);
- }
-
- @Override
- public void request(long n) {
- if (cancelled) return;
- if (downstreamDemand.increase(n)) {
- scheduler.runOrSchedule();
- }
- }
-
- @Override
- public void cancel() {
- cancelled = true;
- upstreamSubscription.cancel();
- }
-
- public void submit(List<ByteBuffer> list) {
- queue.addAll(list);
- demanded.decrementAndGet();
- scheduler.runOrSchedule();
- }
-
- public void signalComplete() {
- completed = true;
- scheduler.runOrSchedule();
- }
-
- public void signalError(Throwable error) {
- if (errorRef.compareAndSet(null,
- Objects.requireNonNull(error))) {
- scheduler.runOrSchedule();
- }
- }
-
- // This method looks at whether some bytes where left over (in leftover)
- // from decoding the previous buffer when the previous buffer was in
- // underflow. If so, it takes bytes one by one from the new buffer 'in'
- // and combines them with the leftover bytes until 'in' is exhausted or a
- // character was produced in 'out', resolving the previous underflow.
- // Returns true if the buffer is still in underflow, false otherwise.
- // However, in both situation some chars might have been produced in 'out'.
- private boolean isUnderFlow(ByteBuffer in, CharBuffer out, boolean endOfInput)
- throws CharacterCodingException {
- int limit = leftover.position();
- if (limit == 0) {
- // no leftover
- return false;
- } else {
- CoderResult res = null;
- while (in.hasRemaining()) {
- leftover.position(limit);
- leftover.limit(++limit);
- leftover.put(in.get());
- leftover.position(0);
- res = decoder.decode(leftover, out,
- endOfInput && !in.hasRemaining());
- int remaining = leftover.remaining();
- if (remaining > 0) {
- assert leftover.position() == 0;
- leftover.position(remaining);
- } else {
- leftover.position(0);
- }
- leftover.limit(leftover.capacity());
- if (res.isUnderflow() && remaining > 0 && in.hasRemaining()) {
- continue;
- }
- if (res.isError()) {
- res.throwException();
- }
- assert !res.isOverflow();
- return false;
- }
- return !endOfInput;
- }
- }
-
- // extract characters from start to end and remove them from
- // the StringBuilder
- private static String take(StringBuilder b, int start, int end) {
- assert start == 0;
- String line;
- if (end == start) return "";
- line = b.substring(start, end);
- b.delete(start, end);
- return line;
- }
-
- // finds end of line, returns -1 if not found, or the position after
- // the line delimiter if found, removing the delimiter in the process.
- private static int endOfLine(StringBuilder b, String eol, boolean endOfInput) {
- int len = b.length();
- if (eol != null) { // delimiter explicitly specified
- int i = b.indexOf(eol);
- if (i >= 0) {
- // remove the delimiter and returns the position
- // of the char after it.
- b.delete(i, i + eol.length());
- return i;
- }
- } else { // no delimiter specified, behaves as BufferedReader::readLine
- boolean crfound = false;
- for (int i = 0; i < len; i++) {
- char c = b.charAt(i);
- if (c == '\n') {
- // '\n' or '\r\n' found.
- // remove the delimiter and returns the position
- // of the char after it.
- b.delete(crfound ? i - 1 : i, i + 1);
- return crfound ? i - 1 : i;
- } else if (crfound) {
- // previous char was '\r', c != '\n'
- assert i != 0;
- // remove the delimiter and returns the position
- // of the char after it.
- b.delete(i - 1, i);
- return i - 1;
- }
- crfound = c == '\r';
- }
- if (crfound && endOfInput) {
- // remove the delimiter and returns the position
- // of the char after it.
- b.delete(len - 1, len);
- return len - 1;
- }
- }
- return endOfInput && len > 0 ? len : -1;
- }
-
- // Looks at whether the StringBuilder contains a line.
- // Returns null if more character are needed.
- private static String nextLine(StringBuilder b, String eol, boolean endOfInput) {
- int next = endOfLine(b, eol, endOfInput);
- return (next > -1) ? take(b, 0, next) : null;
- }
-
- // Attempts to read the next line. Returns the next line if
- // the delimiter was found, null otherwise. The delimiters are
- // consumed.
- private String nextLine()
- throws CharacterCodingException {
- assert nextLine == null;
- LINES:
- while (nextLine == null) {
- boolean endOfInput = completed && queue.isEmpty();
- nextLine = nextLine(builder, newline,
- endOfInput && leftover.position() == 0);
- if (nextLine != null) return nextLine;
- ByteBuffer b;
- BUFFERS:
- while ((b = queue.peek()) != null) {
- if (!b.hasRemaining()) {
- queue.poll();
- continue BUFFERS;
- }
- BYTES:
- while (b.hasRemaining()) {
- buffer.position(0);
- buffer.limit(buffer.capacity());
- boolean endofInput = completed && queue.size() <= 1;
- if (isUnderFlow(b, buffer, endofInput)) {
- assert !b.hasRemaining();
- if (buffer.position() > 0) {
- buffer.flip();
- builder.append(buffer);
- }
- continue BUFFERS;
- }
- CoderResult res = decoder.decode(b, buffer, endofInput);
- if (res.isError()) res.throwException();
- if (buffer.position() > 0) {
- buffer.flip();
- builder.append(buffer);
- continue LINES;
- }
- if (res.isUnderflow() && b.hasRemaining()) {
- //System.out.println("underflow: adding " + b.remaining() + " bytes");
- leftover.put(b);
- assert !b.hasRemaining();
- continue BUFFERS;
- }
- }
- }
-
- assert queue.isEmpty();
- if (endOfInput) {
- // Time to cleanup: there may be some undecoded leftover bytes
- // We need to flush them out.
- // The decoder has been configured to replace malformed/unmappable
- // chars with some replacement, in order to behave like
- // InputStreamReader.
- leftover.flip();
- buffer.position(0);
- buffer.limit(buffer.capacity());
-
- // decode() must be called just before flush, even if there
- // is nothing to decode. We must do this even if leftover
- // has no remaining bytes.
- CoderResult res = decoder.decode(leftover, buffer, endOfInput);
- if (buffer.position() > 0) {
- buffer.flip();
- builder.append(buffer);
- }
- if (res.isError()) res.throwException();
-
- // Now call decoder.flush()
- buffer.position(0);
- buffer.limit(buffer.capacity());
- res = decoder.flush(buffer);
- if (buffer.position() > 0) {
- buffer.flip();
- builder.append(buffer);
- }
- if (res.isError()) res.throwException();
-
- // It's possible that we reach here twice - just for the
- // purpose of checking that no bytes were left over, so
- // we reset leftover/decoder to make the function reentrant.
- leftover.position(0);
- leftover.limit(leftover.capacity());
- decoder.reset();
-
- // if some chars were produced then this call will
- // return them.
- return nextLine = nextLine(builder, newline, endOfInput);
- }
- return null;
- }
- return null;
- }
-
- // The main sequential scheduler loop.
- private void loop() {
- try {
- while (!cancelled) {
- Throwable error = errorRef.get();
- if (error != null) {
- cancelled = true;
- scheduler.stop();
- upstream.onError(error);
- cf.completeExceptionally(error);
- return;
- }
- if (nextLine == null) nextLine = nextLine();
- if (nextLine == null) {
- if (completed) {
- scheduler.stop();
- if (leftover.position() != 0) {
- // Underflow: not all bytes could be
- // decoded, but no more bytes will be coming.
- // This should not happen as we should already
- // have got a MalformedInputException, or
- // replaced the unmappable chars.
- errorRef.compareAndSet(null,
- new IllegalStateException(
- "premature end of input ("
- + leftover.position()
- + " undecoded bytes)"));
- continue;
- } else {
- upstream.onComplete();
- }
- return;
- } else if (demanded.get() == 0
- && !downstreamDemand.isFulfilled()) {
- long incr = Math.max(1, downstreamDemand.get());
- demanded.addAndGet(incr);
- upstreamSubscription.request(incr);
- continue;
- } else return;
- }
- assert nextLine != null;
- assert newline != null && !nextLine.endsWith(newline)
- || !nextLine.endsWith("\n") || !nextLine.endsWith("\r");
- if (downstreamDemand.tryDecrement()) {
- String forward = nextLine;
- nextLine = null;
- upstream.onNext(forward);
- } else return; // no demand: come back later
- }
- } catch (Throwable t) {
- try {
- upstreamSubscription.cancel();
- } finally {
- signalError(t);
- }
- }
- }
-
- static LineSubscription create(Flow.Subscription s,
- Charset charset,
- String lineSeparator,
- Flow.Subscriber<? super String> upstream,
- CompletableFuture<?> cf) {
- return new LineSubscription(Objects.requireNonNull(s),
- Objects.requireNonNull(charset).newDecoder()
- // use the same decoder configuration than
- // java.io.InputStreamReader
- .onMalformedInput(CodingErrorAction.REPLACE)
- .onUnmappableCharacter(CodingErrorAction.REPLACE),
- lineSeparator,
- Objects.requireNonNull(upstream),
- Objects.requireNonNull(cf));
- }
- }
-}
-
--- 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<T> {
-
- static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
- static final System.Logger DEBUG_LOGGER =
- Utils.getDebugLogger("MultiExchange"::toString, DEBUG);
-
- private final HttpRequest userRequest; // the user request
- private final HttpRequestImpl request; // a copy of the user request
- final AccessControlContext acc;
- final HttpClientImpl client;
- final HttpResponse.BodyHandler<T> responseHandler;
- final Executor executor;
- final AtomicInteger attempts = new AtomicInteger();
- HttpRequestImpl currentreq; // used for async only
- Exchange<T> exchange; // the current exchange
- Exchange<T> previous;
- volatile Throwable retryCause;
- volatile boolean expiredOnce;
- volatile HttpResponse<T> response = null;
-
- // Maximum number of times a request will be retried/redirected
- // for any reason
-
- static final int DEFAULT_MAX_ATTEMPTS = 5;
- static final int max_attempts = Utils.getIntegerNetProperty(
- "jdk.httpclient.redirects.retrylimit", DEFAULT_MAX_ATTEMPTS
- );
-
- private final List<HeaderFilter> filters;
- TimedEvent timedEvent;
- volatile boolean cancelled;
- final PushGroup<T> pushGroup;
-
- /**
- * Filter fields. These are attached as required by filters
- * and only used by the filter implementations. This could be
- * generalised into Objects that are passed explicitly to the filters
- * (one per MultiExchange object, and one per Exchange object possibly)
- */
- volatile AuthenticationFilter.AuthInfo serverauth, proxyauth;
- // RedirectHandler
- volatile int numberOfRedirects = 0;
-
- /**
- * MultiExchange with one final response.
- */
- MultiExchange(HttpRequest userRequest,
- HttpRequestImpl requestImpl,
- HttpClientImpl client,
- HttpResponse.BodyHandler<T> responseHandler,
- PushPromiseHandler<T> pushPromiseHandler,
- AccessControlContext acc) {
- this.previous = null;
- this.userRequest = userRequest;
- this.request = requestImpl;
- this.currentreq = request;
- this.client = client;
- this.filters = client.filterChain();
- this.acc = acc;
- this.executor = client.theExecutor();
- this.responseHandler = responseHandler;
- if (acc != null) {
- // Restricts the file publisher with the senders ACC, if any
- if (responseHandler instanceof UntrustedBodyHandler)
- ((UntrustedBodyHandler)this.responseHandler).setAccessControlContext(acc);
- }
-
- if (pushPromiseHandler != null) {
- this.pushGroup = new PushGroup<>(pushPromiseHandler, request, acc);
- } else {
- pushGroup = null;
- }
-
- this.exchange = new Exchange<>(request, this);
- }
-
- private synchronized Exchange<T> getExchange() {
- return exchange;
- }
-
- HttpClientImpl client() {
- return client;
- }
-
- HttpClient.Version version() {
- HttpClient.Version vers = request.version().orElse(client.version());
- if (vers == HttpClient.Version.HTTP_2 && !request.secure() && request.proxy() != null)
- vers = HttpClient.Version.HTTP_1_1;
- return vers;
- }
-
- private synchronized void setExchange(Exchange<T> exchange) {
- if (this.exchange != null && exchange != this.exchange) {
- this.exchange.released();
- }
- this.exchange = exchange;
- }
-
- private void cancelTimer() {
- if (timedEvent != null) {
- client.cancelTimer(timedEvent);
- }
- }
-
- private void requestFilters(HttpRequestImpl r) throws IOException {
- Log.logTrace("Applying request filters");
- for (HeaderFilter filter : filters) {
- Log.logTrace("Applying {0}", filter);
- filter.request(r, this);
- }
- Log.logTrace("All filters applied");
- }
-
- private HttpRequestImpl responseFilters(Response response) throws IOException
- {
- Log.logTrace("Applying response filters");
- for (HeaderFilter filter : filters) {
- Log.logTrace("Applying {0}", filter);
- HttpRequestImpl newreq = filter.response(response);
- if (newreq != null) {
- Log.logTrace("New request: stopping filters");
- return newreq;
- }
- }
- Log.logTrace("All filters applied");
- return null;
- }
-
-// public void cancel() {
-// cancelled = true;
-// getExchange().cancel();
-// }
-
- public void cancel(IOException cause) {
- cancelled = true;
- getExchange().cancel(cause);
- }
-
- public CompletableFuture<HttpResponse<T>> responseAsync() {
- CompletableFuture<Void> start = new MinimalFuture<>();
- CompletableFuture<HttpResponse<T>> cf = responseAsync0(start);
- start.completeAsync( () -> null, executor); // trigger execution
- return cf;
- }
-
- private CompletableFuture<HttpResponse<T>>
- responseAsync0(CompletableFuture<Void> start) {
- return start.thenCompose( v -> responseAsyncImpl())
- .thenCompose((Response r) -> {
- Exchange<T> exch = getExchange();
- return exch.readBodyAsync(responseHandler)
- .thenApply((T body) -> {
- this.response =
- new HttpResponseImpl<>(userRequest, r, this.response, body, exch);
- return this.response;
- });
- });
- }
-
- private CompletableFuture<Response> responseAsyncImpl() {
- CompletableFuture<Response> cf;
- if (attempts.incrementAndGet() > max_attempts) {
- cf = failedFuture(new IOException("Too many retries", retryCause));
- } else {
- if (currentreq.timeout().isPresent()) {
- timedEvent = new TimedEvent(currentreq.timeout().get());
- client.registerTimer(timedEvent);
- }
- try {
- // 1. apply request filters
- requestFilters(currentreq);
- } catch (IOException e) {
- return failedFuture(e);
- }
- Exchange<T> exch = getExchange();
- // 2. get response
- cf = exch.responseAsync()
- .thenCompose((Response response) -> {
- HttpRequestImpl newrequest;
- try {
- // 3. apply response filters
- newrequest = responseFilters(response);
- } catch (IOException e) {
- return failedFuture(e);
- }
- // 4. check filter result and repeat or continue
- if (newrequest == null) {
- if (attempts.get() > 1) {
- Log.logError("Succeeded on attempt: " + attempts);
- }
- return completedFuture(response);
- } else {
- this.response =
- new HttpResponseImpl<>(currentreq, response, this.response, null, exch);
- Exchange<T> oldExch = exch;
- return exch.ignoreBody().handle((r,t) -> {
- currentreq = newrequest;
- expiredOnce = false;
- setExchange(new Exchange<>(currentreq, this, acc));
- return responseAsyncImpl();
- }).thenCompose(Function.identity());
- } })
- .handle((response, ex) -> {
- // 5. handle errors and cancel any timer set
- cancelTimer();
- if (ex == null) {
- assert response != null;
- return completedFuture(response);
- }
- // all exceptions thrown are handled here
- CompletableFuture<Response> errorCF = getExceptionalCF(ex);
- if (errorCF == null) {
- return responseAsyncImpl();
- } else {
- return errorCF;
- } })
- .thenCompose(Function.identity());
- }
- return cf;
- }
-
- /**
- * Takes a Throwable and returns a suitable CompletableFuture that is
- * completed exceptionally, or null.
- */
- private CompletableFuture<Response> getExceptionalCF(Throwable t) {
- if ((t instanceof CompletionException) || (t instanceof ExecutionException)) {
- if (t.getCause() != null) {
- t = t.getCause();
- }
- }
- if (cancelled && t instanceof IOException) {
- t = new HttpTimeoutException("request timed out");
- } else if (t instanceof ConnectionExpiredException) {
- // allow the retry mechanism to do its work
- // ####: method (GET,HEAD, not POST?), no bytes written or read ( differentiate? )
- if (t.getCause() != null) retryCause = t.getCause();
- if (!expiredOnce) {
- DEBUG_LOGGER.log(Level.DEBUG,
- "MultiExchange: ConnectionExpiredException (async): retrying...",
- t);
- expiredOnce = true;
- return null;
- } else {
- DEBUG_LOGGER.log(Level.DEBUG,
- "MultiExchange: ConnectionExpiredException (async): already retried once.",
- t);
- if (t.getCause() != null) t = t.getCause();
- }
- }
- return failedFuture(t);
- }
-
- class TimedEvent extends TimeoutEvent {
- TimedEvent(Duration duration) {
- super(duration);
- }
- @Override
- public void handle() {
- DEBUG_LOGGER.log(Level.DEBUG,
- "Cancelling MultiExchange due to timeout for request %s",
- request);
- cancel(new HttpTimeoutException("request timed out"));
- }
- }
-}
--- a/src/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<Void> cf;
-
- ConnectEvent(CompletableFuture<Void> cf) {
- this.cf = cf;
- }
-
- @Override
- public SelectableChannel channel() {
- return chan;
- }
-
- @Override
- public int interestOps() {
- return SelectionKey.OP_CONNECT;
- }
-
- @Override
- public void handle() {
- try {
- assert !connected : "Already connected";
- assert !chan.isBlocking() : "Unexpected blocking channel";
- debug.log(Level.DEBUG, "ConnectEvent: finishing connect");
- boolean finished = chan.finishConnect();
- assert finished : "Expected channel to be connected";
- debug.log(Level.DEBUG,
- "ConnectEvent: connect finished: %s Local addr: %s", finished, chan.getLocalAddress());
- connected = true;
- // complete async since the event runs on the SelectorManager thread
- cf.completeAsync(() -> null, client().theExecutor());
- } catch (Throwable e) {
- client().theExecutor().execute( () -> cf.completeExceptionally(e));
- }
- }
-
- @Override
- public void abort(IOException ioe) {
- close();
- client().theExecutor().execute( () -> cf.completeExceptionally(ioe));
- }
- }
-
- @Override
- public CompletableFuture<Void> connectAsync() {
- CompletableFuture<Void> cf = new MinimalFuture<>();
- try {
- assert !connected : "Already connected";
- assert !chan.isBlocking() : "Unexpected blocking channel";
- boolean finished = false;
- PrivilegedExceptionAction<Boolean> pa = () -> chan.connect(address);
- try {
- finished = AccessController.doPrivileged(pa);
- } catch (PrivilegedActionException e) {
- cf.completeExceptionally(e.getCause());
- }
- if (finished) {
- debug.log(Level.DEBUG, "connect finished without blocking");
- connected = true;
- cf.complete(null);
- } else {
- debug.log(Level.DEBUG, "registering connect event");
- client().registerEvent(new ConnectEvent(cf));
- }
- } catch (Throwable throwable) {
- cf.completeExceptionally(throwable);
- }
- return cf;
- }
-
- @Override
- SocketChannel channel() {
- return chan;
- }
-
- @Override
- final FlowTube getConnectionFlow() {
- return tube;
- }
-
- PlainHttpConnection(InetSocketAddress addr, HttpClientImpl client) {
- super(addr, client);
- try {
- this.chan = SocketChannel.open();
- chan.configureBlocking(false);
- int bufsize = client.getReceiveBufferSize();
- if (!trySetReceiveBufferSize(bufsize)) {
- trySetReceiveBufferSize(256*1024);
- }
- chan.setOption(StandardSocketOptions.TCP_NODELAY, true);
- // wrap the connected channel in a Tube for async reading and writing
- tube = new SocketTube(client(), chan, Utils::getBuffer);
- } catch (IOException e) {
- throw new InternalError(e);
- }
- }
-
- private boolean trySetReceiveBufferSize(int bufsize) {
- try {
- chan.setOption(StandardSocketOptions.SO_RCVBUF, bufsize);
- return true;
- } catch(IOException x) {
- debug.log(Level.DEBUG,
- "Failed to set receive buffer size to %d on %s",
- bufsize, chan);
- }
- return false;
- }
-
- @Override
- HttpPublisher publisher() { return writePublisher; }
-
-
- @Override
- public String toString() {
- return "PlainHttpConnection: " + super.toString();
- }
-
- /**
- * Closes this connection
- */
- @Override
- public synchronized void close() {
- if (closed) {
- return;
- }
- closed = true;
- try {
- Log.logTrace("Closing: " + toString());
- chan.close();
- } catch (IOException e) {}
- }
-
- @Override
- void shutdownInput() throws IOException {
- debug.log(Level.DEBUG, "Shutting down input");
- chan.shutdownInput();
- }
-
- @Override
- void shutdownOutput() throws IOException {
- debug.log(Level.DEBUG, "Shutting down output");
- chan.shutdownOutput();
- }
-
- @Override
- ConnectionPool.CacheKey cacheKey() {
- return new ConnectionPool.CacheKey(address, null);
- }
-
- @Override
- synchronized boolean connected() {
- return connected;
- }
-
-
- @Override
- boolean isSecure() {
- return false;
- }
-
- @Override
- boolean isProxied() {
- return false;
- }
-
- // Support for WebSocket/RawChannelImpl which unfortunately
- // still depends on synchronous read/writes.
- // It should be removed when RawChannelImpl moves to using asynchronous APIs.
- private static final class PlainDetachedChannel
- extends DetachedConnectionChannel {
- final PlainHttpConnection plainConnection;
- boolean closed;
- PlainDetachedChannel(PlainHttpConnection conn) {
- // We're handing the connection channel over to a web socket.
- // We need the selector manager's thread to stay alive until
- // the WebSocket is closed.
- conn.client().webSocketOpen();
- this.plainConnection = conn;
- }
-
- @Override
- SocketChannel channel() {
- return plainConnection.channel();
- }
-
- @Override
- ByteBuffer read() throws IOException {
- ByteBuffer dst = ByteBuffer.allocate(8192);
- int n = readImpl(dst);
- if (n > 0) {
- return dst;
- } else if (n == 0) {
- return Utils.EMPTY_BYTEBUFFER;
- } else {
- return null;
- }
- }
-
- @Override
- public void close() {
- HttpClientImpl client = plainConnection.client();
- try {
- plainConnection.close();
- } finally {
- // notify the HttpClientImpl that the websocket is no
- // no longer operating.
- synchronized(this) {
- if (closed == true) return;
- closed = true;
- }
- client.webSocketClose();
- }
- }
-
- @Override
- public long write(ByteBuffer[] buffers, int start, int number)
- throws IOException
- {
- return channel().write(buffers, start, number);
- }
-
- @Override
- public void shutdownInput() throws IOException {
- plainConnection.shutdownInput();
- }
-
- @Override
- public void shutdownOutput() throws IOException {
- plainConnection.shutdownOutput();
- }
-
- private int readImpl(ByteBuffer buf) throws IOException {
- int mark = buf.position();
- int n;
- n = channel().read(buf);
- if (n == -1) {
- return -1;
- }
- Utils.flipToMark(buf, mark);
- return n;
- }
- }
-
- // Support for WebSocket/RawChannelImpl which unfortunately
- // still depends on synchronous read/writes.
- // It should be removed when RawChannelImpl moves to using asynchronous APIs.
- @Override
- DetachedConnectionChannel detachChannel() {
- client().cancelRegistration(channel());
- return new PlainDetachedChannel(this);
- }
-
-}
--- a/src/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);
- }
-}
--- 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<Void> connectAsync() {
- debug.log(Level.DEBUG, "Connecting plain connection");
- return delegate.connectAsync()
- .thenCompose((Void v) -> {
- debug.log(Level.DEBUG, "sending HTTP/1.1 CONNECT");
- HttpClientImpl client = client();
- assert client != null;
- HttpRequestImpl req = new HttpRequestImpl("CONNECT", address, proxyHeaders);
- MultiExchange<Void> mulEx = new MultiExchange<>(null, req,
- client, discard(), null, null);
- Exchange<Void> connectExchange = new Exchange<>(req, mulEx);
-
- return connectExchange
- .responseAsyncImpl(delegate)
- .thenCompose((Response resp) -> {
- CompletableFuture<Void> cf = new MinimalFuture<>();
- debug.log(Level.DEBUG, "got response: %d", resp.statusCode());
- if (resp.statusCode() == 407) {
- return connectExchange.ignoreBody().handle((r,t) -> {
- // close delegate after reading body: we won't
- // be reusing that connection anyway.
- delegate.close();
- ProxyAuthenticationRequired authenticationRequired =
- new ProxyAuthenticationRequired(resp);
- cf.completeExceptionally(authenticationRequired);
- return cf;
- }).thenCompose(Function.identity());
- } else if (resp.statusCode() != 200) {
- delegate.close();
- cf.completeExceptionally(new IOException(
- "Tunnel failed, got: "+ resp.statusCode()));
- } else {
- // get the initial/remaining bytes
- ByteBuffer b = ((Http1Exchange<?>)connectExchange.exchImpl).drainLeftOverBytes();
- int remaining = b.remaining();
- assert remaining == 0: "Unexpected remaining: " + remaining;
- connected = true;
- cf.complete(null);
- }
- return cf;
- });
- });
- }
-
- @Override
- boolean isTunnel() { return true; }
-
- @Override
- HttpPublisher publisher() { return delegate.publisher(); }
-
- @Override
- boolean connected() {
- return connected;
- }
-
- @Override
- SocketChannel channel() {
- return delegate.channel();
- }
-
- @Override
- FlowTube getConnectionFlow() {
- return delegate.getConnectionFlow();
- }
-
- @Override
- ConnectionPool.CacheKey cacheKey() {
- return new ConnectionPool.CacheKey(null, proxyAddr);
- }
-
- @Override
- public void close() {
- delegate.close();
- connected = false;
- }
-
- @Override
- void shutdownInput() throws IOException {
- delegate.shutdownInput();
- }
-
- @Override
- void shutdownOutput() throws IOException {
- delegate.shutdownOutput();
- }
-
- @Override
- boolean isSecure() {
- return false;
- }
-
- @Override
- boolean isProxied() {
- return true;
- }
-
- // Support for WebSocket/RawChannelImpl which unfortunately
- // still depends on synchronous read/writes.
- // It should be removed when RawChannelImpl moves to using asynchronous APIs.
- @Override
- DetachedConnectionChannel detachChannel() {
- return delegate.detachChannel();
- }
-}
--- a/src/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<Void> pa = () -> { r.run(); return null; };
- AccessController.doPrivileged(pa, acc);
- }
- }
-
- @Override
- public void execute(Runnable r) {
- executor.execute(new PrivilegedRunnable(r, acc));
- }
-}
--- 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;
- }
-}
--- 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<T> implements Flow.Publisher<T> {
-
- // Only one of `iterable` and `throwable` can be non-null. throwable is
- // non-null when an error has been encountered, by the creator of
- // PullPublisher, while subscribing the subscriber, but before subscribe has
- // completed.
- private final Iterable<T> iterable;
- private final Throwable throwable;
-
- PullPublisher(Iterable<T> iterable, Throwable throwable) {
- this.iterable = iterable;
- this.throwable = throwable;
- }
-
- PullPublisher(Iterable<T> iterable) {
- this(iterable, null);
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super T> subscriber) {
- Subscription sub;
- if (throwable != null) {
- assert iterable == null : "non-null iterable: " + iterable;
- sub = new Subscription(subscriber, null, throwable);
- } else {
- assert throwable == null : "non-null exception: " + throwable;
- sub = new Subscription(subscriber, iterable.iterator(), null);
- }
- subscriber.onSubscribe(sub);
-
- if (throwable != null) {
- sub.pullScheduler.runOrSchedule();
- }
- }
-
- private class Subscription implements Flow.Subscription {
-
- private final Flow.Subscriber<? super T> subscriber;
- private final Iterator<T> iter;
- private volatile boolean completed;
- private volatile boolean cancelled;
- private volatile Throwable error;
- final SequentialScheduler pullScheduler = new SequentialScheduler(new PullTask());
- private final Demand demand = new Demand();
-
- Subscription(Flow.Subscriber<? super T> subscriber,
- Iterator<T> iter,
- Throwable throwable) {
- this.subscriber = subscriber;
- this.iter = iter;
- this.error = throwable;
- }
-
- final class PullTask extends SequentialScheduler.CompleteRestartableTask {
- @Override
- protected void run() {
- if (completed || cancelled) {
- return;
- }
-
- Throwable t = error;
- if (t != null) {
- completed = true;
- pullScheduler.stop();
- subscriber.onError(t);
- return;
- }
-
- while (demand.tryDecrement() && !cancelled) {
- if (!iter.hasNext()) {
- break;
- } else {
- subscriber.onNext(iter.next());
- }
- }
- if (!iter.hasNext() && !cancelled) {
- completed = true;
- pullScheduler.stop();
- subscriber.onComplete();
- }
- }
- }
-
- @Override
- public void request(long n) {
- if (cancelled)
- return; // no-op
-
- if (n <= 0) {
- error = new IllegalArgumentException("illegal non-positive request:" + n);
- } else {
- demand.increase(n);
- }
- pullScheduler.runOrSchedule();
- }
-
- @Override
- public void cancel() {
- cancelled = true;
- }
- }
-}
--- a/src/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<T> {
- private final HttpRequest initiatingRequest;
-
- final CompletableFuture<Void> noMorePushesCF;
-
- volatile Throwable error; // any exception that occurred during pushes
-
- // user's subscriber object
- final PushPromiseHandler<T> pushPromiseHandler;
-
- private final AccessControlContext acc;
-
- int numberOfPushes;
- int remainingPushes;
- boolean noMorePushes = false;
-
- PushGroup(PushPromiseHandler<T> pushPromiseHandler,
- HttpRequestImpl initiatingRequest,
- AccessControlContext acc) {
- this(pushPromiseHandler, initiatingRequest, new MinimalFuture<>(), acc);
- }
-
- // Check mainBodyHandler before calling nested constructor.
- private PushGroup(HttpResponse.PushPromiseHandler<T> pushPromiseHandler,
- HttpRequestImpl initiatingRequest,
- CompletableFuture<HttpResponse<T>> mainResponse,
- AccessControlContext acc) {
- this.noMorePushesCF = new MinimalFuture<>();
- this.pushPromiseHandler = pushPromiseHandler;
- this.initiatingRequest = initiatingRequest;
- // Restricts the file publisher with the senders ACC, if any
- if (pushPromiseHandler instanceof UntrustedBodyHandler)
- ((UntrustedBodyHandler)this.pushPromiseHandler).setAccessControlContext(acc);
- this.acc = acc;
- }
-
- interface Acceptor<T> {
- BodyHandler<T> bodyHandler();
- CompletableFuture<HttpResponse<T>> cf();
- boolean accepted();
- }
-
- private static class AcceptorImpl<T> implements Acceptor<T> {
- private volatile HttpResponse.BodyHandler<T> bodyHandler;
- private volatile CompletableFuture<HttpResponse<T>> cf;
-
- CompletableFuture<HttpResponse<T>> accept(BodyHandler<T> bodyHandler) {
- Objects.requireNonNull(bodyHandler);
- if (this.bodyHandler != null)
- throw new IllegalStateException("non-null bodyHandler");
- this.bodyHandler = bodyHandler;
- cf = new MinimalFuture<>();
- return cf;
- }
-
- @Override public BodyHandler<T> bodyHandler() { return bodyHandler; }
-
- @Override public CompletableFuture<HttpResponse<T>> cf() { return cf; }
-
- @Override public boolean accepted() { return cf != null; }
- }
-
- Acceptor<T> acceptPushRequest(HttpRequest pushRequest) {
- AcceptorImpl<T> acceptor = new AcceptorImpl<>();
-
- pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, acceptor::accept);
-
- synchronized (this) {
- if (acceptor.accepted()) {
- if (acceptor.bodyHandler instanceof UntrustedBodyHandler) {
- ((UntrustedBodyHandler) acceptor.bodyHandler).setAccessControlContext(acc);
- }
- numberOfPushes++;
- remainingPushes++;
- }
- return acceptor;
- }
- }
-
- // This is called when the main body response completes because it means
- // no more PUSH_PROMISEs are possible
-
- synchronized void noMorePushes(boolean noMore) {
- noMorePushes = noMore;
- checkIfCompleted();
- noMorePushesCF.complete(null);
- }
-
- synchronized CompletableFuture<Void> pushesCF() {
- return noMorePushesCF;
- }
-
- synchronized boolean noMorePushes() {
- return noMorePushes;
- }
-
- synchronized void pushCompleted() {
- remainingPushes--;
- checkIfCompleted();
- }
-
- synchronized void checkIfCompleted() {
- if (Log.trace()) {
- Log.logTrace("PushGroup remainingPushes={0} error={1} noMorePushes={2}",
- remainingPushes,
- (error==null)?error:error.getClass().getSimpleName(),
- noMorePushes);
- }
- if (remainingPushes == 0 && error == null && noMorePushes) {
- if (Log.trace()) {
- Log.logTrace("push completed");
- }
- }
- }
-
- synchronized void pushError(Throwable t) {
- if (t == null) {
- return;
- }
- this.error = t;
- }
-}
--- a/src/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<ByteBuffer> initial;
-
- RawChannelImpl(HttpClientImpl client,
- HttpConnection connection,
- Supplier<ByteBuffer> initial)
- throws IOException
- {
- this.client = client;
- this.detachedChannel = connection.detachChannel();
- this.initial = initial;
-
- SocketChannel chan = connection.channel();
- client.cancelRegistration(chan);
- // Constructing a RawChannel is supposed to have a "hand over"
- // semantics, in other words if construction fails, the channel won't be
- // needed by anyone, in which case someone still needs to close it
- try {
- chan.configureBlocking(false);
- } catch (IOException e) {
- try {
- chan.close();
- } catch (IOException e1) {
- e.addSuppressed(e1);
- } finally {
- detachedChannel.close();
- }
- throw e;
- }
- }
-
- private class NonBlockingRawAsyncEvent extends AsyncEvent {
-
- private final RawEvent re;
-
- NonBlockingRawAsyncEvent(RawEvent re) {
- // !BLOCKING & !REPEATING
- this.re = re;
- }
-
- @Override
- public SelectableChannel channel() {
- return detachedChannel.channel();
- }
-
- @Override
- public int interestOps() {
- return re.interestOps();
- }
-
- @Override
- public void handle() {
- re.handle();
- }
-
- @Override
- public void abort(IOException ioe) { }
- }
-
- @Override
- public void registerEvent(RawEvent event) throws IOException {
- client.registerEvent(new NonBlockingRawAsyncEvent(event));
- }
-
- @Override
- public ByteBuffer read() throws IOException {
- assert !detachedChannel.channel().isBlocking();
- // connection.read() will no longer be available.
- return detachedChannel.read();
- }
-
- @Override
- public ByteBuffer initialByteBuffer() {
- synchronized (initialLock) {
- if (initial == null) {
- throw new IllegalStateException();
- }
- ByteBuffer ref = initial.get();
- ref = ref.hasRemaining() ? Utils.copy(ref)
- : Utils.EMPTY_BYTEBUFFER;
- initial = null;
- return ref;
- }
- }
-
- @Override
- public long write(ByteBuffer[] src, int offset, int len) throws IOException {
- // this makes the whitebox driver test fail.
- return detachedChannel.write(src, offset, len);
- }
-
- @Override
- public void shutdownInput() throws IOException {
- detachedChannel.shutdownInput();
- }
-
- @Override
- public void shutdownOutput() throws IOException {
- detachedChannel.shutdownOutput();
- }
-
- @Override
- public void close() throws IOException {
- detachedChannel.close();
- }
-
- @Override
- public String toString() {
- return super.toString()+"("+ detachedChannel.toString() + ")";
- }
-
-
-}
--- a/src/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();
- }
- }
-}
--- 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<ByteBuffer> delegate;
- private final int length;
- private final byte[] content;
- private final int offset;
- private final int bufSize;
-
- public ByteArrayPublisher(byte[] content) {
- this(content, 0, content.length);
- }
-
- public ByteArrayPublisher(byte[] content, int offset, int length) {
- this(content, offset, length, Utils.BUFSIZE);
- }
-
- /* bufSize exposed for testing purposes */
- ByteArrayPublisher(byte[] content, int offset, int length, int bufSize) {
- this.content = content;
- this.offset = offset;
- this.length = length;
- this.bufSize = bufSize;
- }
-
- List<ByteBuffer> copy(byte[] content, int offset, int length) {
- List<ByteBuffer> bufs = new ArrayList<>();
- while (length > 0) {
- ByteBuffer b = ByteBuffer.allocate(Math.min(bufSize, length));
- int max = b.capacity();
- int tocopy = Math.min(max, length);
- b.put(content, offset, tocopy);
- offset += tocopy;
- length -= tocopy;
- b.flip();
- bufs.add(b);
- }
- return bufs;
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
- List<ByteBuffer> copy = copy(content, offset, length);
- this.delegate = new PullPublisher<>(copy);
- delegate.subscribe(subscriber);
- }
-
- @Override
- public long contentLength() {
- return length;
- }
- }
-
- // This implementation has lots of room for improvement.
- public static class IterablePublisher implements BodyPublisher {
- private volatile Flow.Publisher<ByteBuffer> delegate;
- private final Iterable<byte[]> content;
- private volatile long contentLength;
-
- public IterablePublisher(Iterable<byte[]> content) {
- this.content = Objects.requireNonNull(content);
- }
-
- // The ByteBufferIterator will iterate over the byte[] arrays in
- // the content one at the time.
- //
- class ByteBufferIterator implements Iterator<ByteBuffer> {
- final ConcurrentLinkedQueue<ByteBuffer> buffers = new ConcurrentLinkedQueue<>();
- final Iterator<byte[]> iterator = content.iterator();
- @Override
- public boolean hasNext() {
- return !buffers.isEmpty() || iterator.hasNext();
- }
-
- @Override
- public ByteBuffer next() {
- ByteBuffer buffer = buffers.poll();
- while (buffer == null) {
- copy();
- buffer = buffers.poll();
- }
- return buffer;
- }
-
- ByteBuffer getBuffer() {
- return Utils.getBuffer();
- }
-
- void copy() {
- byte[] bytes = iterator.next();
- int length = bytes.length;
- if (length == 0 && iterator.hasNext()) {
- // avoid inserting empty buffers, except
- // if that's the last.
- return;
- }
- int offset = 0;
- do {
- ByteBuffer b = getBuffer();
- int max = b.capacity();
-
- int tocopy = Math.min(max, length);
- b.put(bytes, offset, tocopy);
- offset += tocopy;
- length -= tocopy;
- b.flip();
- buffers.add(b);
- } while (length > 0);
- }
- }
-
- public Iterator<ByteBuffer> iterator() {
- return new ByteBufferIterator();
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
- Iterable<ByteBuffer> iterable = this::iterator;
- this.delegate = new PullPublisher<>(iterable);
- delegate.subscribe(subscriber);
- }
-
- static long computeLength(Iterable<byte[]> bytes) {
- long len = 0;
- for (byte[] b : bytes) {
- len = Math.addExact(len, (long)b.length);
- }
- return len;
- }
-
- @Override
- public long contentLength() {
- if (contentLength == 0) {
- synchronized(this) {
- if (contentLength == 0) {
- contentLength = computeLength(content);
- }
- }
- }
- return contentLength;
- }
- }
-
- public static class StringPublisher extends ByteArrayPublisher {
- public StringPublisher(String content, Charset charset) {
- super(content.getBytes(charset));
- }
- }
-
- public static class EmptyPublisher implements BodyPublisher {
- private final Flow.Publisher<ByteBuffer> delegate =
- new PullPublisher<ByteBuffer>(Collections.emptyList(), null);
-
- @Override
- public long contentLength() {
- return 0;
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
- delegate.subscribe(subscriber);
- }
- }
-
- public static class FilePublisher implements BodyPublisher {
- private final File file;
- private volatile AccessControlContext acc;
-
- public FilePublisher(Path name) {
- file = name.toFile();
- }
-
- void setAccessControlContext(AccessControlContext acc) {
- this.acc = acc;
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
- if (System.getSecurityManager() != null && acc == null)
- throw new InternalError(
- "Unexpected null acc when security manager has been installed");
-
- InputStream is;
- try {
- PrivilegedExceptionAction<FileInputStream> pa =
- () -> new FileInputStream(file);
- is = AccessController.doPrivileged(pa, acc);
- } catch (PrivilegedActionException pae) {
- throw new UncheckedIOException((IOException)pae.getCause());
- }
- PullPublisher<ByteBuffer> publisher =
- new PullPublisher<>(() -> new StreamIterator(is));
- publisher.subscribe(subscriber);
- }
-
- @Override
- public long contentLength() {
- assert System.getSecurityManager() != null ? acc != null: true;
- PrivilegedAction<Long> pa = () -> file.length();
- return AccessController.doPrivileged(pa, acc);
- }
- }
-
- /**
- * Reads one buffer ahead all the time, blocking in hasNext()
- */
- public static class StreamIterator implements Iterator<ByteBuffer> {
- final InputStream is;
- final Supplier<? extends ByteBuffer> bufSupplier;
- volatile ByteBuffer nextBuffer;
- volatile boolean need2Read = true;
- volatile boolean haveNext;
-
- StreamIterator(InputStream is) {
- this(is, Utils::getBuffer);
- }
-
- StreamIterator(InputStream is, Supplier<? extends ByteBuffer> bufSupplier) {
- this.is = is;
- this.bufSupplier = bufSupplier;
- }
-
-// Throwable error() {
-// return error;
-// }
-
- private int read() {
- nextBuffer = bufSupplier.get();
- nextBuffer.clear();
- byte[] buf = nextBuffer.array();
- int offset = nextBuffer.arrayOffset();
- int cap = nextBuffer.capacity();
- try {
- int n = is.read(buf, offset, cap);
- if (n == -1) {
- is.close();
- return -1;
- }
- //flip
- nextBuffer.limit(n);
- nextBuffer.position(0);
- return n;
- } catch (IOException ex) {
- return -1;
- }
- }
-
- @Override
- public synchronized boolean hasNext() {
- if (need2Read) {
- haveNext = read() != -1;
- if (haveNext) {
- need2Read = false;
- }
- return haveNext;
- }
- return haveNext;
- }
-
- @Override
- public synchronized ByteBuffer next() {
- if (!hasNext()) {
- throw new NoSuchElementException();
- }
- need2Read = true;
- return nextBuffer;
- }
-
- }
-
- public static class InputStreamPublisher implements BodyPublisher {
- private final Supplier<? extends InputStream> streamSupplier;
-
- public InputStreamPublisher(Supplier<? extends InputStream> streamSupplier) {
- this.streamSupplier = Objects.requireNonNull(streamSupplier);
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
- PullPublisher<ByteBuffer> publisher;
- InputStream is = streamSupplier.get();
- if (is == null) {
- Throwable t = new IOException("streamSupplier returned null");
- publisher = new PullPublisher<>(null, t);
- } else {
- publisher = new PullPublisher<>(iterableOf(is), null);
- }
- publisher.subscribe(subscriber);
- }
-
- protected Iterable<ByteBuffer> iterableOf(InputStream is) {
- return () -> new StreamIterator(is);
- }
-
- @Override
- public long contentLength() {
- return -1;
- }
- }
-
- public static final class PublisherAdapter implements BodyPublisher {
-
- private final Publisher<? extends ByteBuffer> publisher;
- private final long contentLength;
-
- public PublisherAdapter(Publisher<? extends ByteBuffer> publisher,
- long contentLength) {
- this.publisher = Objects.requireNonNull(publisher);
- this.contentLength = contentLength;
- }
-
- @Override
- public final long contentLength() {
- return contentLength;
- }
-
- @Override
- public final void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
- publisher.subscribe(subscriber);
- }
- }
-}
--- 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();
- }
-}
--- 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<Path> {
- private final Path file;
- private final OpenOption[]openOptions;
- private volatile AccessControlContext acc;
-
- public PathBodyHandler(Path file, OpenOption... openOptions) {
- this.file = file;
- this.openOptions = openOptions;
- }
-
- @Override
- public void setAccessControlContext(AccessControlContext acc) {
- this.acc = acc;
- }
-
- @Override
- public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
- PathSubscriber bs = (PathSubscriber) asFileImpl(file, openOptions);
- bs.setAccessControlContext(acc);
- return bs;
- }
- }
-
- /** With push promise Map implementation */
- public static class PushPromisesHandlerWithMap<T>
- implements HttpResponse.PushPromiseHandler<T>
- {
- private final ConcurrentMap<HttpRequest,CompletableFuture<HttpResponse<T>>> pushPromisesMap;
- private final Function<HttpRequest,BodyHandler<T>> pushPromiseHandler;
-
- public PushPromisesHandlerWithMap(Function<HttpRequest,BodyHandler<T>> pushPromiseHandler,
- ConcurrentMap<HttpRequest,CompletableFuture<HttpResponse<T>>> pushPromisesMap) {
- this.pushPromiseHandler = pushPromiseHandler;
- this.pushPromisesMap = pushPromisesMap;
- }
-
- @Override
- public void applyPushPromise(
- HttpRequest initiatingRequest, HttpRequest pushRequest,
- Function<BodyHandler<T>,CompletableFuture<HttpResponse<T>>> acceptor)
- {
- URI initiatingURI = initiatingRequest.uri();
- URI pushRequestURI = pushRequest.uri();
- if (!initiatingURI.getHost().equalsIgnoreCase(pushRequestURI.getHost()))
- return;
-
- int initiatingPort = initiatingURI.getPort();
- if (initiatingPort == -1 ) {
- if ("https".equalsIgnoreCase(initiatingURI.getScheme()))
- initiatingPort = 443;
- else
- initiatingPort = 80;
- }
- int pushPort = pushRequestURI.getPort();
- if (pushPort == -1 ) {
- if ("https".equalsIgnoreCase(pushRequestURI.getScheme()))
- pushPort = 443;
- else
- pushPort = 80;
- }
- if (initiatingPort != pushPort)
- return;
-
- CompletableFuture<HttpResponse<T>> cf =
- acceptor.apply(pushPromiseHandler.apply(pushRequest));
- pushPromisesMap.put(pushRequest, cf);
- }
- }
-
- // Similar to Path body handler, but for file download. Supports setting ACC.
- public static class FileDownloadBodyHandler implements UntrustedBodyHandler<Path> {
- private final Path directory;
- private final OpenOption[]openOptions;
- private volatile AccessControlContext acc;
-
- public FileDownloadBodyHandler(Path directory, OpenOption... openOptions) {
- this.directory = directory;
- this.openOptions = openOptions;
- }
-
- @Override
- public void setAccessControlContext(AccessControlContext acc) {
- this.acc = acc;
- }
-
- @Override
- public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
- String dispoHeader = headers.firstValue("Content-Disposition")
- .orElseThrow(() -> unchecked(new IOException("No Content-Disposition")));
- if (!dispoHeader.startsWith("attachment;")) {
- throw unchecked(new IOException("Unknown Content-Disposition type"));
- }
- int n = dispoHeader.indexOf("filename=");
- if (n == -1) {
- throw unchecked(new IOException("Bad Content-Disposition type"));
- }
- int lastsemi = dispoHeader.lastIndexOf(';');
- String disposition;
- if (lastsemi < n) {
- disposition = dispoHeader.substring(n + 9);
- } else {
- disposition = dispoHeader.substring(n + 9, lastsemi);
- }
- Path file = Paths.get(directory.toString(), disposition);
-
- PathSubscriber bs = (PathSubscriber)asFileImpl(file, openOptions);
- bs.setAccessControlContext(acc);
- return bs;
- }
- }
-
- // no security check
- private static BodySubscriber<Path> asFileImpl(Path file, OpenOption... openOptions) {
- return new ResponseSubscribers.PathSubscriber(file, openOptions);
- }
-}
--- 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<ByteBuffer> {
- void onSubscribe(AbstractSubscription sub);
- }
-
- // Returns a parser that will take care of parsing the received byte
- // buffers and forward them to the BodySubscriber.
- // When the parser is done, it will call onComplete.
- // If parsing was successful, the throwable parameter will be null.
- // Otherwise it will be the exception that occurred
- // Note: revisit: it might be better to use a CompletableFuture than
- // a completion handler.
- BodyParser getBodyParser(Consumer<Throwable> onComplete)
- throws IOException {
- if (contentChunked()) {
- return new ChunkedBodyParser(onComplete);
- } else {
- return new FixedLengthBodyParser(contentLength, onComplete);
- }
- }
-
-
- static enum ChunkState {READING_LENGTH, READING_DATA, DONE}
- class ChunkedBodyParser implements BodyParser {
- final ByteBuffer READMORE = Utils.EMPTY_BYTEBUFFER;
- final Consumer<Throwable> onComplete;
- final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
- final String dbgTag = ResponseContent.this.dbgTag + "/ChunkedBodyParser";
-
- volatile Throwable closedExceptionally;
- volatile int partialChunklen = 0; // partially read chunk len
- volatile int chunklen = -1; // number of bytes in chunk
- volatile int bytesremaining; // number of bytes in chunk left to be read incl CRLF
- volatile boolean cr = false; // tryReadChunkLength has found CR
- volatile int bytesToConsume; // number of bytes that still need to be consumed before proceeding
- volatile ChunkState state = ChunkState.READING_LENGTH; // current state
- volatile AbstractSubscription sub;
- ChunkedBodyParser(Consumer<Throwable> onComplete) {
- this.onComplete = onComplete;
- }
-
- String dbgString() {
- return dbgTag;
- }
-
- @Override
- public void onSubscribe(AbstractSubscription sub) {
- debug.log(Level.DEBUG, () -> "onSubscribe: "
- + pusher.getClass().getName());
- pusher.onSubscribe(this.sub = sub);
- }
-
- @Override
- public void accept(ByteBuffer b) {
- if (closedExceptionally != null) {
- debug.log(Level.DEBUG, () -> "already closed: "
- + closedExceptionally);
- return;
- }
- boolean completed = false;
- try {
- List<ByteBuffer> out = new ArrayList<>();
- do {
- if (tryPushOneHunk(b, out)) {
- // We're done! (true if the final chunk was parsed).
- if (!out.isEmpty()) {
- // push what we have and complete
- // only reduce demand if we actually push something.
- // we would not have come here if there was no
- // demand.
- boolean hasDemand = sub.demand().tryDecrement();
- assert hasDemand;
- pusher.onNext(Collections.unmodifiableList(out));
- }
- debug.log(Level.DEBUG, () -> "done!");
- assert closedExceptionally == null;
- assert state == ChunkState.DONE;
- onFinished.run();
- pusher.onComplete();
- completed = true;
- onComplete.accept(closedExceptionally); // should be null
- break;
- }
- // the buffer may contain several hunks, and therefore
- // we must loop while it's not exhausted.
- } while (b.hasRemaining());
-
- if (!completed && !out.isEmpty()) {
- // push what we have.
- // only reduce demand if we actually push something.
- // we would not have come here if there was no
- // demand.
- boolean hasDemand = sub.demand().tryDecrement();
- assert hasDemand;
- pusher.onNext(Collections.unmodifiableList(out));
- }
- assert state == ChunkState.DONE || !b.hasRemaining();
- } catch(Throwable t) {
- closedExceptionally = t;
- if (!completed) onComplete.accept(t);
- }
- }
-
- // reads and returns chunklen. Position of chunkbuf is first byte
- // of chunk on return. chunklen includes the CR LF at end of chunk
- // returns -1 if needs more bytes
- private int tryReadChunkLen(ByteBuffer chunkbuf) throws IOException {
- assert state == ChunkState.READING_LENGTH;
- while (chunkbuf.hasRemaining()) {
- int c = chunkbuf.get();
- if (cr) {
- if (c == LF) {
- return partialChunklen;
- } else {
- throw new IOException("invalid chunk header");
- }
- }
- if (c == CR) {
- cr = true;
- } else {
- int digit = toDigit(c);
- partialChunklen = partialChunklen * 16 + digit;
- }
- }
- return -1;
- }
-
-
- // try to consume as many bytes as specified by bytesToConsume.
- // returns the number of bytes that still need to be consumed.
- // In practice this method is only called to consume one CRLF pair
- // with bytesToConsume set to 2, so it will only return 0 (if completed),
- // 1, or 2 (if chunkbuf doesn't have the 2 chars).
- private int tryConsumeBytes(ByteBuffer chunkbuf) throws IOException {
- int n = bytesToConsume;
- if (n > 0) {
- int e = Math.min(chunkbuf.remaining(), n);
-
- // verifies some assertions
- // this methods is called only to consume CRLF
- if (Utils.ASSERTIONSENABLED) {
- assert n <= 2 && e <= 2;
- ByteBuffer tmp = chunkbuf.slice();
- // if n == 2 assert that we will first consume CR
- assert (n == 2 && e > 0) ? tmp.get() == CR : true;
- // if n == 1 || n == 2 && e == 2 assert that we then consume LF
- assert (n == 1 || e == 2) ? tmp.get() == LF : true;
- }
-
- chunkbuf.position(chunkbuf.position() + e);
- n -= e;
- bytesToConsume = n;
- }
- assert n >= 0;
- return n;
- }
-
- /**
- * Returns a ByteBuffer containing chunk of data or a "hunk" of data
- * (a chunk of a chunk if the chunk size is larger than our ByteBuffers).
- * If the given chunk does not have enough data this method return
- * an empty ByteBuffer (READMORE).
- * If we encounter the final chunk (an empty chunk) this method
- * returns null.
- */
- ByteBuffer tryReadOneHunk(ByteBuffer chunk) throws IOException {
- int unfulfilled = bytesremaining;
- int toconsume = bytesToConsume;
- ChunkState st = state;
- if (st == ChunkState.READING_LENGTH && chunklen == -1) {
- debug.log(Level.DEBUG, () -> "Trying to read chunk len"
- + " (remaining in buffer:"+chunk.remaining()+")");
- int clen = chunklen = tryReadChunkLen(chunk);
- if (clen == -1) return READMORE;
- debug.log(Level.DEBUG, "Got chunk len %d", clen);
- cr = false; partialChunklen = 0;
- unfulfilled = bytesremaining = clen;
- if (clen == 0) toconsume = bytesToConsume = 2; // that was the last chunk
- else st = state = ChunkState.READING_DATA; // read the data
- }
-
- if (toconsume > 0) {
- debug.log(Level.DEBUG,
- "Trying to consume bytes: %d (remaining in buffer: %s)",
- toconsume, chunk.remaining());
- if (tryConsumeBytes(chunk) > 0) {
- return READMORE;
- }
- }
-
- toconsume = bytesToConsume;
- assert toconsume == 0;
-
-
- if (st == ChunkState.READING_LENGTH) {
- // we will come here only if chunklen was 0, after having
- // consumed the trailing CRLF
- int clen = chunklen;
- assert clen == 0;
- debug.log(Level.DEBUG, "No more chunks: %d", clen);
- // the DONE state is not really needed but it helps with
- // assertions...
- state = ChunkState.DONE;
- return null;
- }
-
- int clen = chunklen;
- assert clen > 0;
- assert st == ChunkState.READING_DATA;
-
- ByteBuffer returnBuffer = READMORE; // May be a hunk or a chunk
- if (unfulfilled > 0) {
- int bytesread = chunk.remaining();
- debug.log(Level.DEBUG, "Reading chunk: available %d, needed %d",
- bytesread, unfulfilled);
-
- int bytes2return = Math.min(bytesread, unfulfilled);
- debug.log(Level.DEBUG, "Returning chunk bytes: %d", bytes2return);
- returnBuffer = Utils.sliceWithLimitedCapacity(chunk, bytes2return).asReadOnlyBuffer();
- unfulfilled = bytesremaining -= bytes2return;
- if (unfulfilled == 0) bytesToConsume = 2;
- }
-
- assert unfulfilled >= 0;
-
- if (unfulfilled == 0) {
- debug.log(Level.DEBUG,
- "No more bytes to read - %d yet to consume.",
- unfulfilled);
- // check whether the trailing CRLF is consumed, try to
- // consume it if not. If tryConsumeBytes needs more bytes
- // then we will come back here later - skipping the block
- // that reads data because remaining==0, and finding
- // that the two bytes are now consumed.
- if (tryConsumeBytes(chunk) == 0) {
- // we're done for this chunk! reset all states and
- // prepare to read the next chunk.
- chunklen = -1;
- partialChunklen = 0;
- cr = false;
- state = ChunkState.READING_LENGTH;
- debug.log(Level.DEBUG, "Ready to read next chunk");
- }
- }
- if (returnBuffer == READMORE) {
- debug.log(Level.DEBUG, "Need more data");
- }
- return returnBuffer;
- }
-
-
- // Attempt to parse and push one hunk from the buffer.
- // Returns true if the final chunk was parsed.
- // Returns false if we need to push more chunks.
- private boolean tryPushOneHunk(ByteBuffer b, List<ByteBuffer> out)
- throws IOException {
- assert state != ChunkState.DONE;
- ByteBuffer b1 = tryReadOneHunk(b);
- if (b1 != null) {
- //assert b1.hasRemaining() || b1 == READMORE;
- if (b1.hasRemaining()) {
- debug.log(Level.DEBUG, "Sending chunk to consumer (%d)",
- b1.remaining());
- out.add(b1);
- debug.log(Level.DEBUG, "Chunk sent.");
- }
- return false; // we haven't parsed the final chunk yet.
- } else {
- return true; // we're done! the final chunk was parsed.
- }
- }
-
- private int toDigit(int b) throws IOException {
- if (b >= 0x30 && b <= 0x39) {
- return b - 0x30;
- }
- if (b >= 0x41 && b <= 0x46) {
- return b - 0x41 + 10;
- }
- if (b >= 0x61 && b <= 0x66) {
- return b - 0x61 + 10;
- }
- throw new IOException("Invalid chunk header byte " + b);
- }
-
- }
-
- class FixedLengthBodyParser implements BodyParser {
- final int contentLength;
- final Consumer<Throwable> onComplete;
- final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
- final String dbgTag = ResponseContent.this.dbgTag + "/FixedLengthBodyParser";
- volatile int remaining;
- volatile Throwable closedExceptionally;
- volatile AbstractSubscription sub;
- FixedLengthBodyParser(int contentLength, Consumer<Throwable> onComplete) {
- this.contentLength = this.remaining = contentLength;
- this.onComplete = onComplete;
- }
-
- String dbgString() {
- return dbgTag;
- }
-
- @Override
- public void onSubscribe(AbstractSubscription sub) {
- debug.log(Level.DEBUG, () -> "length="
- + contentLength +", onSubscribe: "
- + pusher.getClass().getName());
- pusher.onSubscribe(this.sub = sub);
- try {
- if (contentLength == 0) {
- onFinished.run();
- pusher.onComplete();
- onComplete.accept(null);
- }
- } catch (Throwable t) {
- closedExceptionally = t;
- try {
- pusher.onError(t);
- } finally {
- onComplete.accept(t);
- }
- }
- }
-
- @Override
- public void accept(ByteBuffer b) {
- if (closedExceptionally != null) {
- debug.log(Level.DEBUG, () -> "already closed: "
- + closedExceptionally);
- return;
- }
- boolean completed = false;
- try {
- int unfulfilled = remaining;
- debug.log(Level.DEBUG, "Parser got %d bytes (%d remaining / %d)",
- b.remaining(), unfulfilled, contentLength);
- assert unfulfilled != 0 || contentLength == 0 || b.remaining() == 0;
-
- if (unfulfilled == 0 && contentLength > 0) return;
-
- if (b.hasRemaining() && unfulfilled > 0) {
- // only reduce demand if we actually push something.
- // we would not have come here if there was no
- // demand.
- boolean hasDemand = sub.demand().tryDecrement();
- assert hasDemand;
- int amount = Math.min(b.remaining(), unfulfilled);
- unfulfilled = remaining -= amount;
- ByteBuffer buffer = Utils.sliceWithLimitedCapacity(b, amount);
- pusher.onNext(List.of(buffer.asReadOnlyBuffer()));
- }
- if (unfulfilled == 0) {
- // We're done! All data has been received.
- assert closedExceptionally == null;
- onFinished.run();
- pusher.onComplete();
- completed = true;
- onComplete.accept(closedExceptionally); // should be null
- } else {
- assert b.remaining() == 0;
- }
- } catch (Throwable t) {
- debug.log(Level.DEBUG, "Unexpected exception", t);
- closedExceptionally = t;
- if (!completed) {
- onComplete.accept(t);
- }
- }
- }
- }
-}
--- a/src/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<Void> {
- private final Consumer<Optional<byte[]>> consumer;
- private Flow.Subscription subscription;
- private final CompletableFuture<Void> result = new MinimalFuture<>();
- private final AtomicBoolean subscribed = new AtomicBoolean();
-
- public ConsumerSubscriber(Consumer<Optional<byte[]>> consumer) {
- this.consumer = Objects.requireNonNull(consumer);
- }
-
- @Override
- public CompletionStage<Void> getBody() {
- return result;
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- if (!subscribed.compareAndSet(false, true)) {
- subscription.cancel();
- } else {
- this.subscription = subscription;
- subscription.request(1);
- }
- }
-
- @Override
- public void onNext(List<ByteBuffer> items) {
- for (ByteBuffer item : items) {
- byte[] buf = new byte[item.remaining()];
- item.get(buf);
- consumer.accept(Optional.of(buf));
- }
- subscription.request(1);
- }
-
- @Override
- public void onError(Throwable throwable) {
- result.completeExceptionally(throwable);
- }
-
- @Override
- public void onComplete() {
- consumer.accept(Optional.empty());
- result.complete(null);
- }
-
- }
-
- public static class PathSubscriber implements BodySubscriber<Path> {
-
- private final Path file;
- private final CompletableFuture<Path> 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<FileChannel> 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<ByteBuffer> items) {
- try {
- out.write(items.toArray(Utils.EMPTY_BB_ARRAY));
- } catch (IOException ex) {
- Utils.close(out);
- subscription.cancel();
- result.completeExceptionally(ex);
- }
- subscription.request(1);
- }
-
- @Override
- public void onError(Throwable e) {
- result.completeExceptionally(e);
- Utils.close(out);
- }
-
- @Override
- public void onComplete() {
- Utils.close(out);
- result.complete(file);
- }
-
- @Override
- public CompletionStage<Path> getBody() {
- return result;
- }
- }
-
- public static class ByteArraySubscriber<T> implements BodySubscriber<T> {
- private final Function<byte[], T> finisher;
- private final CompletableFuture<T> result = new MinimalFuture<>();
- private final List<ByteBuffer> received = new ArrayList<>();
-
- private volatile Flow.Subscription subscription;
-
- public ByteArraySubscriber(Function<byte[],T> finisher) {
- this.finisher = finisher;
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- if (this.subscription != null) {
- subscription.cancel();
- return;
- }
- this.subscription = subscription;
- // We can handle whatever you've got
- subscription.request(Long.MAX_VALUE);
- }
-
- @Override
- public void onNext(List<ByteBuffer> items) {
- // incoming buffers are allocated by http client internally,
- // and won't be used anywhere except this place.
- // So it's free simply to store them for further processing.
- assert Utils.hasRemaining(items);
- received.addAll(items);
- }
-
- @Override
- public void onError(Throwable throwable) {
- received.clear();
- result.completeExceptionally(throwable);
- }
-
- static private byte[] join(List<ByteBuffer> bytes) {
- int size = Utils.remaining(bytes, Integer.MAX_VALUE);
- byte[] res = new byte[size];
- int from = 0;
- for (ByteBuffer b : bytes) {
- int l = b.remaining();
- b.get(res, from, l);
- from += l;
- }
- return res;
- }
-
- @Override
- public void onComplete() {
- try {
- result.complete(finisher.apply(join(received)));
- received.clear();
- } catch (IllegalArgumentException e) {
- result.completeExceptionally(e);
- }
- }
-
- @Override
- public CompletionStage<T> getBody() {
- return result;
- }
- }
-
- /**
- * An InputStream built on top of the Flow API.
- */
- public static class HttpResponseInputStream extends InputStream
- implements BodySubscriber<InputStream>
- {
- final static 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<ByteBuffer> 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<List<ByteBuffer>> buffers;
- private volatile Flow.Subscription subscription;
- private volatile boolean closed;
- private volatile Throwable failed;
- private volatile Iterator<ByteBuffer> currentListItr;
- private volatile ByteBuffer currentBuffer;
- private final AtomicBoolean subscribed = new AtomicBoolean();
-
- public HttpResponseInputStream() {
- this(MAX_BUFFERS_IN_QUEUE);
- }
-
- HttpResponseInputStream(int maxBuffers) {
- int capacity = (maxBuffers <= 0 ? MAX_BUFFERS_IN_QUEUE : maxBuffers);
- // 1 additional slot needed for LAST_LIST added by onComplete
- this.buffers = new ArrayBlockingQueue<>(capacity + 1);
- }
-
- @Override
- public CompletionStage<InputStream> getBody() {
- // Returns the stream immediately, before the
- // response body is received.
- // This makes it possible for sendAsync().get().body()
- // to complete before the response body is received.
- return CompletableFuture.completedStage(this);
- }
-
- // Returns the current byte buffer to read from.
- // If the current buffer has no remaining data, this method will take the
- // next buffer from the buffers queue, possibly blocking until
- // a new buffer is made available through the Flow API, or the
- // end of the flow has been reached.
- private ByteBuffer current() throws IOException {
- while (currentBuffer == null || !currentBuffer.hasRemaining()) {
- // Check whether the stream is closed or exhausted
- if (closed || failed != null) {
- throw new IOException("closed", failed);
- }
- if (currentBuffer == LAST_BUFFER) break;
-
- try {
- if (currentListItr == null || !currentListItr.hasNext()) {
- // Take a new list of buffers from the queue, blocking
- // if none is available yet...
-
- DEBUG_LOGGER.log(Level.DEBUG, "Taking list of Buffers");
- List<ByteBuffer> 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<ByteBuffer> 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<Stream<String>> createLineStream() {
- return createLineStream(UTF_8);
- }
-
- public static BodySubscriber<Stream<String>> createLineStream(Charset charset) {
- Objects.requireNonNull(charset);
- BodySubscriber<InputStream> s = new HttpResponseInputStream();
- return new MappedSubscriber<InputStream,Stream<String>>(s,
- (InputStream stream) -> {
- return new BufferedReader(new InputStreamReader(stream, charset))
- .lines().onClose(() -> Utils.close(stream));
- });
- }
-
- /**
- * Currently this consumes all of the data and ignores it
- */
- public static class NullSubscriber<T> implements BodySubscriber<T> {
-
- private final CompletableFuture<T> cf = new MinimalFuture<>();
- private final Optional<T> result;
- private final AtomicBoolean subscribed = new AtomicBoolean();
-
- public NullSubscriber(Optional<T> result) {
- this.result = result;
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- if (!subscribed.compareAndSet(false, true)) {
- subscription.cancel();
- } else {
- subscription.request(Long.MAX_VALUE);
- }
- }
-
- @Override
- public void onNext(List<ByteBuffer> items) {
- Objects.requireNonNull(items);
- }
-
- @Override
- public void onError(Throwable throwable) {
- cf.completeExceptionally(throwable);
- }
-
- @Override
- public void onComplete() {
- if (result.isPresent()) {
- cf.complete(result.get());
- } else {
- cf.complete(null);
- }
- }
-
- @Override
- public CompletionStage<T> getBody() {
- return cf;
- }
- }
-
- /** An adapter between {@code BodySubscriber} and {@code Flow.Subscriber}. */
- public static final class SubscriberAdapter<S extends Subscriber<? super List<ByteBuffer>>,R>
- implements BodySubscriber<R>
- {
- private final CompletableFuture<R> cf = new MinimalFuture<>();
- private final S subscriber;
- private final Function<S,R> finisher;
- private volatile Subscription subscription;
-
- public SubscriberAdapter(S subscriber, Function<S,R> finisher) {
- this.subscriber = Objects.requireNonNull(subscriber);
- this.finisher = Objects.requireNonNull(finisher);
- }
-
- @Override
- public void onSubscribe(Subscription subscription) {
- Objects.requireNonNull(subscription);
- if (this.subscription != null) {
- subscription.cancel();
- } else {
- this.subscription = subscription;
- subscriber.onSubscribe(subscription);
- }
- }
-
- @Override
- public void onNext(List<ByteBuffer> item) {
- Objects.requireNonNull(item);
- try {
- subscriber.onNext(item);
- } catch (Throwable throwable) {
- subscription.cancel();
- onError(throwable);
- }
- }
-
- @Override
- public void onError(Throwable throwable) {
- Objects.requireNonNull(throwable);
- try {
- subscriber.onError(throwable);
- } finally {
- cf.completeExceptionally(throwable);
- }
- }
-
- @Override
- public void onComplete() {
- try {
- subscriber.onComplete();
- } finally {
- try {
- cf.complete(finisher.apply(subscriber));
- } catch (Throwable throwable) {
- cf.completeExceptionally(throwable);
- }
- }
- }
-
- @Override
- public CompletionStage<R> getBody() {
- return cf;
- }
- }
-
- /**
- * A body subscriber which receives input from an upstream subscriber
- * and maps that subscriber's body type to a new type. The upstream subscriber
- * delegates all flow operations directly to this object. The
- * {@link CompletionStage} returned by {@link #getBody()}} takes the output
- * of the upstream {@code getBody()} and applies the mapper function to
- * obtain the new {@code CompletionStage} type.
- *
- * Uses an Executor that must be set externally.
- *
- * @param <T> the upstream body type
- * @param <U> this subscriber's body type
- */
- public static class MappedSubscriber<T,U> implements BodySubscriber<U> {
- final BodySubscriber<T> upstream;
- final Function<T,U> mapper;
-
- /**
- *
- * @param upstream
- * @param mapper
- */
- public MappedSubscriber(BodySubscriber<T> upstream, Function<T,U> mapper) {
- this.upstream = upstream;
- this.mapper = mapper;
- }
-
- @Override
- public CompletionStage<U> getBody() {
- return upstream.getBody()
- .thenApply(mapper);
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- upstream.onSubscribe(subscription);
- }
-
- @Override
- public void onNext(List<ByteBuffer> item) {
- upstream.onNext(item);
- }
-
- @Override
- public void onError(Throwable throwable) {
- upstream.onError(throwable);
- }
-
- @Override
- public void onComplete() {
- upstream.onComplete();
- }
- }
-}
--- a/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.
- * <p>
- * This class is only used to implement the {@link
- * AbstractAsyncSSLConnection.SSLConnectionChannel} which is handed of
- * to RawChannelImpl when creating a WebSocket.
- */
-class SSLDelegate {
-
- final SSLEngine engine;
- final EngineWrapper wrapper;
- final Lock handshaking = new ReentrantLock();
- final SocketChannel chan;
-
- SSLDelegate(SSLEngine eng, SocketChannel chan)
- {
- this.engine = eng;
- this.chan = chan;
- this.wrapper = new EngineWrapper(chan, engine);
- }
-
- // alpn[] may be null
-// SSLDelegate(SocketChannel chan, HttpClientImpl client, String[] alpn, String sn)
-// throws IOException
-// {
-// serverName = sn;
-// SSLContext context = client.sslContext();
-// engine = context.createSSLEngine();
-// engine.setUseClientMode(true);
-// SSLParameters sslp = client.sslParameters();
-// sslParameters = Utils.copySSLParameters(sslp);
-// if (sn != null) {
-// SNIHostName sni = new SNIHostName(sn);
-// sslParameters.setServerNames(List.of(sni));
-// }
-// if (alpn != null) {
-// sslParameters.setApplicationProtocols(alpn);
-// Log.logSSL("SSLDelegate: Setting application protocols: {0}" + Arrays.toString(alpn));
-// } else {
-// Log.logSSL("SSLDelegate: No application protocols proposed");
-// }
-// engine.setSSLParameters(sslParameters);
-// wrapper = new EngineWrapper(chan, engine);
-// this.chan = chan;
-// this.client = client;
-// }
-
-// SSLParameters getSSLParameters() {
-// return sslParameters;
-// }
-
- static long countBytes(ByteBuffer[] buffers, int start, int number) {
- long c = 0;
- for (int i=0; i<number; i++) {
- c+= buffers[start+i].remaining();
- }
- return c;
- }
-
-
- static class WrapperResult {
- static WrapperResult createOK() {
- WrapperResult r = new WrapperResult();
- r.buf = null;
- r.result = new SSLEngineResult(Status.OK, NOT_HANDSHAKING, 0, 0);
- return r;
- }
- SSLEngineResult result;
-
- ByteBuffer buf; // buffer containing result data
- }
-
- int app_buf_size;
- int packet_buf_size;
-
- enum BufType {
- PACKET,
- APPLICATION
- }
-
- ByteBuffer allocate (BufType type) {
- return allocate (type, -1);
- }
-
- // TODO: Use buffer pool for this
- ByteBuffer allocate (BufType type, int len) {
- assert engine != null;
- synchronized (this) {
- int size;
- if (type == BufType.PACKET) {
- if (packet_buf_size == 0) {
- SSLSession sess = engine.getSession();
- packet_buf_size = sess.getPacketBufferSize();
- }
- if (len > packet_buf_size) {
- packet_buf_size = len;
- }
- size = packet_buf_size;
- } else {
- if (app_buf_size == 0) {
- SSLSession sess = engine.getSession();
- app_buf_size = sess.getApplicationBufferSize();
- }
- if (len > app_buf_size) {
- app_buf_size = len;
- }
- size = app_buf_size;
- }
- return ByteBuffer.allocate (size);
- }
- }
-
- /* reallocates the buffer by :-
- * 1. creating a new buffer double the size of the old one
- * 2. putting the contents of the old buffer into the new one
- * 3. set xx_buf_size to the new size if it was smaller than new size
- *
- * flip is set to true if the old buffer needs to be flipped
- * before it is copied.
- */
- private ByteBuffer realloc (ByteBuffer b, boolean flip, BufType type) {
- // TODO: there should be the linear growth, rather than exponential as
- // we definitely know the maximum amount of space required to unwrap
- synchronized (this) {
- int nsize = 2 * b.capacity();
- ByteBuffer n = allocate (type, nsize);
- if (flip) {
- b.flip();
- }
- n.put(b);
- b = n;
- }
- return b;
- }
-
- /**
- * This is a thin wrapper over SSLEngine and the SocketChannel, which
- * guarantees the ordering of wraps/unwraps with respect to the underlying
- * channel read/writes. It handles the UNDER/OVERFLOW status codes
- * It does not handle the handshaking status codes, or the CLOSED status code
- * though once the engine is closed, any attempt to read/write to it
- * will get an exception. The overall result is returned.
- * It functions synchronously/blocking
- */
- class EngineWrapper {
-
- SocketChannel chan;
- SSLEngine engine;
- final Object wrapLock;
- final Object unwrapLock;
- ByteBuffer unwrap_src, wrap_dst;
- boolean closed = false;
- int u_remaining; // the number of bytes left in unwrap_src after an unwrap()
-
- EngineWrapper (SocketChannel chan, SSLEngine engine) {
- this.chan = chan;
- this.engine = engine;
- wrapLock = new Object();
- unwrapLock = new Object();
- unwrap_src = allocate(BufType.PACKET);
- wrap_dst = allocate(BufType.PACKET);
- }
-
-// void close () throws IOException {
-// }
-
- WrapperResult wrapAndSend(ByteBuffer src, boolean ignoreClose)
- throws IOException
- {
- ByteBuffer[] buffers = new ByteBuffer[1];
- buffers[0] = src;
- return wrapAndSend(buffers, 0, 1, ignoreClose);
- }
-
- /* try to wrap and send the data in src. Handles OVERFLOW.
- * Might block if there is an outbound blockage or if another
- * thread is calling wrap(). Also, might not send any data
- * if an unwrap is needed.
- */
- WrapperResult wrapAndSend(ByteBuffer[] src,
- int offset,
- int len,
- boolean ignoreClose)
- throws IOException
- {
- if (closed && !ignoreClose) {
- throw new IOException ("Engine is closed");
- }
- Status status;
- WrapperResult r = new WrapperResult();
- synchronized (wrapLock) {
- wrap_dst.clear();
- do {
- r.result = engine.wrap (src, offset, len, wrap_dst);
- status = r.result.getStatus();
- if (status == Status.BUFFER_OVERFLOW) {
- wrap_dst = realloc (wrap_dst, true, BufType.PACKET);
- }
- } while (status == Status.BUFFER_OVERFLOW);
- if (status == Status.CLOSED && !ignoreClose) {
- closed = true;
- return r;
- }
- if (r.result.bytesProduced() > 0) {
- wrap_dst.flip();
- int l = wrap_dst.remaining();
- assert l == r.result.bytesProduced();
- while (l>0) {
- l -= chan.write (wrap_dst);
- }
- }
- }
- return r;
- }
-
- /* block until a complete message is available and return it
- * in dst, together with the Result. dst may have been re-allocated
- * so caller should check the returned value in Result
- * If handshaking is in progress then, possibly no data is returned
- */
- WrapperResult recvAndUnwrap(ByteBuffer dst) throws IOException {
- Status status;
- WrapperResult r = new WrapperResult();
- r.buf = dst;
- if (closed) {
- throw new IOException ("Engine is closed");
- }
- boolean needData;
- if (u_remaining > 0) {
- unwrap_src.compact();
- unwrap_src.flip();
- needData = false;
- } else {
- unwrap_src.clear();
- needData = true;
- }
- synchronized (unwrapLock) {
- int x;
- do {
- if (needData) {
- x = chan.read (unwrap_src);
- if (x == -1) {
- throw new IOException ("connection closed for reading");
- }
- unwrap_src.flip();
- }
- r.result = engine.unwrap (unwrap_src, r.buf);
- status = r.result.getStatus();
- if (status == Status.BUFFER_UNDERFLOW) {
- if (unwrap_src.limit() == unwrap_src.capacity()) {
- /* buffer not big enough */
- unwrap_src = realloc (
- unwrap_src, false, BufType.PACKET
- );
- } else {
- /* Buffer not full, just need to read more
- * data off the channel. Reset pointers
- * for reading off SocketChannel
- */
- unwrap_src.position (unwrap_src.limit());
- unwrap_src.limit (unwrap_src.capacity());
- }
- needData = true;
- } else if (status == Status.BUFFER_OVERFLOW) {
- r.buf = realloc (r.buf, true, BufType.APPLICATION);
- needData = false;
- } else if (status == Status.CLOSED) {
- closed = true;
- r.buf.flip();
- return r;
- }
- } while (status != Status.OK);
- }
- u_remaining = unwrap_src.remaining();
- return r;
- }
- }
-
-// WrapperResult sendData (ByteBuffer src) throws IOException {
-// ByteBuffer[] buffers = new ByteBuffer[1];
-// buffers[0] = src;
-// return sendData(buffers, 0, 1);
-// }
-
- /**
- * send the data in the given ByteBuffer. If a handshake is needed
- * then this is handled within this method. When this call returns,
- * all of the given user data has been sent and any handshake has been
- * completed. Caller should check if engine has been closed.
- */
- WrapperResult sendData (ByteBuffer[] src, int offset, int len) throws IOException {
- WrapperResult r = WrapperResult.createOK();
- while (countBytes(src, offset, len) > 0) {
- r = wrapper.wrapAndSend(src, offset, len, false);
- Status status = r.result.getStatus();
- if (status == Status.CLOSED) {
- doClosure ();
- return r;
- }
- HandshakeStatus hs_status = r.result.getHandshakeStatus();
- if (hs_status != HandshakeStatus.FINISHED &&
- hs_status != HandshakeStatus.NOT_HANDSHAKING)
- {
- doHandshake(hs_status);
- }
- }
- return r;
- }
-
- /**
- * read data thru the engine into the given ByteBuffer. If the
- * given buffer was not large enough, a new one is allocated
- * and returned. This call handles handshaking automatically.
- * Caller should check if engine has been closed.
- */
- WrapperResult recvData (ByteBuffer dst) throws IOException {
- /* we wait until some user data arrives */
- int mark = dst.position();
- WrapperResult r = null;
- int pos = dst.position();
- while (dst.position() == pos) {
- r = wrapper.recvAndUnwrap (dst);
- dst = (r.buf != dst) ? r.buf: dst;
- Status status = r.result.getStatus();
- if (status == Status.CLOSED) {
- doClosure ();
- return r;
- }
-
- HandshakeStatus hs_status = r.result.getHandshakeStatus();
- if (hs_status != HandshakeStatus.FINISHED &&
- hs_status != HandshakeStatus.NOT_HANDSHAKING)
- {
- doHandshake (hs_status);
- }
- }
- Utils.flipToMark(dst, mark);
- return r;
- }
-
- /* we've received a close notify. Need to call wrap to send
- * the response
- */
- void doClosure () throws IOException {
- try {
- handshaking.lock();
- ByteBuffer tmp = allocate(BufType.APPLICATION);
- WrapperResult r;
- do {
- tmp.clear();
- tmp.flip ();
- r = wrapper.wrapAndSend(tmp, true);
- } while (r.result.getStatus() != Status.CLOSED);
- } finally {
- handshaking.unlock();
- }
- }
-
- /* do the (complete) handshake after acquiring the handshake lock.
- * If two threads call this at the same time, then we depend
- * on the wrapper methods being idempotent. eg. if wrapAndSend()
- * is called with no data to send then there must be no problem
- */
- @SuppressWarnings("fallthrough")
- void doHandshake (HandshakeStatus hs_status) throws IOException {
- boolean wasBlocking;
- try {
- wasBlocking = chan.isBlocking();
- handshaking.lock();
- chan.configureBlocking(true);
- ByteBuffer tmp = allocate(BufType.APPLICATION);
- while (hs_status != HandshakeStatus.FINISHED &&
- hs_status != HandshakeStatus.NOT_HANDSHAKING)
- {
- WrapperResult r = null;
- switch (hs_status) {
- case NEED_TASK:
- Runnable task;
- while ((task = engine.getDelegatedTask()) != null) {
- /* run in current thread, because we are already
- * running an external Executor
- */
- task.run();
- }
- /* fall thru - call wrap again */
- case NEED_WRAP:
- tmp.clear();
- tmp.flip();
- r = wrapper.wrapAndSend(tmp, false);
- break;
-
- case NEED_UNWRAP:
- tmp.clear();
- r = wrapper.recvAndUnwrap (tmp);
- if (r.buf != tmp) {
- tmp = r.buf;
- }
- assert tmp.position() == 0;
- break;
- }
- hs_status = r.result.getHandshakeStatus();
- }
- Log.logSSL(getSessionInfo());
- if (!wasBlocking) {
- chan.configureBlocking(false);
- }
- } finally {
- handshaking.unlock();
- }
- }
-
-// static void printParams(SSLParameters p) {
-// System.out.println("SSLParameters:");
-// if (p == null) {
-// System.out.println("Null params");
-// return;
-// }
-// for (String cipher : p.getCipherSuites()) {
-// System.out.printf("cipher: %s\n", cipher);
-// }
-// // JDK 8 EXCL START
-// for (String approto : p.getApplicationProtocols()) {
-// System.out.printf("application protocol: %s\n", approto);
-// }
-// // JDK 8 EXCL END
-// for (String protocol : p.getProtocols()) {
-// System.out.printf("protocol: %s\n", protocol);
-// }
-// if (p.getServerNames() != null) {
-// for (SNIServerName sname : p.getServerNames()) {
-// System.out.printf("server name: %s\n", sname.toString());
-// }
-// }
-// }
-
- String getSessionInfo() {
- StringBuilder sb = new StringBuilder();
- String application = engine.getApplicationProtocol();
- SSLSession sess = engine.getSession();
- String cipher = sess.getCipherSuite();
- String protocol = sess.getProtocol();
- sb.append("Handshake complete alpn: ")
- .append(application)
- .append(", Cipher: ")
- .append(cipher)
- .append(", Protocol: ")
- .append(protocol);
- return sb.toString();
- }
-}
--- a/src/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<ByteBuffer> buffersSource;
- private final Object lock = new Object();
- private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
- private final InternalReadPublisher readPublisher;
- private final InternalWriteSubscriber writeSubscriber;
- private final long id = IDS.incrementAndGet();
-
- public SocketTube(HttpClientImpl client, SocketChannel channel,
- Supplier<ByteBuffer> buffersSource) {
- this.client = client;
- this.channel = channel;
- this.buffersSource = buffersSource;
- this.readPublisher = new InternalReadPublisher();
- this.writeSubscriber = new InternalWriteSubscriber();
- }
-
-// private static Flow.Subscription nopSubscription() {
-// return new Flow.Subscription() {
-// @Override public void request(long n) { }
-// @Override public void cancel() { }
-// };
-// }
-
- /**
- * Returns {@code true} if this flow is finished.
- * This happens when this flow internal read subscription is completed,
- * either normally (EOF reading) or exceptionally (EOF writing, or
- * underlying socket closed, or some exception occurred while reading or
- * writing to the socket).
- *
- * @return {@code true} if this flow is finished.
- */
- public boolean isFinished() {
- InternalReadPublisher.InternalReadSubscription subscription =
- readPublisher.subscriptionImpl;
- return subscription != null && subscription.completed
- || subscription == null && errorRef.get() != null;
- }
-
- // ===================================================================== //
- // Flow.Publisher //
- // ======================================================================//
-
- /**
- * {@inheritDoc }
- * @apiNote This method should be called first. In particular, the caller
- * must ensure that this method must be called by the read
- * subscriber before the write publisher can call {@code onSubscribe}.
- * Failure to adhere to this contract may result in assertion errors.
- */
- @Override
- public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
- Objects.requireNonNull(s);
- assert s instanceof TubeSubscriber : "Expected TubeSubscriber, got:" + s;
- readPublisher.subscribe(s);
- }
-
-
- // ===================================================================== //
- // Flow.Subscriber //
- // ======================================================================//
-
- /**
- * {@inheritDoc }
- * @apiNote The caller must ensure that {@code subscribe} is called by
- * the read subscriber before {@code onSubscribe} is called by
- * the write publisher.
- * Failure to adhere to this contract may result in assertion errors.
- */
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- writeSubscriber.onSubscribe(subscription);
- }
-
- @Override
- public void onNext(List<ByteBuffer> item) {
- writeSubscriber.onNext(item);
- }
-
- @Override
- public void onError(Throwable throwable) {
- writeSubscriber.onError(throwable);
- }
-
- @Override
- public void onComplete() {
- writeSubscriber.onComplete();
- }
-
- // ===================================================================== //
- // Events //
- // ======================================================================//
-
- /**
- * A restartable task used to process tasks in sequence.
- */
- private static class SocketFlowTask implements RestartableTask {
- final Runnable task;
- private final Object monitor = new Object();
- SocketFlowTask(Runnable task) {
- this.task = task;
- }
- @Override
- public final void run(DeferredCompleter taskCompleter) {
- try {
- // non contentious synchronized for visibility.
- synchronized(monitor) {
- task.run();
- }
- } finally {
- taskCompleter.complete();
- }
- }
- }
-
- // This is best effort - there's no guarantee that the printed set
- // of values is consistent. It should only be considered as
- // weakly accurate - in particular in what concerns the events states,
- // especially when displaying a read event state from a write event
- // callback and conversely.
- void debugState(String when) {
- if (debug.isLoggable(Level.DEBUG)) {
- StringBuilder state = new StringBuilder();
-
- InternalReadPublisher.InternalReadSubscription sub =
- readPublisher.subscriptionImpl;
- InternalReadPublisher.ReadEvent readEvent =
- sub == null ? null : sub.readEvent;
- Demand rdemand = sub == null ? null : sub.demand;
- InternalWriteSubscriber.WriteEvent writeEvent =
- writeSubscriber.writeEvent;
- AtomicLong wdemand = writeSubscriber.writeDemand;
- int rops = readEvent == null ? 0 : readEvent.interestOps();
- long rd = rdemand == null ? 0 : rdemand.get();
- int wops = writeEvent == null ? 0 : writeEvent.interestOps();
- long wd = wdemand == null ? 0 : wdemand.get();
-
- state.append(when).append(" Reading: [ops=")
- .append(rops).append(", demand=").append(rd)
- .append(", stopped=")
- .append((sub == null ? false : sub.readScheduler.isStopped()))
- .append("], Writing: [ops=").append(wops)
- .append(", demand=").append(wd)
- .append("]");
- debug.log(Level.DEBUG, state.toString());
- }
- }
-
- /**
- * A repeatable event that can be paused or resumed by changing
- * its interestOps.
- * When the event is fired, it is first paused before being signaled.
- * It is the responsibility of the code triggered by {@code signalEvent}
- * to resume the event if required.
- */
- private static abstract class SocketFlowEvent extends AsyncEvent {
- final SocketChannel channel;
- final int defaultInterest;
- volatile int interestOps;
- volatile boolean registered;
- SocketFlowEvent(int defaultInterest, SocketChannel channel) {
- super(AsyncEvent.REPEATING);
- this.defaultInterest = defaultInterest;
- this.channel = channel;
- }
- final boolean registered() {return registered;}
- final void resume() {
- interestOps = defaultInterest;
- registered = true;
- }
- final void pause() {interestOps = 0;}
- @Override
- public final SelectableChannel channel() {return channel;}
- @Override
- public final int interestOps() {return interestOps;}
-
- @Override
- public final void handle() {
- pause(); // pause, then signal
- signalEvent(); // won't be fired again until resumed.
- }
- @Override
- public final void abort(IOException error) {
- debug().log(Level.DEBUG, () -> "abort: " + error);
- pause(); // pause, then signal
- signalError(error); // should not be resumed after abort (not checked)
- }
-
- protected abstract void signalEvent();
- protected abstract void signalError(Throwable error);
- abstract System.Logger debug();
- }
-
- // ===================================================================== //
- // Writing //
- // ======================================================================//
-
- // This class makes the assumption that the publisher will call
- // onNext sequentially, and that onNext won't be called if the demand
- // has not been incremented by request(1).
- // It has a 'queue of 1' meaning that it will call request(1) in
- // onSubscribe, and then only after its 'current' buffer list has been
- // fully written and current set to null;
- private final class InternalWriteSubscriber
- implements Flow.Subscriber<List<ByteBuffer>> {
-
- volatile Flow.Subscription subscription;
- volatile List<ByteBuffer> current;
- volatile boolean completed;
- final WriteEvent writeEvent = new WriteEvent(channel, this);
- final AtomicLong writeDemand = new AtomicLong();
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- Flow.Subscription previous = this.subscription;
- this.subscription = subscription;
- debug.log(Level.DEBUG, "subscribed for writing");
- if (current == null) {
- if (previous == subscription || previous == null) {
- if (writeDemand.compareAndSet(0, 1)) {
- subscription.request(1);
- }
- } else {
- writeDemand.set(1);
- subscription.request(1);
- }
- }
- }
-
- @Override
- public void onNext(List<ByteBuffer> bufs) {
- assert current == null; // this is a queue of 1.
- assert subscription != null;
- current = bufs;
- tryFlushCurrent(client.isSelectorThread()); // may be in selector thread
- // For instance in HTTP/2, a received SETTINGS frame might trigger
- // the sending of a SETTINGS frame in turn which might cause
- // onNext to be called from within the same selector thread that the
- // original SETTINGS frames arrived on. If rs is the read-subscriber
- // and ws is the write-subscriber then the following can occur:
- // ReadEvent -> rs.onNext(bytes) -> process server SETTINGS -> write
- // client SETTINGS -> ws.onNext(bytes) -> tryFlushCurrent
- debugState("leaving w.onNext");
- }
-
- // we don't use a SequentialScheduler here: we rely on
- // onNext() being called sequentially, and not being called
- // if we haven't call request(1)
- // onNext is usually called from within a user/executor thread.
- // we will perform the initial writing in that thread.
- // if for some reason, not all data can be written, the writeEvent
- // will be resumed, and the rest of the data will be written from
- // the selector manager thread when the writeEvent is fired.
- // If we are in the selector manager thread, then we will use the executor
- // to call request(1), ensuring that onNext() won't be called from
- // within the selector thread.
- // If we are not in the selector manager thread, then we don't care.
- void tryFlushCurrent(boolean inSelectorThread) {
- List<ByteBuffer> bufs = current;
- if (bufs == null) return;
- try {
- assert inSelectorThread == client.isSelectorThread() :
- "should " + (inSelectorThread ? "" : "not ")
- + " be in the selector thread";
- long remaining = Utils.remaining(bufs);
- debug.log(Level.DEBUG, "trying to write: %d", remaining);
- long written = writeAvailable(bufs);
- debug.log(Level.DEBUG, "wrote: %d", remaining);
- if (written == -1) {
- signalError(new EOFException("EOF reached while writing"));
- return;
- }
- assert written <= remaining;
- if (remaining - written == 0) {
- current = null;
- writeDemand.decrementAndGet();
- Runnable requestMore = this::requestMore;
- if (inSelectorThread) {
- assert client.isSelectorThread();
- client.theExecutor().execute(requestMore);
- } else {
- assert !client.isSelectorThread();
- requestMore.run();
- }
- } else {
- resumeWriteEvent(inSelectorThread);
- }
- } catch (Throwable t) {
- signalError(t);
- subscription.cancel();
- }
- }
-
- void requestMore() {
- try {
- if (completed) return;
- long d = writeDemand.get();
- if (writeDemand.compareAndSet(0,1)) {
- debug.log(Level.DEBUG, "write: requesting more...");
- subscription.request(1);
- } else {
- debug.log(Level.DEBUG, "write: no need to request more: %d", d);
- }
- } catch (Throwable t) {
- debug.log(Level.DEBUG, () ->
- "write: error while requesting more: " + t);
- signalError(t);
- subscription.cancel();
- } finally {
- debugState("leaving requestMore: ");
- }
- }
-
- @Override
- public void onError(Throwable throwable) {
- signalError(throwable);
- }
-
- @Override
- public void onComplete() {
- completed = true;
- // no need to pause the write event here: the write event will
- // be paused if there is nothing more to write.
- List<ByteBuffer> bufs = current;
- long remaining = bufs == null ? 0 : Utils.remaining(bufs);
- debug.log(Level.DEBUG, "write completed, %d yet to send", remaining);
- debugState("InternalWriteSubscriber::onComplete");
- }
-
- void resumeWriteEvent(boolean inSelectorThread) {
- debug.log(Level.DEBUG, "scheduling write event");
- resumeEvent(writeEvent, this::signalError);
- }
-
-// void pauseWriteEvent() {
-// debug.log(Level.DEBUG, "pausing write event");
-// pauseEvent(writeEvent, this::signalError);
-// }
-
- void signalWritable() {
- debug.log(Level.DEBUG, "channel is writable");
- tryFlushCurrent(true);
- }
-
- void signalError(Throwable error) {
- debug.log(Level.DEBUG, () -> "write error: " + error);
- completed = true;
- readPublisher.signalError(error);
- }
-
- // A repeatable WriteEvent which is paused after firing and can
- // be resumed if required - see SocketFlowEvent;
- final class WriteEvent extends SocketFlowEvent {
- final InternalWriteSubscriber sub;
- WriteEvent(SocketChannel channel, InternalWriteSubscriber sub) {
- super(SelectionKey.OP_WRITE, channel);
- this.sub = sub;
- }
- @Override
- protected final void signalEvent() {
- try {
- client.eventUpdated(this);
- sub.signalWritable();
- } catch(Throwable t) {
- sub.signalError(t);
- }
- }
-
- @Override
- protected void signalError(Throwable error) {
- sub.signalError(error);
- }
-
- @Override
- System.Logger debug() {
- return debug;
- }
-
- }
-
- }
-
- // ===================================================================== //
- // Reading //
- // ===================================================================== //
-
- // The InternalReadPublisher uses a SequentialScheduler to ensure that
- // onNext/onError/onComplete are called sequentially on the caller's
- // subscriber.
- // However, it relies on the fact that the only time where
- // runOrSchedule() is called from a user/executor thread is in signalError,
- // right after the errorRef has been set.
- // Because the sequential scheduler's task always checks for errors first,
- // and always terminate the scheduler on error, then it is safe to assume
- // that if it reaches the point where it reads from the channel, then
- // it is running in the SelectorManager thread. This is because all
- // other invocation of runOrSchedule() are triggered from within a
- // ReadEvent.
- //
- // When pausing/resuming the event, some shortcuts can then be taken
- // when we know we're running in the selector manager thread
- // (in that case there's no need to call client.eventUpdated(readEvent);
- //
- private final class InternalReadPublisher
- implements Flow.Publisher<List<ByteBuffer>> {
- private final InternalReadSubscription subscriptionImpl
- = new InternalReadSubscription();
- AtomicReference<ReadSubscription> pendingSubscription = new AtomicReference<>();
- private volatile ReadSubscription subscription;
-
- @Override
- public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
- Objects.requireNonNull(s);
-
- TubeSubscriber sub = FlowTube.asTubeSubscriber(s);
- ReadSubscription target = new ReadSubscription(subscriptionImpl, sub);
- ReadSubscription previous = pendingSubscription.getAndSet(target);
-
- if (previous != null && previous != target) {
- debug.log(Level.DEBUG,
- () -> "read publisher: dropping pending subscriber: "
- + previous.subscriber);
- previous.errorRef.compareAndSet(null, errorRef.get());
- previous.signalOnSubscribe();
- if (subscriptionImpl.completed) {
- previous.signalCompletion();
- } else {
- previous.subscriber.dropSubscription();
- }
- }
-
- debug.log(Level.DEBUG, "read publisher got subscriber");
- subscriptionImpl.signalSubscribe();
- debugState("leaving read.subscribe: ");
- }
-
- void signalError(Throwable error) {
- if (!errorRef.compareAndSet(null, error)) {
- return;
- }
- subscriptionImpl.handleError();
- }
-
- final class ReadSubscription implements Flow.Subscription {
- final InternalReadSubscription impl;
- final TubeSubscriber subscriber;
- final AtomicReference<Throwable> errorRef = new AtomicReference<>();
- volatile boolean subscribed;
- volatile boolean cancelled;
- volatile boolean completed;
-
- public ReadSubscription(InternalReadSubscription impl,
- TubeSubscriber subscriber) {
- this.impl = impl;
- this.subscriber = subscriber;
- }
-
- @Override
- public void cancel() {
- cancelled = true;
- }
-
- @Override
- public void request(long n) {
- if (!cancelled) {
- impl.request(n);
- } else {
- debug.log(Level.DEBUG,
- "subscription cancelled, ignoring request %d", n);
- }
- }
-
- void signalCompletion() {
- assert subscribed || cancelled;
- if (completed || cancelled) return;
- synchronized (this) {
- if (completed) return;
- completed = true;
- }
- Throwable error = errorRef.get();
- if (error != null) {
- debug.log(Level.DEBUG, () ->
- "forwarding error to subscriber: "
- + error);
- subscriber.onError(error);
- } else {
- debug.log(Level.DEBUG, "completing subscriber");
- subscriber.onComplete();
- }
- }
-
- void signalOnSubscribe() {
- if (subscribed || cancelled) return;
- synchronized (this) {
- if (subscribed || cancelled) return;
- subscribed = true;
- }
- subscriber.onSubscribe(this);
- debug.log(Level.DEBUG, "onSubscribe called");
- if (errorRef.get() != null) {
- signalCompletion();
- }
- }
- }
-
- final class InternalReadSubscription implements Flow.Subscription {
-
- private final Demand demand = new Demand();
- final SequentialScheduler readScheduler;
- private volatile boolean completed;
- private final ReadEvent readEvent;
- private final AsyncEvent subscribeEvent;
-
- InternalReadSubscription() {
- readScheduler = new SequentialScheduler(new SocketFlowTask(this::read));
- subscribeEvent = new AsyncTriggerEvent(this::signalError,
- this::handleSubscribeEvent);
- readEvent = new ReadEvent(channel, this);
- }
-
- /*
- * This method must be invoked before any other method of this class.
- */
- final void signalSubscribe() {
- if (readScheduler.isStopped() || completed) {
- // if already completed or stopped we can handle any
- // pending connection directly from here.
- debug.log(Level.DEBUG,
- "handling pending subscription while completed");
- handlePending();
- } else {
- try {
- debug.log(Level.DEBUG,
- "registering subscribe event");
- client.registerEvent(subscribeEvent);
- } catch (Throwable t) {
- signalError(t);
- handlePending();
- }
- }
- }
-
- final void handleSubscribeEvent() {
- assert client.isSelectorThread();
- debug.log(Level.DEBUG, "subscribe event raised");
- readScheduler.runOrSchedule();
- if (readScheduler.isStopped() || completed) {
- // if already completed or stopped we can handle any
- // pending connection directly from here.
- debug.log(Level.DEBUG,
- "handling pending subscription when completed");
- handlePending();
- }
- }
-
-
- /*
- * Although this method is thread-safe, the Reactive-Streams spec seems
- * to not require it to be as such. It's a responsibility of the
- * subscriber to signal demand in a thread-safe manner.
- *
- * https://github.com/reactive-streams/reactive-streams-jvm/blob/dd24d2ab164d7de6c316f6d15546f957bec29eaa/README.md
- * (rules 2.7 and 3.4)
- */
- @Override
- public final void request(long n) {
- if (n > 0L) {
- boolean wasFulfilled = demand.increase(n);
- if (wasFulfilled) {
- debug.log(Level.DEBUG, "got some demand for reading");
- resumeReadEvent();
- // if demand has been changed from fulfilled
- // to unfulfilled register read event;
- }
- } else {
- signalError(new IllegalArgumentException("non-positive request"));
- }
- debugState("leaving request("+n+"): ");
- }
-
- @Override
- public final void cancel() {
- pauseReadEvent();
- readScheduler.stop();
- }
-
- private void resumeReadEvent() {
- debug.log(Level.DEBUG, "resuming read event");
- resumeEvent(readEvent, this::signalError);
- }
-
- private void pauseReadEvent() {
- debug.log(Level.DEBUG, "pausing read event");
- pauseEvent(readEvent, this::signalError);
- }
-
-
- final void handleError() {
- assert errorRef.get() != null;
- readScheduler.runOrSchedule();
- }
-
- final void signalError(Throwable error) {
- if (!errorRef.compareAndSet(null, error)) {
- return;
- }
- debug.log(Level.DEBUG, () -> "got read error: " + error);
- readScheduler.runOrSchedule();
- }
-
- final void signalReadable() {
- readScheduler.runOrSchedule();
- }
-
- /** The body of the task that runs in SequentialScheduler. */
- final void read() {
- // It is important to only call pauseReadEvent() when stopping
- // the scheduler. The event is automatically paused before
- // firing, and trying to pause it again could cause a race
- // condition between this loop, which calls tryDecrementDemand(),
- // and the thread that calls request(n), which will try to resume
- // reading.
- try {
- while(!readScheduler.isStopped()) {
- if (completed) return;
-
- // make sure we have a subscriber
- if (handlePending()) {
- debug.log(Level.DEBUG, "pending subscriber subscribed");
- return;
- }
-
- // If an error was signaled, we might not be in the
- // the selector thread, and that is OK, because we
- // will just call onError and return.
- ReadSubscription current = subscription;
- TubeSubscriber subscriber = current.subscriber;
- Throwable error = errorRef.get();
- if (error != null) {
- completed = true;
- // safe to pause here because we're finished anyway.
- pauseReadEvent();
- debug.log(Level.DEBUG, () -> "Sending error " + error
- + " to subscriber " + subscriber);
- current.errorRef.compareAndSet(null, error);
- current.signalCompletion();
- readScheduler.stop();
- debugState("leaving read() loop with error: ");
- return;
- }
-
- // If we reach here then we must be in the selector thread.
- assert client.isSelectorThread();
- if (demand.tryDecrement()) {
- // we have demand.
- try {
- List<ByteBuffer> bytes = readAvailable();
- if (bytes == EOF) {
- if (!completed) {
- debug.log(Level.DEBUG, "got read EOF");
- completed = true;
- // safe to pause here because we're finished
- // anyway.
- pauseReadEvent();
- current.signalCompletion();
- readScheduler.stop();
- }
- debugState("leaving read() loop after EOF: ");
- return;
- } else if (Utils.remaining(bytes) > 0) {
- // the subscriber is responsible for offloading
- // to another thread if needed.
- debug.log(Level.DEBUG, () -> "read bytes: "
- + Utils.remaining(bytes));
- assert !current.completed;
- subscriber.onNext(bytes);
- // we could continue looping until the demand
- // reaches 0. However, that would risk starving
- // other connections (bound to other socket
- // channels) - as other selected keys activated
- // by the selector manager thread might be
- // waiting for this event to terminate.
- // So resume the read event and return now...
- resumeReadEvent();
- debugState("leaving read() loop after onNext: ");
- return;
- } else {
- // nothing available!
- debug.log(Level.DEBUG, "no more bytes available");
- // re-increment the demand and resume the read
- // event. This ensures that this loop is
- // executed again when the socket becomes
- // readable again.
- demand.increase(1);
- resumeReadEvent();
- debugState("leaving read() loop with no bytes");
- return;
- }
- } catch (Throwable x) {
- signalError(x);
- continue;
- }
- } else {
- debug.log(Level.DEBUG, "no more demand for reading");
- // the event is paused just after firing, so it should
- // still be paused here, unless the demand was just
- // incremented from 0 to n, in which case, the
- // event will be resumed, causing this loop to be
- // invoked again when the socket becomes readable:
- // This is what we want.
- // Trying to pause the event here would actually
- // introduce a race condition between this loop and
- // request(n).
- debugState("leaving read() loop with no demand");
- break;
- }
- }
- } catch (Throwable t) {
- debug.log(Level.DEBUG, "Unexpected exception in read loop", t);
- signalError(t);
- } finally {
- handlePending();
- }
- }
-
- boolean handlePending() {
- ReadSubscription pending = pendingSubscription.getAndSet(null);
- if (pending == null) return false;
- debug.log(Level.DEBUG, "handling pending subscription for %s",
- pending.subscriber);
- ReadSubscription current = subscription;
- if (current != null && current != pending && !completed) {
- current.subscriber.dropSubscription();
- }
- debug.log(Level.DEBUG, "read demand reset to 0");
- subscriptionImpl.demand.reset(); // subscriber will increase demand if it needs to.
- pending.errorRef.compareAndSet(null, errorRef.get());
- if (!readScheduler.isStopped()) {
- subscription = pending;
- } else {
- debug.log(Level.DEBUG, "socket tube is already stopped");
- }
- debug.log(Level.DEBUG, "calling onSubscribe");
- pending.signalOnSubscribe();
- if (completed) {
- pending.errorRef.compareAndSet(null, errorRef.get());
- pending.signalCompletion();
- }
- return true;
- }
- }
-
-
- // A repeatable ReadEvent which is paused after firing and can
- // be resumed if required - see SocketFlowEvent;
- final class ReadEvent extends SocketFlowEvent {
- final InternalReadSubscription sub;
- ReadEvent(SocketChannel channel, InternalReadSubscription sub) {
- super(SelectionKey.OP_READ, channel);
- this.sub = sub;
- }
- @Override
- protected final void signalEvent() {
- try {
- client.eventUpdated(this);
- sub.signalReadable();
- } catch(Throwable t) {
- sub.signalError(t);
- }
- }
-
- @Override
- protected final void signalError(Throwable error) {
- sub.signalError(error);
- }
-
- @Override
- System.Logger debug() {
- return debug;
- }
- }
-
- }
-
- // ===================================================================== //
- // Socket Channel Read/Write //
- // ===================================================================== //
- static final int MAX_BUFFERS = 3;
- static final List<ByteBuffer> EOF = List.of();
-
- private List<ByteBuffer> readAvailable() throws IOException {
- ByteBuffer buf = buffersSource.get();
- assert buf.hasRemaining();
-
- int read;
- int pos = buf.position();
- List<ByteBuffer> list = null;
- while (buf.hasRemaining()) {
- while ((read = channel.read(buf)) > 0) {
- if (!buf.hasRemaining()) break;
- }
-
- // nothing read;
- if (buf.position() == pos) {
- // An empty list signal the end of data, and should only be
- // returned if read == -1.
- // If we already read some data, then we must return what we have
- // read, and -1 will be returned next time the caller attempts to
- // read something.
- if (list == null && read == -1) { // eof
- list = EOF;
- break;
- }
- }
- buf.limit(buf.position());
- buf.position(pos);
- if (list == null) {
- list = List.of(buf);
- } else {
- if (!(list instanceof ArrayList)) {
- list = new ArrayList<>(list);
- }
- list.add(buf);
- }
- if (read <= 0 || list.size() == MAX_BUFFERS) break;
- buf = buffersSource.get();
- pos = buf.position();
- assert buf.hasRemaining();
- }
- return list;
- }
-
- private long writeAvailable(List<ByteBuffer> bytes) throws IOException {
- ByteBuffer[] srcs = bytes.toArray(Utils.EMPTY_BB_ARRAY);
- final long remaining = Utils.remaining(srcs);
- long written = 0;
- while (remaining > written) {
- long w = channel.write(srcs);
- if (w == -1 && written == 0) return -1;
- if (w == 0) break;
- written += w;
- }
- return written;
- }
-
- private void resumeEvent(SocketFlowEvent event,
- Consumer<Throwable> errorSignaler) {
- boolean registrationRequired;
- synchronized(lock) {
- registrationRequired = !event.registered();
- event.resume();
- }
- try {
- if (registrationRequired) {
- client.registerEvent(event);
- } else {
- client.eventUpdated(event);
- }
- } catch(Throwable t) {
- errorSignaler.accept(t);
- }
- }
-
- private void pauseEvent(SocketFlowEvent event,
- Consumer<Throwable> errorSignaler) {
- synchronized(lock) {
- event.pause();
- }
- try {
- client.eventUpdated(event);
- } catch(Throwable t) {
- errorSignaler.accept(t);
- }
- }
-
- @Override
- public void connectFlows(TubePublisher writePublisher,
- TubeSubscriber readSubscriber) {
- debug.log(Level.DEBUG, "connecting flows");
- this.subscribe(readSubscriber);
- writePublisher.subscribe(this);
- }
-
-
- @Override
- public String toString() {
- return dbgString();
- }
-
- final String dbgString() {
- return "SocketTube("+id+")";
- }
-}
--- a/src/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<HttpResponse> and the the first one on the list is completed
- * with the next response
- *
- * getResponseAsync() -- queries list of response CFs and returns first one
- * if one exists. Otherwise, creates one and adds it to list
- * and returns it. Completion is achieved through the
- * incoming() upcall from connection reader thread.
- *
- * getResponse() -- calls getResponseAsync() and waits for CF to complete
- *
- * responseBodyAsync() -- calls responseBody() in an executor thread.
- *
- * incoming() -- entry point called from connection reader thread. Frames are
- * either handled immediately without blocking or for data frames
- * placed on the stream's inputQ which is consumed by the stream's
- * reader thread.
- *
- * PushedStream sub class
- * ======================
- * Sending side methods are not used because the request comes from a PUSH_PROMISE
- * frame sent by the server. When a PUSH_PROMISE is received the PushedStream
- * is created. PushedStream does not use responseCF list as there can be only
- * one response. The CF is created when the object created and when the response
- * HEADERS frame is received the object is completed.
- */
-class Stream<T> extends ExchangeImpl<T> {
-
- final static boolean DEBUG = Utils.DEBUG; // Revisit: temporary developer's flag
- final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
-
- final ConcurrentLinkedQueue<Http2Frame> inputQ = new ConcurrentLinkedQueue<>();
- final SequentialScheduler sched =
- SequentialScheduler.synchronizedScheduler(this::schedule);
- final SubscriptionBase userSubscription = new SubscriptionBase(sched, this::cancel);
-
- /**
- * This stream's identifier. Assigned lazily by the HTTP2Connection before
- * the stream's first frame is sent.
- */
- protected volatile int streamid;
-
- long requestContentLen;
-
- final Http2Connection connection;
- final HttpRequestImpl request;
- final DecodingCallback rspHeadersConsumer;
- HttpHeadersImpl responseHeaders;
- final HttpHeadersImpl requestPseudoHeaders;
- volatile HttpResponse.BodySubscriber<T> responseSubscriber;
- final HttpRequest.BodyPublisher requestPublisher;
- volatile RequestSubscriber requestSubscriber;
- volatile int responseCode;
- volatile Response response;
- volatile Throwable failed; // The exception with which this stream was canceled.
- final CompletableFuture<Void> requestBodyCF = new MinimalFuture<>();
- volatile CompletableFuture<T> responseBodyCF;
-
- /** True if END_STREAM has been seen in a frame received on this stream. */
- private volatile boolean remotelyClosed;
- private volatile boolean closed;
- private volatile boolean endStreamSent;
-
- // state flags
- private boolean requestSent, responseReceived;
-
- /**
- * A reference to this Stream's connection Send Window controller. The
- * stream MUST acquire the appropriate amount of Send Window before
- * sending any data. Will be null for PushStreams, as they cannot send data.
- */
- private final WindowController windowController;
- private final WindowUpdateSender windowUpdater;
-
- @Override
- HttpConnection connection() {
- return connection.connection;
- }
-
- /**
- * Invoked either from incoming() -> {receiveDataFrame() or receiveResetFrame() }
- * of after user subscription window has re-opened, from SubscriptionBase.request()
- */
- private void schedule() {
- if (responseSubscriber == null)
- // can't process anything yet
- return;
-
- try {
- while (!inputQ.isEmpty()) {
- Http2Frame frame = inputQ.peek();
- if (frame instanceof ResetFrame) {
- inputQ.remove();
- handleReset((ResetFrame)frame);
- return;
- }
- DataFrame df = (DataFrame)frame;
- boolean finished = df.getFlag(DataFrame.END_STREAM);
-
- List<ByteBuffer> buffers = df.getData();
- List<ByteBuffer> dsts = Collections.unmodifiableList(buffers);
- int size = Utils.remaining(dsts, Integer.MAX_VALUE);
- if (size == 0 && finished) {
- inputQ.remove();
- Log.logTrace("responseSubscriber.onComplete");
- debug.log(Level.DEBUG, "incoming: onComplete");
- sched.stop();
- responseSubscriber.onComplete();
- setEndStreamReceived();
- return;
- } else if (userSubscription.tryDecrement()) {
- inputQ.remove();
- Log.logTrace("responseSubscriber.onNext {0}", size);
- debug.log(Level.DEBUG, "incoming: onNext(%d)", size);
- responseSubscriber.onNext(dsts);
- if (consumed(df)) {
- Log.logTrace("responseSubscriber.onComplete");
- debug.log(Level.DEBUG, "incoming: onComplete");
- sched.stop();
- responseSubscriber.onComplete();
- setEndStreamReceived();
- return;
- }
- } else {
- return;
- }
- }
- } catch (Throwable throwable) {
- failed = throwable;
- }
-
- Throwable t = failed;
- if (t != null) {
- sched.stop();
- responseSubscriber.onError(t);
- close();
- }
- }
-
- // Callback invoked after the Response BodySubscriber has consumed the
- // buffers contained in a DataFrame.
- // Returns true if END_STREAM is reached, false otherwise.
- private boolean consumed(DataFrame df) {
- // RFC 7540 6.1:
- // The entire DATA frame payload is included in flow control,
- // including the Pad Length and Padding fields if present
- int len = df.payloadLength();
- connection.windowUpdater.update(len);
-
- if (!df.getFlag(DataFrame.END_STREAM)) {
- // Don't send window update on a stream which is
- // closed or half closed.
- windowUpdater.update(len);
- return false; // more data coming
- }
- return true; // end of stream
- }
-
- @Override
- CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
- boolean returnConnectionToPool,
- Executor executor)
- {
- Log.logTrace("Reading body on stream {0}", streamid);
- BodySubscriber<T> bodySubscriber = handler.apply(responseCode, responseHeaders);
- CompletableFuture<T> cf = receiveData(bodySubscriber, 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<T> receiveData(BodySubscriber<T> 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<ExchangeImpl<T>> sendBodyAsync() {
- return sendBodyImpl().thenApply( v -> this);
- }
-
- @SuppressWarnings("unchecked")
- Stream(Http2Connection connection,
- Exchange<T> e,
- WindowController windowController)
- {
- super(e);
- this.connection = connection;
- this.windowController = windowController;
- this.request = e.request();
- this.requestPublisher = request.requestPublisher; // may be null
- responseHeaders = new HttpHeadersImpl();
- rspHeadersConsumer = (name, value) -> {
- responseHeaders.addHeader(name.toString(), value.toString());
- if (Log.headers() && Log.trace()) {
- Log.logTrace("RECEIVED HEADER (streamid={0}): {1}: {2}",
- streamid, name, value);
- }
- };
- this.requestPseudoHeaders = new HttpHeadersImpl();
- // NEW
- this.windowUpdater = new StreamWindowUpdateSender(connection);
- }
-
- /**
- * Entry point from Http2Connection reader thread.
- *
- * Data frames will be removed by response body thread.
- */
- void incoming(Http2Frame frame) throws IOException {
- debug.log(Level.DEBUG, "incoming: %s", frame);
- if ((frame instanceof HeaderFrame)) {
- HeaderFrame hframe = (HeaderFrame)frame;
- if (hframe.endHeaders()) {
- Log.logTrace("handling response (streamid={0})", streamid);
- handleResponse();
- if (hframe.getFlag(HeaderFrame.END_STREAM)) {
- receiveDataFrame(new DataFrame(streamid, DataFrame.END_STREAM, List.of()));
- }
- }
- } else if (frame instanceof DataFrame) {
- receiveDataFrame((DataFrame)frame);
- } else {
- otherFrame(frame);
- }
- }
-
- void otherFrame(Http2Frame frame) throws IOException {
- switch (frame.type()) {
- case WindowUpdateFrame.TYPE:
- incoming_windowUpdate((WindowUpdateFrame) frame);
- break;
- case ResetFrame.TYPE:
- incoming_reset((ResetFrame) frame);
- break;
- case PriorityFrame.TYPE:
- incoming_priority((PriorityFrame) frame);
- break;
- default:
- String msg = "Unexpected frame: " + frame.toString();
- throw new IOException(msg);
- }
- }
-
- // The Hpack decoder decodes into one of these consumers of name,value pairs
-
- DecodingCallback rspHeadersConsumer() {
- return rspHeadersConsumer;
- }
-
- protected void handleResponse() throws IOException {
- responseCode = (int)responseHeaders
- .firstValueAsLong(":status")
- .orElseThrow(() -> new IOException("no statuscode in response"));
-
- response = new Response(
- request, exchange, responseHeaders,
- responseCode, HttpClient.Version.HTTP_2);
-
- /* TODO: review if needs to be removed
- the value is not used, but in case `content-length` doesn't parse as
- long, there will be NumberFormatException. If left as is, make sure
- code up the stack handles NFE correctly. */
- responseHeaders.firstValueAsLong("content-length");
-
- if (Log.headers()) {
- StringBuilder sb = new StringBuilder("RESPONSE HEADERS:\n");
- Log.dumpHeaders(sb, " ", responseHeaders);
- Log.logHeaders(sb.toString());
- }
-
- completeResponse(response);
- }
-
- void incoming_reset(ResetFrame frame) {
- Log.logTrace("Received RST_STREAM on stream {0}", streamid);
- if (endStreamReceived()) {
- Log.logTrace("Ignoring RST_STREAM frame received on remotely closed stream {0}", streamid);
- } else if (closed) {
- Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
- } else {
- // put it in the input queue in order to read all
- // pending data frames first. Indeed, a server may send
- // RST_STREAM after sending END_STREAM, in which case we should
- // ignore it. However, we won't know if we have received END_STREAM
- // or not until all pending data frames are read.
- receiveResetFrame(frame);
- // RST_STREAM was pushed to the queue. It will be handled by
- // asyncReceive after all pending data frames have been
- // processed.
- Log.logTrace("RST_STREAM pushed in queue for stream {0}", streamid);
- }
- }
-
- void handleReset(ResetFrame frame) {
- Log.logTrace("Handling RST_STREAM on stream {0}", streamid);
- if (!closed) {
- close();
- int error = frame.getErrorCode();
- completeResponseExceptionally(new IOException(ErrorFrame.stringForCode(error)));
- } else {
- Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
- }
- }
-
- void incoming_priority(PriorityFrame frame) {
- // TODO: implement priority
- throw new UnsupportedOperationException("Not implemented");
- }
-
- private void incoming_windowUpdate(WindowUpdateFrame frame)
- throws IOException
- {
- int amount = frame.getUpdate();
- if (amount <= 0) {
- Log.logTrace("Resetting stream: {0} %d, Window Update amount: %d\n",
- streamid, streamid, amount);
- connection.resetStream(streamid, ResetFrame.FLOW_CONTROL_ERROR);
- } else {
- assert streamid != 0;
- boolean success = windowController.increaseStreamWindow(amount, streamid);
- if (!success) { // overflow
- connection.resetStream(streamid, ResetFrame.FLOW_CONTROL_ERROR);
- }
- }
- }
-
- void incoming_pushPromise(HttpRequestImpl pushRequest,
- PushedStream<T> pushStream)
- throws IOException
- {
- if (Log.requests()) {
- Log.logRequest("PUSH_PROMISE: " + pushRequest.toString());
- }
- PushGroup<T> pushGroup = exchange.getPushGroup();
- if (pushGroup == null) {
- Log.logTrace("Rejecting push promise stream " + streamid);
- connection.resetStream(pushStream.streamid, ResetFrame.REFUSED_STREAM);
- pushStream.close();
- return;
- }
-
- PushGroup.Acceptor<T> acceptor = pushGroup.acceptPushRequest(pushRequest);
-
- if (!acceptor.accepted()) {
- // cancel / reject
- IOException ex = new IOException("Stream " + streamid + " cancelled by users handler");
- if (Log.trace()) {
- Log.logTrace("No body subscriber for {0}: {1}", pushRequest,
- ex.getMessage());
- }
- pushStream.cancelImpl(ex);
- return;
- }
-
- CompletableFuture<HttpResponse<T>> pushResponseCF = acceptor.cf();
- HttpResponse.BodyHandler<T> pushHandler = acceptor.bodyHandler();
- assert pushHandler != null;
-
- pushStream.requestSent();
- pushStream.setPushHandler(pushHandler); // TODO: could wrap the handler to throw on acceptPushPromise ?
- // setup housekeeping for when the push is received
- // TODO: deal with ignoring of CF anti-pattern
- CompletableFuture<HttpResponse<T>> cf = pushStream.responseCF();
- cf.whenComplete((HttpResponse<T> resp, Throwable t) -> {
- t = Utils.getCompletionCause(t);
- if (Log.trace()) {
- Log.logTrace("Push completed on stream {0} for {1}{2}",
- pushStream.streamid, resp,
- ((t==null) ? "": " with exception " + t));
- }
- if (t != null) {
- pushGroup.pushError(t);
- pushResponseCF.completeExceptionally(t);
- } else {
- pushResponseCF.complete(resp);
- }
- pushGroup.pushCompleted();
- });
-
- }
-
- private OutgoingHeaders<Stream<T>> headerFrame(long contentLength) {
- HttpHeadersImpl h = request.getSystemHeaders();
- if (contentLength > 0) {
- h.setHeader("content-length", Long.toString(contentLength));
- }
- setPseudoHeaderFields();
- HttpHeaders sysh = filter(h);
- HttpHeaders userh = filter(request.getUserHeaders());
- OutgoingHeaders<Stream<T>> f = new OutgoingHeaders<>(sysh, userh, this);
- if (contentLength == 0) {
- f.setFlag(HeadersFrame.END_STREAM);
- endStreamSent = true;
- }
- return f;
- }
-
- private boolean hasProxyAuthorization(HttpHeaders headers) {
- return headers.firstValue("proxy-authorization")
- .isPresent();
- }
-
- // Determines whether we need to build a new HttpHeader object.
- //
- // Ideally we should pass the filter to OutgoingHeaders refactor the
- // code that creates the HeaderFrame to honor the filter.
- // We're not there yet - so depending on the filter we need to
- // apply and the content of the header we will try to determine
- // whether anything might need to be filtered.
- // If nothing needs filtering then we can just use the
- // original headers.
- private boolean needsFiltering(HttpHeaders headers,
- BiPredicate<String, List<String>> filter) {
- if (filter == Utils.PROXY_TUNNEL_FILTER || filter == Utils.PROXY_FILTER) {
- // we're either connecting or proxying
- // slight optimization: we only need to filter out
- // disabled schemes, so if there are none just
- // pass through.
- return Utils.proxyHasDisabledSchemes(filter == Utils.PROXY_TUNNEL_FILTER)
- && hasProxyAuthorization(headers);
- } else {
- // we're talking to a server, either directly or through
- // a tunnel.
- // Slight optimization: we only need to filter out
- // proxy authorization headers, so if there are none just
- // pass through.
- return hasProxyAuthorization(headers);
- }
- }
-
- private HttpHeaders filter(HttpHeaders headers) {
- HttpConnection conn = connection();
- BiPredicate<String, List<String>> filter =
- conn.headerFilter(request);
- if (needsFiltering(headers, filter)) {
- return ImmutableHeaders.of(headers.map(), filter);
- }
- return headers;
- }
-
- private void setPseudoHeaderFields() {
- HttpHeadersImpl hdrs = requestPseudoHeaders;
- String method = request.method();
- hdrs.setHeader(":method", method);
- URI uri = request.uri();
- hdrs.setHeader(":scheme", uri.getScheme());
- // TODO: userinfo deprecated. Needs to be removed
- hdrs.setHeader(":authority", uri.getAuthority());
- // TODO: ensure header names beginning with : not in user headers
- String query = uri.getQuery();
- String path = uri.getPath();
- if (path == null || path.isEmpty()) {
- if (method.equalsIgnoreCase("OPTIONS")) {
- path = "*";
- } else {
- path = "/";
- }
- }
- if (query != null) {
- path += "?" + query;
- }
- hdrs.setHeader(":path", path);
- }
-
- HttpHeadersImpl getRequestPseudoHeaders() {
- return requestPseudoHeaders;
- }
-
- /** Sets endStreamReceived. Should be called only once. */
- void setEndStreamReceived() {
- assert remotelyClosed == false: "Unexpected endStream already set";
- remotelyClosed = true;
- responseReceived();
- }
-
- /** Tells whether, or not, the END_STREAM Flag has been seen in any frame
- * received on this stream. */
- private boolean endStreamReceived() {
- return remotelyClosed;
- }
-
- @Override
- CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
- debug.log(Level.DEBUG, "sendHeadersOnly()");
- if (Log.requests() && request != null) {
- Log.logRequest(request.toString());
- }
- if (requestPublisher != null) {
- requestContentLen = requestPublisher.contentLength();
- } else {
- requestContentLen = 0;
- }
- OutgoingHeaders<Stream<T>> f = headerFrame(requestContentLen);
- connection.sendFrame(f);
- CompletableFuture<ExchangeImpl<T>> cf = new MinimalFuture<>();
- cf.complete(this); // #### good enough for now
- return cf;
- }
-
- @Override
- void released() {
- if (streamid > 0) {
- debug.log(Level.DEBUG, "Released stream %d", streamid);
- // remove this stream from the Http2Connection map.
- connection.closeStream(streamid);
- } else {
- debug.log(Level.DEBUG, "Can't release stream %d", streamid);
- }
- }
-
- @Override
- void completed() {
- // There should be nothing to do here: the stream should have
- // been already closed (or will be closed shortly after).
- }
-
- void registerStream(int id) {
- this.streamid = id;
- connection.putStream(this, streamid);
- debug.log(Level.DEBUG, "Registered stream %d", id);
- }
-
- void signalWindowUpdate() {
- RequestSubscriber subscriber = requestSubscriber;
- assert subscriber != null;
- debug.log(Level.DEBUG, "Signalling window update");
- subscriber.sendScheduler.runOrSchedule();
- }
-
- static final ByteBuffer COMPLETED = ByteBuffer.allocate(0);
- class RequestSubscriber implements Flow.Subscriber<ByteBuffer> {
- // can be < 0 if the actual length is not known.
- private final long contentLength;
- private volatile long remainingContentLength;
- private volatile Subscription subscription;
-
- // Holds the outgoing data. There will be at most 2 outgoing ByteBuffers.
- // 1) The data that was published by the request body Publisher, and
- // 2) the COMPLETED sentinel, since onComplete can be invoked without demand.
- final ConcurrentLinkedDeque<ByteBuffer> outgoing = new ConcurrentLinkedDeque<>();
-
- private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
- // A scheduler used to honor window updates. Writing must be paused
- // when the window is exhausted, and resumed when the window acquires
- // some space. The sendScheduler makes it possible to implement this
- // behaviour in an asynchronous non-blocking way.
- // See RequestSubscriber::trySend below.
- final SequentialScheduler sendScheduler;
-
- RequestSubscriber(long contentLen) {
- this.contentLength = contentLen;
- this.remainingContentLength = contentLen;
- this.sendScheduler =
- SequentialScheduler.synchronizedScheduler(this::trySend);
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- if (this.subscription != null) {
- throw new IllegalStateException("already subscribed");
- }
- this.subscription = subscription;
- debug.log(Level.DEBUG, "RequestSubscriber: onSubscribe, request 1");
- subscription.request(1);
- }
-
- @Override
- public void onNext(ByteBuffer item) {
- debug.log(Level.DEBUG, "RequestSubscriber: onNext(%d)", item.remaining());
- int size = outgoing.size();
- assert size == 0 : "non-zero size: " + size;
- onNextImpl(item);
- }
-
- private void onNextImpl(ByteBuffer item) {
- // Got some more request body bytes to send.
- if (requestBodyCF.isDone()) {
- // stream already cancelled, probably in timeout
- sendScheduler.stop();
- subscription.cancel();
- return;
- }
- outgoing.add(item);
- sendScheduler.runOrSchedule();
- }
-
- @Override
- public void onError(Throwable throwable) {
- debug.log(Level.DEBUG, () -> "RequestSubscriber: onError: " + throwable);
- // ensure that errors are handled within the flow.
- if (errorRef.compareAndSet(null, throwable)) {
- sendScheduler.runOrSchedule();
- }
- }
-
- @Override
- public void onComplete() {
- debug.log(Level.DEBUG, "RequestSubscriber: onComplete");
- int size = outgoing.size();
- assert size == 0 || size == 1 : "non-zero or one size: " + size;
- // last byte of request body has been obtained.
- // ensure that everything is completed within the flow.
- onNextImpl(COMPLETED);
- }
-
- // Attempts to send the data, if any.
- // Handles errors and completion state.
- // Pause writing if the send window is exhausted, resume it if the
- // send window has some bytes that can be acquired.
- void trySend() {
- try {
- // handle errors raised by onError;
- Throwable t = errorRef.get();
- if (t != null) {
- sendScheduler.stop();
- if (requestBodyCF.isDone()) return;
- subscription.cancel();
- requestBodyCF.completeExceptionally(t);
- return;
- }
-
- do {
- // handle COMPLETED;
- ByteBuffer item = outgoing.peekFirst();
- if (item == null) return;
- else if (item == COMPLETED) {
- sendScheduler.stop();
- complete();
- return;
- }
-
- // handle bytes to send downstream
- while (item.hasRemaining()) {
- debug.log(Level.DEBUG, "trySend: %d", item.remaining());
- assert !endStreamSent : "internal error, send data after END_STREAM flag";
- DataFrame df = getDataFrame(item);
- if (df == null) {
- debug.log(Level.DEBUG, "trySend: can't send yet: %d",
- item.remaining());
- return; // the send window is exhausted: come back later
- }
-
- if (contentLength > 0) {
- remainingContentLength -= df.getDataLength();
- if (remainingContentLength < 0) {
- String msg = connection().getConnectionFlow()
- + " stream=" + streamid + " "
- + "[" + Thread.currentThread().getName() + "] "
- + "Too many bytes in request body. Expected: "
- + contentLength + ", got: "
- + (contentLength - remainingContentLength);
- connection.resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
- throw new IOException(msg);
- } else if (remainingContentLength == 0) {
- df.setFlag(DataFrame.END_STREAM);
- endStreamSent = true;
- }
- }
- debug.log(Level.DEBUG, "trySend: sending: %d", df.getDataLength());
- connection.sendDataFrame(df);
- }
- assert !item.hasRemaining();
- ByteBuffer b = outgoing.removeFirst();
- assert b == item;
- } while (outgoing.peekFirst() != null);
-
- debug.log(Level.DEBUG, "trySend: request 1");
- subscription.request(1);
- } catch (Throwable ex) {
- debug.log(Level.DEBUG, "trySend: ", ex);
- sendScheduler.stop();
- subscription.cancel();
- requestBodyCF.completeExceptionally(ex);
- }
- }
-
- private void complete() throws IOException {
- long remaining = remainingContentLength;
- long written = contentLength - remaining;
- if (remaining > 0) {
- connection.resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
- // let trySend() handle the exception
- throw new IOException(connection().getConnectionFlow()
- + " stream=" + streamid + " "
- + "[" + Thread.currentThread().getName() +"] "
- + "Too few bytes returned by the publisher ("
- + written + "/"
- + contentLength + ")");
- }
- if (!endStreamSent) {
- endStreamSent = true;
- connection.sendDataFrame(getEmptyEndStreamDataFrame());
- }
- requestBodyCF.complete(null);
- }
- }
-
- /**
- * Send a RESET frame to tell server to stop sending data on this stream
- */
- @Override
- public CompletableFuture<Void> ignoreBody() {
- try {
- connection.resetStream(streamid, ResetFrame.STREAM_CLOSED);
- return MinimalFuture.completedFuture(null);
- } catch (Throwable e) {
- Log.logTrace("Error resetting stream {0}", e.toString());
- return MinimalFuture.failedFuture(e);
- }
- }
-
- DataFrame getDataFrame(ByteBuffer buffer) {
- int requestAmount = Math.min(connection.getMaxSendFrameSize(), buffer.remaining());
- // blocks waiting for stream send window, if exhausted
- int actualAmount = windowController.tryAcquire(requestAmount, streamid, this);
- if (actualAmount <= 0) return null;
- ByteBuffer outBuf = Utils.sliceWithLimitedCapacity(buffer, actualAmount);
- DataFrame df = new DataFrame(streamid, 0 , outBuf);
- return df;
- }
-
- private DataFrame getEmptyEndStreamDataFrame() {
- return new DataFrame(streamid, DataFrame.END_STREAM, List.of());
- }
-
- /**
- * A List of responses relating to this stream. Normally there is only
- * one response, but intermediate responses like 100 are allowed
- * and must be passed up to higher level before continuing. Deals with races
- * such as if responses are returned before the CFs get created by
- * getResponseAsync()
- */
-
- final List<CompletableFuture<Response>> response_cfs = new ArrayList<>(5);
-
- @Override
- CompletableFuture<Response> getResponseAsync(Executor executor) {
- CompletableFuture<Response> cf;
- // The code below deals with race condition that can be caused when
- // completeResponse() is being called before getResponseAsync()
- synchronized (response_cfs) {
- if (!response_cfs.isEmpty()) {
- // This CompletableFuture was created by completeResponse().
- // it will be already completed.
- cf = response_cfs.remove(0);
- // if we find a cf here it should be already completed.
- // finding a non completed cf should not happen. just assert it.
- assert cf.isDone() : "Removing uncompleted response: could cause code to hang!";
- } else {
- // getResponseAsync() is called first. Create a CompletableFuture
- // that will be completed by completeResponse() when
- // completeResponse() is called.
- cf = new MinimalFuture<>();
- response_cfs.add(cf);
- }
- }
- if (executor != null && !cf.isDone()) {
- // protect from executing later chain of CompletableFuture operations from SelectorManager thread
- cf = cf.thenApplyAsync(r -> r, executor);
- }
- Log.logTrace("Response future (stream={0}) is: {1}", streamid, cf);
- PushGroup<?> pg = exchange.getPushGroup();
- if (pg != null) {
- // if an error occurs make sure it is recorded in the PushGroup
- cf = cf.whenComplete((t,e) -> pg.pushError(Utils.getCompletionCause(e)));
- }
- return cf;
- }
-
- /**
- * Completes the first uncompleted CF on list, and removes it. If there is no
- * uncompleted CF then creates one (completes it) and adds to list
- */
- void completeResponse(Response resp) {
- synchronized (response_cfs) {
- CompletableFuture<Response> cf;
- int cfs_len = response_cfs.size();
- for (int i=0; i<cfs_len; i++) {
- cf = response_cfs.get(i);
- if (!cf.isDone()) {
- Log.logTrace("Completing response (streamid={0}): {1}",
- streamid, cf);
- cf.complete(resp);
- response_cfs.remove(cf);
- return;
- } // else we found the previous response: just leave it alone.
- }
- cf = MinimalFuture.completedFuture(resp);
- Log.logTrace("Created completed future (streamid={0}): {1}",
- streamid, cf);
- response_cfs.add(cf);
- }
- }
-
- // methods to update state and remove stream when finished
-
- synchronized void requestSent() {
- requestSent = true;
- if (responseReceived) {
- close();
- }
- }
-
- synchronized void responseReceived() {
- responseReceived = true;
- if (requestSent) {
- close();
- }
- }
-
- /**
- * same as above but for errors
- */
- void completeResponseExceptionally(Throwable t) {
- synchronized (response_cfs) {
- // use index to avoid ConcurrentModificationException
- // caused by removing the CF from within the loop.
- for (int i = 0; i < response_cfs.size(); i++) {
- CompletableFuture<Response> cf = response_cfs.get(i);
- if (!cf.isDone()) {
- cf.completeExceptionally(t);
- response_cfs.remove(i);
- return;
- }
- }
- response_cfs.add(MinimalFuture.failedFuture(t));
- }
- }
-
- CompletableFuture<Void> sendBodyImpl() {
- requestBodyCF.whenComplete((v, t) -> requestSent());
- if (requestPublisher != null) {
- final RequestSubscriber subscriber = new RequestSubscriber(requestContentLen);
- requestPublisher.subscribe(requestSubscriber = subscriber);
- } else {
- // there is no request body, therefore the request is complete,
- // END_STREAM has already sent with outgoing headers
- requestBodyCF.complete(null);
- }
- return requestBodyCF;
- }
-
- @Override
- void cancel() {
- cancel(new IOException("Stream " + streamid + " cancelled"));
- }
-
- @Override
- void cancel(IOException cause) {
- cancelImpl(cause);
- }
-
- // This method sends a RST_STREAM frame
- void cancelImpl(Throwable e) {
- debug.log(Level.DEBUG, "cancelling stream {0}: {1}", streamid, e);
- if (Log.trace()) {
- Log.logTrace("cancelling stream {0}: {1}\n", streamid, e);
- }
- boolean closing;
- if (closing = !closed) { // assigning closing to !closed
- synchronized (this) {
- failed = e;
- if (closing = !closed) { // assigning closing to !closed
- closed=true;
- }
- }
- }
- if (closing) { // true if the stream has not been closed yet
- if (responseSubscriber != null)
- sched.runOrSchedule();
- }
- completeResponseExceptionally(e);
- if (!requestBodyCF.isDone()) {
- requestBodyCF.completeExceptionally(e); // we may be sending the body..
- }
- if (responseBodyCF != null) {
- responseBodyCF.completeExceptionally(e);
- }
- try {
- // will send a RST_STREAM frame
- if (streamid != 0) {
- connection.resetStream(streamid, ResetFrame.CANCEL);
- }
- } catch (IOException ex) {
- Log.logError(ex);
- }
- }
-
- // This method doesn't send any frame
- void close() {
- if (closed) return;
- synchronized(this) {
- if (closed) return;
- closed = true;
- }
- Log.logTrace("Closing stream {0}", streamid);
- connection.closeStream(streamid);
- Log.logTrace("Stream {0} closed", streamid);
- }
-
- static class PushedStream<T> extends Stream<T> {
- final PushGroup<T> pushGroup;
- // push streams need the response CF allocated up front as it is
- // given directly to user via the multi handler callback function.
- final CompletableFuture<Response> pushCF;
- CompletableFuture<HttpResponse<T>> responseCF;
- final HttpRequestImpl pushReq;
- HttpResponse.BodyHandler<T> pushHandler;
-
- PushedStream(PushGroup<T> pushGroup,
- Http2Connection connection,
- Exchange<T> pushReq) {
- // ## no request body possible, null window controller
- super(connection, pushReq, null);
- this.pushGroup = pushGroup;
- this.pushReq = pushReq.request();
- this.pushCF = new MinimalFuture<>();
- this.responseCF = new MinimalFuture<>();
-
- }
-
- CompletableFuture<HttpResponse<T>> responseCF() {
- return responseCF;
- }
-
- synchronized void setPushHandler(HttpResponse.BodyHandler<T> pushHandler) {
- this.pushHandler = pushHandler;
- }
-
- synchronized HttpResponse.BodyHandler<T> getPushHandler() {
- // ignored parameters to function can be used as BodyHandler
- return this.pushHandler;
- }
-
- // Following methods call the super class but in case of
- // error record it in the PushGroup. The error method is called
- // with a null value when no error occurred (is a no-op)
- @Override
- CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
- return super.sendBodyAsync()
- .whenComplete((ExchangeImpl<T> v, Throwable t)
- -> pushGroup.pushError(Utils.getCompletionCause(t)));
- }
-
- @Override
- CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
- return super.sendHeadersAsync()
- .whenComplete((ExchangeImpl<T> ex, Throwable t)
- -> pushGroup.pushError(Utils.getCompletionCause(t)));
- }
-
- @Override
- CompletableFuture<Response> getResponseAsync(Executor executor) {
- CompletableFuture<Response> cf = pushCF.whenComplete(
- (v, t) -> pushGroup.pushError(Utils.getCompletionCause(t)));
- if(executor!=null && !cf.isDone()) {
- cf = cf.thenApplyAsync( r -> r, executor);
- }
- return cf;
- }
-
- @Override
- CompletableFuture<T> readBodyAsync(
- HttpResponse.BodyHandler<T> handler,
- boolean returnConnectionToPool,
- Executor executor)
- {
- return super.readBodyAsync(handler, returnConnectionToPool, executor)
- .whenComplete((v, t) -> pushGroup.pushError(t));
- }
-
- @Override
- void completeResponse(Response r) {
- Log.logResponse(r::toString);
- pushCF.complete(r); // not strictly required for push API
- // start reading the body using the obtained BodySubscriber
- CompletableFuture<Void> start = new MinimalFuture<>();
- start.thenCompose( v -> readBodyAsync(getPushHandler(), false, getExchange().executor()))
- .whenComplete((T body, Throwable t) -> {
- if (t != null) {
- responseCF.completeExceptionally(t);
- } else {
- HttpResponseImpl<T> resp =
- new HttpResponseImpl<>(r.request, r, null, body, getExchange());
- responseCF.complete(resp);
- }
- });
- start.completeAsync(() -> null, getExchange().executor());
- }
-
- @Override
- void completeResponseExceptionally(Throwable t) {
- pushCF.completeExceptionally(t);
- }
-
-// @Override
-// synchronized void responseReceived() {
-// super.responseReceived();
-// }
-
- // create and return the PushResponseImpl
- @Override
- protected void handleResponse() {
- responseCode = (int)responseHeaders
- .firstValueAsLong(":status")
- .orElse(-1);
-
- if (responseCode == -1) {
- completeResponseExceptionally(new IOException("No status code"));
- }
-
- this.response = new Response(
- pushReq, exchange, responseHeaders,
- responseCode, HttpClient.Version.HTTP_2);
-
- /* TODO: review if needs to be removed
- the value is not used, but in case `content-length` doesn't parse
- as long, there will be NumberFormatException. If left as is, make
- sure code up the stack handles NFE correctly. */
- responseHeaders.firstValueAsLong("content-length");
-
- if (Log.headers()) {
- StringBuilder sb = new StringBuilder("RESPONSE HEADERS");
- sb.append(" (streamid=").append(streamid).append("): ");
- Log.dumpHeaders(sb, " ", responseHeaders);
- Log.logHeaders(sb.toString());
- }
-
- // different implementations for normal streams and pushed streams
- completeResponse(response);
- }
- }
-
- final class StreamWindowUpdateSender extends WindowUpdateSender {
-
- StreamWindowUpdateSender(Http2Connection connection) {
- super(connection);
- }
-
- @Override
- int getStreamId() {
- return streamid;
- }
- }
-
- /**
- * Returns true if this exchange was canceled.
- * @return true if this exchange was canceled.
- */
- synchronized boolean isCanceled() {
- return failed != null;
- }
-
- /**
- * Returns the cause for which this exchange was canceled, if available.
- * @return the cause for which this exchange was canceled, if available.
- */
- synchronized Throwable getCancelCause() {
- return failed;
- }
-
- final String dbgString() {
- return connection.dbgString() + "/Stream("+streamid+")";
- }
-}
--- a/src/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<TimeoutEvent> {
-
- private static final AtomicLong COUNTER = new AtomicLong();
- // we use id in compareTo to make compareTo consistent with equals
- // see TimeoutEvent::compareTo below;
- private final long id = COUNTER.incrementAndGet();
- private final Instant deadline;
-
- TimeoutEvent(Duration duration) {
- deadline = Instant.now().plus(duration);
- }
-
- public abstract void handle();
-
- public Instant deadline() {
- return deadline;
- }
-
- @Override
- public int compareTo(TimeoutEvent other) {
- if (other == this) return 0;
- // if two events have the same deadline, but are not equals, then the
- // smaller is the one that was created before (has the smaller id).
- // This is arbitrary and we don't really care which is smaller or
- // greater, but we need a total order, so two events with the
- // same deadline cannot compare == 0 if they are not equals.
- final int compareDeadline = this.deadline.compareTo(other.deadline);
- if (compareDeadline == 0 && !this.equals(other)) {
- long diff = this.id - other.id; // should take care of wrap around
- if (diff < 0) return -1;
- else if (diff > 0) return 1;
- else assert false : "Different events with same id and deadline";
- }
- return compareDeadline;
- }
-
- @Override
- public String toString() {
- return "TimeoutEvent[id=" + id + ", deadline=" + deadline + "]";
- }
-}
--- a/src/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<T> extends HttpResponse.BodyHandler<T> {
- void setAccessControlContext(AccessControlContext acc);
-}
--- 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<Integer,Integer> streams = new HashMap<>();
- /** A Map of streams awaiting Send Window. The key is the stream id. The
- * value is a pair of the Stream ( representing the key's stream id ) and
- * the requested amount of send Window. */
- private final Map<Integer, Map.Entry<Stream<?>, Integer>> pending
- = new LinkedHashMap<>();
-
- private final ReentrantLock controllerLock = new ReentrantLock();
-
- /** A Controller with the default initial window size. */
- WindowController() {
- connectionWindowSize = DEFAULT_INITIAL_WINDOW_SIZE;
- }
-
-// /** A Controller with the given initial window size. */
-// WindowController(int initialConnectionWindowSize) {
-// connectionWindowSize = initialConnectionWindowSize;
-// }
-
- /** Registers the given stream with this controller. */
- void registerStream(int streamid, int initialStreamWindowSize) {
- controllerLock.lock();
- try {
- Integer old = streams.put(streamid, initialStreamWindowSize);
- if (old != null)
- throw new InternalError("Unexpected entry ["
- + old + "] for streamid: " + streamid);
- } finally {
- controllerLock.unlock();
- }
- }
-
- /** Removes/De-registers the given stream with this controller. */
- void removeStream(int streamid) {
- controllerLock.lock();
- try {
- Integer old = streams.remove(streamid);
- // Odd stream numbers (client streams) should have been registered.
- // Even stream numbers (server streams - aka Push Streams) should
- // not be registered
- final boolean isClientStream = (streamid % 2) == 1;
- if (old == null && isClientStream) {
- throw new InternalError("Expected entry for streamid: " + streamid);
- } else if (old != null && !isClientStream) {
- throw new InternalError("Unexpected entry for streamid: " + streamid);
- }
- } finally {
- controllerLock.unlock();
- }
- }
-
- /**
- * Attempts to acquire the requested amount of Send Window for the given
- * stream.
- *
- * The actual amount of Send Window available may differ from the requested
- * amount. The actual amount, returned by this method, is the minimum of,
- * 1) the requested amount, 2) the stream's Send Window, and 3) the
- * connection's Send Window.
- *
- * A negative or zero value is returned if there's no window available.
- * When the result is negative or zero, this method arranges for the
- * given stream's {@link Stream#signalWindowUpdate()} method to be invoke at
- * a later time when the connection and/or stream window's have been
- * increased. The {@code tryAcquire} method should then be invoked again to
- * attempt to acquire the available window.
- */
- int tryAcquire(int requestAmount, int streamid, Stream<?> stream) {
- controllerLock.lock();
- try {
- Integer streamSize = streams.get(streamid);
- if (streamSize == null)
- throw new InternalError("Expected entry for streamid: "
- + streamid);
- int x = Math.min(requestAmount,
- Math.min(streamSize, connectionWindowSize));
-
- if (x <= 0) { // stream window size may be negative
- DEBUG_LOGGER.log(Level.DEBUG,
- "Stream %d requesting %d but only %d available (stream: %d, connection: %d)",
- streamid, requestAmount, Math.min(streamSize, connectionWindowSize),
- streamSize, connectionWindowSize);
- // If there's not enough window size available, put the
- // caller in a pending list.
- pending.put(streamid, Map.entry(stream, requestAmount));
- return x;
- }
-
- // Remove the caller from the pending list ( if was waiting ).
- pending.remove(streamid);
-
- // Update window sizes and return the allocated amount to the caller.
- streamSize -= x;
- streams.put(streamid, streamSize);
- connectionWindowSize -= x;
- DEBUG_LOGGER.log(Level.DEBUG,
- "Stream %d amount allocated %d, now %d available (stream: %d, connection: %d)",
- streamid, x, Math.min(streamSize, connectionWindowSize),
- streamSize, connectionWindowSize);
- return x;
- } finally {
- controllerLock.unlock();
- }
- }
-
- /**
- * Increases the Send Window size for the connection.
- *
- * A number of awaiting requesters, from unfulfilled tryAcquire requests,
- * may have their stream's {@link Stream#signalWindowUpdate()} method
- * scheduled to run ( i.e. awake awaiters ).
- *
- * @return false if, and only if, the addition of the given amount would
- * cause the Send Window to exceed 2^31-1 (overflow), otherwise true
- */
- boolean increaseConnectionWindow(int amount) {
- List<Stream<?>> candidates = null;
- controllerLock.lock();
- try {
- int size = connectionWindowSize;
- size += amount;
- if (size < 0)
- return false;
- connectionWindowSize = size;
- DEBUG_LOGGER.log(Level.DEBUG, "Connection window size is now %d", size);
-
- // Notify waiting streams, until the new increased window size is
- // effectively exhausted.
- Iterator<Map.Entry<Integer,Map.Entry<Stream<?>,Integer>>> iter =
- pending.entrySet().iterator();
-
- while (iter.hasNext() && size > 0) {
- Map.Entry<Integer,Map.Entry<Stream<?>,Integer>> item = iter.next();
- Integer streamSize = streams.get(item.getKey());
- if (streamSize == null) {
- iter.remove();
- } else {
- Map.Entry<Stream<?>,Integer> e = item.getValue();
- int requestedAmount = e.getValue();
- // only wakes up the pending streams for which there is
- // at least 1 byte of space in both windows
- int minAmount = 1;
- if (size >= minAmount && streamSize >= minAmount) {
- size -= Math.min(streamSize, requestedAmount);
- iter.remove();
- if (candidates == null)
- candidates = new ArrayList<>();
- candidates.add(e.getKey());
- }
- }
- }
- } finally {
- controllerLock.unlock();
- }
- if (candidates != null) {
- candidates.forEach(Stream::signalWindowUpdate);
- }
- return true;
- }
-
- /**
- * Increases the Send Window size for the given stream.
- *
- * If the given stream is awaiting window size, from an unfulfilled
- * tryAcquire request, it will have its stream's {@link
- * Stream#signalWindowUpdate()} method scheduled to run ( i.e. awoken ).
- *
- * @return false if, and only if, the addition of the given amount would
- * cause the Send Window to exceed 2^31-1 (overflow), otherwise true
- */
- boolean increaseStreamWindow(int amount, int streamid) {
- Stream<?> s = null;
- controllerLock.lock();
- try {
- Integer size = streams.get(streamid);
- if (size == null)
- throw new InternalError("Expected entry for streamid: " + streamid);
- size += amount;
- if (size < 0)
- return false;
- streams.put(streamid, size);
- DEBUG_LOGGER.log(Level.DEBUG,
- "Stream %s window size is now %s", streamid, size);
-
- Map.Entry<Stream<?>,Integer> p = pending.get(streamid);
- if (p != null) {
- int minAmount = 1;
- // only wakes up the pending stream if there is at least
- // 1 byte of space in both windows
- if (size >= minAmount
- && connectionWindowSize >= minAmount) {
- pending.remove(streamid);
- s = p.getKey();
- }
- }
- } finally {
- controllerLock.unlock();
- }
-
- if (s != null)
- s.signalWindowUpdate();
-
- return true;
- }
-
- /**
- * Adjusts, either increases or decreases, the active streams registered
- * with this controller. May result in a stream's Send Window size becoming
- * negative.
- */
- void adjustActiveStreams(int adjustAmount) {
- assert adjustAmount != 0;
-
- controllerLock.lock();
- try {
- for (Map.Entry<Integer,Integer> entry : streams.entrySet()) {
- int streamid = entry.getKey();
- // the API only supports sending on Streams initialed by
- // the client, i.e. odd stream numbers
- if (streamid != 0 && (streamid % 2) != 0) {
- Integer size = entry.getValue();
- size += adjustAmount;
- streams.put(streamid, size);
- DEBUG_LOGGER.log(Level.DEBUG,
- "Stream %s window size is now %s", streamid, size);
- }
- }
- } finally {
- controllerLock.unlock();
- }
- }
-
- /** Returns the Send Window size for the connection. */
- int connectionWindowSize() {
- controllerLock.lock();
- try {
- return connectionWindowSize;
- } finally {
- controllerLock.unlock();
- }
- }
-
-// /** Returns the Send Window size for the given stream. */
-// int streamWindowSize(int streamid) {
-// controllerLock.lock();
-// try {
-// Integer size = streams.get(streamid);
-// if (size == null)
-// throw new InternalError("Expected entry for streamid: " + streamid);
-// return size;
-// } finally {
-// controllerLock.unlock();
-// }
-// }
-
-}
--- a/src/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() + ")";
- }
-
-}
--- 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<ByteBuffer> pool = new ConcurrentLinkedQueue<>();
-
- public ByteBufferPool() {
- }
-
- public ByteBufferReference get(int size) {
- ByteBuffer buffer;
- while ((buffer = pool.poll()) != null) {
- if (buffer.capacity() >= size) {
- return ByteBufferReference.of(buffer, this);
- }
- }
- return ByteBufferReference.of(ByteBuffer.allocate(size), this);
- }
-
- public void release(ByteBuffer buffer) {
- buffer.clear();
- pool.offer(buffer);
- }
-
-}
--- 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<ByteBuffer> {
-
- private ByteBuffer buffer;
- private final ByteBufferPool pool;
-
- public static ByteBufferReference of(ByteBuffer buffer) {
- return of(buffer, null);
- }
-
- public static ByteBufferReference of(ByteBuffer buffer, ByteBufferPool pool) {
- Objects.requireNonNull(buffer);
- return new ByteBufferReference(buffer, pool);
- }
-
- public static ByteBuffer[] toBuffers(ByteBufferReference... refs) {
- ByteBuffer[] bufs = new ByteBuffer[refs.length];
- for (int i = 0; i < refs.length; i++) {
- bufs[i] = refs[i].get();
- }
- return bufs;
- }
-
- public static ByteBufferReference[] toReferences(ByteBuffer... buffers) {
- ByteBufferReference[] refs = new ByteBufferReference[buffers.length];
- for (int i = 0; i < buffers.length; i++) {
- refs[i] = of(buffers[i]);
- }
- return refs;
- }
-
-
- public static void clear(ByteBufferReference[] refs) {
- for(ByteBufferReference ref : refs) {
- ref.clear();
- }
- }
-
- private ByteBufferReference(ByteBuffer buffer, ByteBufferPool pool) {
- this.buffer = buffer;
- this.pool = pool;
- }
-
- @Override
- public ByteBuffer get() {
- ByteBuffer buf = this.buffer;
- assert buf!=null : "getting ByteBuffer after clearance";
- return buf;
- }
-
- public void clear() {
- ByteBuffer buf = this.buffer;
- assert buf!=null : "double ByteBuffer clearance";
- this.buffer = null;
- if (pool != null) {
- pool.release(buf);
- }
- }
-}
--- 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);
- }
-}
--- 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".
- * <p>
- * 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<String> 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.
- * <p>
- * The logger will add some decoration to the printed message, in the form of
- * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
- *
- * @apiNote To obtain a logger that will always print things on stderr in
- * addition to forwarding to the internal logger, use
- * {@code new DebugLogger(logger, this::dbgTag, Level.OFF, Level.ALL);}.
- * To obtain a logger that will only forward to the internal logger,
- * use {@code new DebugLogger(logger, this::dbgTag, Level.OFF, Level.OFF);}.
- *
- * @param logger The internal logger to which messages will be forwarded.
- * This should be either {@link #HPACK} or {@link #HTTP};
- *
- * @param dbgTag A lambda that returns a string that identifies the caller
- * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
- * @param outLevel The level above which messages will be also printed on
- * System.out (in addition to being forwarded to the internal logger).
- * @param errLevel The level above which messages will be also printed on
- * System.err (in addition to being forwarded to the internal logger).
- *
- * @return A logger for HTTP internal debug traces
- */
- private DebugLogger(Logger logger,
- Supplier<String> dbgTag,
- Level outLevel,
- Level errLevel) {
- this.dbgTag = dbgTag;
- this.errLevel = errLevel;
- this.outLevel = outLevel;
- this.logger = Objects.requireNonNull(logger);
- // support only static configuration.
- this.debugOn = isEnabled(Level.DEBUG);
- this.traceOn = isEnabled(Level.TRACE);
- }
-
- @Override
- public String getName() {
- return logger.getName();
- }
-
- private boolean isEnabled(Level level) {
- if (level == Level.OFF) return false;
- int severity = level.getSeverity();
- return severity >= errLevel.getSeverity()
- || severity >= outLevel.getSeverity()
- || logger.isLoggable(level);
- }
-
- @Override
- public boolean isLoggable(Level level) {
- // fast path, we assume these guys never change.
- // support only static configuration.
- if (level == Level.DEBUG) return debugOn;
- if (level == Level.TRACE) return traceOn;
- return isEnabled(level);
- }
-
- @Override
- public void log(Level level, ResourceBundle unused,
- String format, Object... params) {
- // fast path, we assume these guys never change.
- // support only static configuration.
- if (level == Level.DEBUG && !debugOn) return;
- if (level == Level.TRACE && !traceOn) return;
-
- int severity = level.getSeverity();
- if (errLevel != Level.OFF
- && errLevel.getSeverity() <= severity) {
- print(System.err, level, format, params, null);
- }
- if (outLevel != Level.OFF
- && outLevel.getSeverity() <= severity) {
- print(System.out, level, format, params, null);
- }
- if (logger.isLoggable(level)) {
- logger.log(level, unused,
- getFormat(new StringBuilder(), format, params).toString(),
- params);
- }
- }
-
- @Override
- public void log(Level level, ResourceBundle unused, String msg,
- Throwable thrown) {
- // fast path, we assume these guys never change.
- if (level == Level.DEBUG && !debugOn) return;
- if (level == Level.TRACE && !traceOn) return;
-
- if (errLevel != Level.OFF
- && errLevel.getSeverity() <= level.getSeverity()) {
- print(System.err, level, msg, null, thrown);
- }
- if (outLevel != Level.OFF
- && outLevel.getSeverity() <= level.getSeverity()) {
- print(System.out, level, msg, null, thrown);
- }
- if (logger.isLoggable(level)) {
- logger.log(level, unused,
- getFormat(new StringBuilder(), msg, null).toString(),
- thrown);
- }
- }
-
- private void print(PrintStream out, Level level, String msg,
- Object[] params, Throwable t) {
- StringBuilder sb = new StringBuilder();
- sb.append(level.name()).append(':').append(' ');
- sb = format(sb, msg, params);
- if (t != null) sb.append(' ').append(t.toString());
- out.println(sb.toString());
- if (t != null) {
- t.printStackTrace(out);
- }
- }
-
- private StringBuilder decorate(StringBuilder sb, String msg) {
- String tag = dbgTag == null ? null : dbgTag.get();
- String res = msg == null ? "" : msg;
- long elapsed = System.nanoTime() - START_NANOS;
- long millis = elapsed / 1000_000;
- long secs = millis / 1000;
- sb.append('[').append(Thread.currentThread().getName()).append(']')
- .append(' ').append('[');
- if (secs > 0) {
- sb.append(secs).append('s');
- }
- millis = millis % 1000;
- if (millis > 0) {
- if (secs > 0) sb.append(' ');
- sb.append(millis).append("ms");
- }
- sb.append(']').append(' ');
- if (tag != null) {
- sb.append(tag).append(' ');
- }
- sb.append(res);
- return sb;
- }
-
-
- private StringBuilder getFormat(StringBuilder sb, String format, Object[] params) {
- if (format == null || params == null || params.length == 0) {
- return decorate(sb, format);
- } else if (format.contains("{0}") || format.contains("{1}")) {
- return decorate(sb, format);
- } else if (format.contains("%s") || format.contains("%d")) {
- try {
- return decorate(sb, String.format(format, params));
- } catch (Throwable t) {
- return decorate(sb, format);
- }
- } else {
- return decorate(sb, format);
- }
- }
-
- private StringBuilder format(StringBuilder sb, String format, Object[] params) {
- if (format == null || params == null || params.length == 0) {
- return decorate(sb, format);
- } else if (format.contains("{0}") || format.contains("{1}")) {
- return decorate(sb, java.text.MessageFormat.format(format, params));
- } else if (format.contains("%s") || format.contains("%d")) {
- try {
- return decorate(sb, String.format(format, params));
- } catch (Throwable t) {
- return decorate(sb, format);
- }
- } else {
- return decorate(sb, format);
- }
- }
-
- public static DebugLogger createHttpLogger(Supplier<String> dbgTag, Level outLevel, Level errLevel) {
- return new DebugLogger(HTTP, dbgTag, outLevel, errLevel);
- }
-
- public static DebugLogger createHpackLogger(Supplier<String> dbgTag, Level outLevel, Level errLevel) {
- return new DebugLogger(HPACK, dbgTag, outLevel, errLevel);
- }
-}
--- 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).
- *
- * <p> Demand is the aggregated number of elements requested by a Subscriber
- * which is yet to be delivered (fulfilled) by the Publisher.
- */
-public final class Demand {
-
- private final AtomicLong val = new AtomicLong();
-
- /**
- * Increases this demand by the specified positive value.
- *
- * @param n
- * increment {@code > 0}
- *
- * @return {@code true} iff prior to this operation this demand was fulfilled
- */
- public boolean increase(long n) {
- if (n <= 0) {
- throw new IllegalArgumentException(String.valueOf(n));
- }
- long prev = val.getAndAccumulate(n, (p, i) -> p + i < 0 ? Long.MAX_VALUE : p + i);
- return prev == 0;
- }
-
- /**
- * Tries to decrease this demand by the specified positive value.
- *
- * <p> The actual value this demand has been decreased by might be less than
- * {@code n}, including {@code 0} (no decrease at all).
- *
- * @param n
- * decrement {@code > 0}
- *
- * @return a value {@code m} ({@code 0 <= m <= n}) this demand has been
- * actually decreased by
- */
- public long decreaseAndGet(long n) {
- if (n <= 0) {
- throw new IllegalArgumentException(String.valueOf(n));
- }
- long p, d;
- do {
- d = val.get();
- p = Math.min(d, n);
- } while (!val.compareAndSet(d, d - p));
- return p;
- }
-
- /**
- * Tries to decrease this demand by {@code 1}.
- *
- * @return {@code true} iff this demand has been decreased by {@code 1}
- */
- public boolean tryDecrement() {
- return decreaseAndGet(1) == 1;
- }
-
- /**
- * @return {@code true} iff there is no unfulfilled demand
- */
- public boolean isFulfilled() {
- return val.get() == 0;
- }
-
- /**
- * Resets this object to its initial state.
- */
- public void reset() {
- val.set(0);
- }
-
- /**
- * Returns the current value of this demand.
- *
- * @return the current value of this demand
- */
- public long get() {
- return val.get();
- }
-
- @Override
- public String toString() {
- return String.valueOf(val.get());
- }
-}
--- 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.
- * <p>
- * The {@code connectFlows} method should be called to connect the bidirectional
- * flow. A FlowTube supports handing over the same read subscription to different
- * sequential read subscribers over time. When {@code connectFlows(writePublisher,
- * readSubscriber} is called, the FlowTube will call {@code dropSubscription} on
- * its former readSubscriber, and {@code onSubscribe} on its new readSubscriber.
- */
-public interface FlowTube extends
- Flow.Publisher<List<ByteBuffer>>,
- Flow.Subscriber<List<ByteBuffer>> {
-
- /**
- * A subscriber for reading from the bidirectional flow.
- * A TubeSubscriber is a {@code Flow.Subscriber} that can be canceled
- * by calling {@code dropSubscription()}.
- * Once {@code dropSubscription()} is called, the {@code TubeSubscriber}
- * should stop calling any method on its subscription.
- */
- static interface TubeSubscriber extends Flow.Subscriber<List<ByteBuffer>> {
-
- /**
- * Called when the flow is connected again, and the subscription
- * is handed over to a new subscriber.
- * Once {@code dropSubscription()} is called, the {@code TubeSubscriber}
- * should stop calling any method on its subscription.
- */
- default void dropSubscription() { }
-
- }
-
- /**
- * A publisher for writing to the bidirectional flow.
- */
- static interface TubePublisher extends Flow.Publisher<List<ByteBuffer>> {
-
- }
-
- /**
- * Connects the bidirectional flows to a write {@code Publisher} and a
- * read {@code Subscriber}. This method can be called sequentially
- * several times to switch existing publishers and subscribers to a new
- * pair of write subscriber and read publisher.
- * @param writePublisher A new publisher for writing to the bidirectional flow.
- * @param readSubscriber A new subscriber for reading from the bidirectional
- * flow.
- */
- default void connectFlows(TubePublisher writePublisher,
- TubeSubscriber readSubscriber) {
-
- this.subscribe(readSubscriber);
- writePublisher.subscribe(this);
- }
-
- /**
- * Returns true if this flow was completed, either exceptionally
- * or normally (EOF reached).
- * @return true if the flow is finished
- */
- boolean isFinished();
-
-
- /**
- * Returns {@code s} if {@code s} is a {@code TubeSubscriber}, otherwise
- * wraps it in a {@code TubeSubscriber}.
- * Using the wrapper is only appropriate in the case where
- * {@code dropSubscription} doesn't need to be implemented, and the
- * {@code TubeSubscriber} is subscribed only once.
- *
- * @param s a subscriber for reading.
- * @return a {@code TubeSubscriber}: either {@code s} if {@code s} is a
- * {@code TubeSubscriber}, otherwise a {@code TubeSubscriber}
- * wrapper that delegates to {@code s}
- */
- static TubeSubscriber asTubeSubscriber(Flow.Subscriber<? super List<ByteBuffer>> s) {
- if (s instanceof TubeSubscriber) {
- return (TubeSubscriber) s;
- }
- return new AbstractTubeSubscriber.TubeSubscriberWrapper(s);
- }
-
- /**
- * Returns {@code s} if {@code s} is a {@code TubePublisher}, otherwise
- * wraps it in a {@code TubePublisher}.
- *
- * @param p a publisher for writing.
- * @return a {@code TubePublisher}: either {@code s} if {@code s} is a
- * {@code TubePublisher}, otherwise a {@code TubePublisher}
- * wrapper that delegates to {@code s}
- */
- static TubePublisher asTubePublisher(Flow.Publisher<List<ByteBuffer>> p) {
- if (p instanceof TubePublisher) {
- return (TubePublisher) p;
- }
- return new AbstractTubePublisher.TubePublisherWrapper(p);
- }
-
- /**
- * Convenience abstract class for {@code TubePublisher} implementations.
- * It is not required that a {@code TubePublisher} implementation extends
- * this class.
- */
- static abstract class AbstractTubePublisher implements TubePublisher {
- static final class TubePublisherWrapper extends AbstractTubePublisher {
- final Flow.Publisher<List<ByteBuffer>> delegate;
- public TubePublisherWrapper(Flow.Publisher<List<ByteBuffer>> delegate) {
- this.delegate = delegate;
- }
- @Override
- public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
- delegate.subscribe(subscriber);
- }
- }
- }
-
- /**
- * Convenience abstract class for {@code TubeSubscriber} implementations.
- * It is not required that a {@code TubeSubscriber} implementation extends
- * this class.
- */
- static abstract class AbstractTubeSubscriber implements TubeSubscriber {
- static final class TubeSubscriberWrapper extends AbstractTubeSubscriber {
- final Flow.Subscriber<? super List<ByteBuffer>> delegate;
- TubeSubscriberWrapper(Flow.Subscriber<? super List<ByteBuffer>> delegate) {
- this.delegate = delegate;
- }
- @Override
- public void dropSubscription() {}
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- delegate.onSubscribe(subscription);
- }
- @Override
- public void onNext(List<ByteBuffer> item) {
- delegate.onNext(item);
- }
- @Override
- public void onError(Throwable throwable) {
- delegate.onError(throwable);
- }
- @Override
- public void onComplete() {
- delegate.onComplete();
- }
- }
-
- }
-
-}
--- 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<String,List<String>> headers;
-
- public HttpHeadersImpl() {
- headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
- }
-
- @Override
- public Map<String, List<String>> map() {
- return Collections.unmodifiableMap(headers);
- }
-
- // package private mutators
-
- public HttpHeadersImpl deepCopy() {
- HttpHeadersImpl h1 = new HttpHeadersImpl();
- for (Map.Entry<String,List<String>> entry : headers.entrySet()) {
- List<String> 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<String> values = new ArrayList<>(1); // most headers has one value
- values.add(value);
- headers.put(name, values);
- }
-}
--- 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<String> msgSupplier) {
- if (ssl()) {
- logger.log(Level.INFO, "SSL: " + msgSupplier.get());
- }
- }
-
- public static void logTrace(String s, Object... s1) {
- if (trace()) {
- String format = "TRACE: " + s;
- logger.log(Level.INFO, format, s1);
- }
- }
-
- public static void logRequest(String s, Object... s1) {
- if (requests()) {
- logger.log(Level.INFO, "REQUEST: " + s, s1);
- }
- }
-
- public static void logResponse(Supplier<String> supplier) {
- if (requests()) {
- logger.log(Level.INFO, "RESPONSE: " + supplier.get());
- }
- }
-
- public static void logHeaders(String s, Object... s1) {
- if (headers()) {
- logger.log(Level.INFO, "HEADERS: " + s, s1);
- }
- }
-
- public static boolean loggingFrame(Class<? extends Http2Frame> clazz) {
- if (frametypes == ALL) {
- return true;
- }
- if (clazz == DataFrame.class) {
- return (frametypes & DATA) != 0;
- } else if (clazz == WindowUpdateFrame.class) {
- return (frametypes & WINDOW_UPDATES) != 0;
- } else {
- return (frametypes & CONTROL) != 0;
- }
- }
-
- public static void logFrames(Http2Frame f, String direction) {
- if (frames() && loggingFrame(f.getClass())) {
- logger.log(Level.INFO, "FRAME: " + direction + ": " + f.toString());
- }
- }
-
- public static void logParams(SSLParameters p) {
- if (!Log.ssl()) {
- return;
- }
-
- if (p == null) {
- Log.logSSL("SSLParameters: Null params");
- return;
- }
-
- final StringBuilder sb = new StringBuilder("SSLParameters:");
- final List<Object> params = new ArrayList<>();
- if (p.getCipherSuites() != null) {
- for (String cipher : p.getCipherSuites()) {
- sb.append("\n cipher: {")
- .append(params.size()).append("}");
- params.add(cipher);
- }
- }
-
- // SSLParameters.getApplicationProtocols() can't return null
- // JDK 8 EXCL START
- for (String approto : p.getApplicationProtocols()) {
- sb.append("\n application protocol: {")
- .append(params.size()).append("}");
- params.add(approto);
- }
- // JDK 8 EXCL END
-
- if (p.getProtocols() != null) {
- for (String protocol : p.getProtocols()) {
- sb.append("\n protocol: {")
- .append(params.size()).append("}");
- params.add(protocol);
- }
- }
-
- if (p.getServerNames() != null) {
- for (SNIServerName sname : p.getServerNames()) {
- sb.append("\n server name: {")
- .append(params.size()).append("}");
- params.add(sname.toString());
- }
- }
- sb.append('\n');
-
- Log.logSSL(sb.toString(), params.toArray());
- }
-
- public static void dumpHeaders(StringBuilder sb, String prefix, HttpHeaders headers) {
- if (headers != null) {
- Map<String,List<String>> h = headers.map();
- Set<Map.Entry<String,List<String>>> entries = h.entrySet();
- for (Map.Entry<String,List<String>> entry : entries) {
- String key = entry.getKey();
- List<String> values = entry.getValue();
- sb.append(prefix).append(key).append(":");
- for (String value : values) {
- sb.append(' ').append(value);
- }
- sb.append('\n');
- }
- }
- }
-
-
- // not instantiable
- private Log() {}
-}
--- 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<T> extends CompletableFuture<T> {
-
- @FunctionalInterface
- public interface ExceptionalSupplier<U> {
- U get() throws Throwable;
- }
-
- private final static AtomicLong TOKENS = new AtomicLong();
- private final long id;
-
- public static <U> MinimalFuture<U> completedFuture(U value) {
- MinimalFuture<U> f = new MinimalFuture<>();
- f.complete(value);
- return f;
- }
-
- public static <U> CompletableFuture<U> failedFuture(Throwable ex) {
- requireNonNull(ex);
- MinimalFuture<U> f = new MinimalFuture<>();
- f.completeExceptionally(ex);
- return f;
- }
-
- public static <U> CompletableFuture<U> supply(ExceptionalSupplier<U> supplier) {
- CompletableFuture<U> cf = new MinimalFuture<>();
- try {
- U value = supplier.get();
- cf.complete(value);
- } catch (Throwable t) {
- cf.completeExceptionally(t);
- }
- return cf;
- }
-
- public MinimalFuture() {
- super();
- this.id = TOKENS.incrementAndGet();
- }
-
- @Override
- public <U> MinimalFuture<U> newIncompleteFuture() {
- return new MinimalFuture<>();
- }
-
- @Override
- public void obtrudeValue(T value) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void obtrudeException(Throwable ex) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public String toString() {
- return super.toString() + " (id=" + id +")";
- }
-
- public static <U> MinimalFuture<U> of(CompletionStage<U> stage) {
- MinimalFuture<U> cf = new MinimalFuture<>();
- stage.whenComplete((r,t) -> complete(cf, r, t));
- return cf;
- }
-
- private static <U> void complete(CompletableFuture<U> cf, U result, Throwable t) {
- if (t == null) {
- cf.complete(result);
- } else {
- cf.completeExceptionally(t);
- }
- }
-}
--- 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<T, U> {
-
- public Pair(T first, U second) {
- this.second = second;
- this.first = first;
- }
-
- public final T first;
- public final U second;
-
- // Because 'pair()' is shorter than 'new Pair<>()'.
- // Sometimes this difference might be very significant (especially in a
- // 80-ish characters boundary). Sorry diamond operator.
- public static <T, U> Pair<T, U> pair(T first, U second) {
- return new Pair<>(first, second);
- }
-
- @Override
- public String toString() {
- return "(" + first + ", " + second + ")";
- }
-}
--- 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.
- *
- * <p> Constructor takes two Flow.Subscribers: one that receives the network
- * data (after it has been encrypted by SSLFlowDelegate) data, and one that
- * receives the application data (before it has been encrypted by SSLFlowDelegate).
- *
- * <p> Methods upstreamReader() and upstreamWriter() return the corresponding
- * Flow.Subscribers containing Flows for the encrypted/decrypted upstream data.
- * See diagram below.
- *
- * <p> How Flow.Subscribers are used in this class, and where they come from:
- * <pre>
- * {@code
- *
- *
- *
- * ---------> data flow direction
- *
- *
- * +------------------+
- * upstreamWriter | | downWriter
- * ---------------> | | ------------>
- * obtained from this | | supplied to constructor
- * | SSLFlowDelegate |
- * downReader | | upstreamReader
- * <--------------- | | <--------------
- * supplied to constructor | | obtained from this
- * +------------------+
- * }
- * </pre>
- */
-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<Void> cf;
- final CompletableFuture<String> 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<? super List<ByteBuffer>> downReader,
- Subscriber<? super List<ByteBuffer>> 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<Void> 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<? super List<ByteBuffer>> downReader,
- Subscriber<? super List<ByteBuffer>> downWriter) {
- this.reader.subscribe(downReader);
- this.writer.subscribe(downWriter);
- }
-
- /**
- * Returns a CompletableFuture<String> which completes after
- * the initial handshake completes, and which contains the negotiated
- * alpn.
- */
- public CompletableFuture<String> alpn() {
- return alpnCF;
- }
-
- private void setALPN() {
- // Handshake is finished. So, can retrieve the ALPN now
- if (alpnCF.isDone())
- return;
- String alpn = engine.getApplicationProtocol();
- 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<ByteBuffer> 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<ByteBuffer> buffers, boolean complete) {
- synchronized (readBufferLock) {
- for (ByteBuffer buf : buffers) {
- readBuf.compact();
- while (readBuf.remaining() < buf.remaining())
- reallocReadBuf();
- readBuf.put(buf);
- readBuf.flip();
- }
- if (complete) {
- this.completing = complete;
- }
- }
- }
-
- void schedule() {
- scheduler.runOrSchedule();
- }
-
- void stop() {
- 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<Void> completion() {
- return cf;
- }
-
- public interface Monitorable {
- public String getInfo();
- }
-
- public static class Monitor extends Thread {
- final List<Monitorable> list;
- static Monitor themon;
-
- static {
- themon = new Monitor();
- themon.start(); // uncomment to enable Monitor
- }
-
- Monitor() {
- super("Monitor");
- setDaemon(true);
- list = Collections.synchronizedList(new LinkedList<>());
- }
-
- void addTarget(Monitorable o) {
- list.add(o);
- }
-
- public static void add(Monitorable o) {
- themon.addTarget(o);
- }
-
- @Override
- public void run() {
- System.out.println("Monitor starting");
- 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<ByteBuffer> 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<ByteBuffer> 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<ByteBuffer> l) {
- synchronized (l) {
- Iterator<ByteBuffer> iter = l.iterator();
- while (iter.hasNext()) {
- ByteBuffer b = iter.next();
- if (!b.hasRemaining() && b != SENTINEL) {
- iter.remove();
- }
- }
- }
- }
-
- /**
- * States for handshake. We avoid races when accessing/updating the AtomicInt
- * because updates always schedule an additional call to both the read()
- * and write() functions.
- */
- private static final int NOT_HANDSHAKING = 0;
- private static final int HANDSHAKING = 1;
- private static final int 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<String> 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<Runnable> 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<Runnable> obtainTasks() {
- List<Runnable> l = new ArrayList<>();
- Runnable r;
- while ((r = engine.getDelegatedTask()) != null) {
- l.add(r);
- }
- return l;
- }
-
- private void executeTasks(List<Runnable> tasks) {
- exec.execute(() -> {
- try {
- handshakeState.getAndUpdate((current) -> current | DOING_TASKS);
- List<Runnable> nextTasks = tasks;
- do {
- nextTasks.forEach(Runnable::run);
- if (engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
- nextTasks = obtainTasks();
- } else {
- break;
- }
- } while (true);
- handshakeState.getAndUpdate((current) -> current & ~DOING_TASKS);
- writer.addData(HS_TRIGGER);
- resumeActivity();
- } catch (Throwable t) {
- handleError(t);
- }
- });
- }
-
-
- 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<List<ByteBuffer>> upstreamReader() {
- return reader;
- }
-
- /**
- * Returns the upstream Flow.Subscriber of the writing (outgoing) side.
- * This flow contains the plaintext data before it is encrypted.
- */
- public Flow.Subscriber<List<ByteBuffer>> upstreamWriter() {
- return writer;
- }
-
- public boolean resumeReader() {
- return reader.signalScheduling();
- }
-
- public void resetReaderDemand() {
- reader.resetDownstreamDemand();
- }
-
- static class EngineResult {
- final SSLEngineResult result;
- final ByteBuffer destBuffer;
-
- // normal result
- EngineResult(SSLEngineResult result) {
- this(result, null);
- }
-
- EngineResult(SSLEngineResult result, ByteBuffer destBuffer) {
- this.result = result;
- this.destBuffer = destBuffer;
- }
-
- // 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;
- }
- }
- }
-}
--- 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.
- * <p>
- * The following diagram shows a typical usage of the SSLTube, where
- * the SSLTube wraps a SocketTube on the right hand side, and is connected
- * to an HttpConnection on the left hand side.
- *
- * <preformatted>{@code
- * +---------- SSLTube -------------------------+
- * | |
- * | +---SSLFlowDelegate---+ |
- * HttpConnection | | | | SocketTube
- * read sink <- SSLSubscriberW. <- Reader <- upstreamR.() <---- read source
- * (a subscriber) | | \ / | | (a publisher)
- * | | SSLEngine | |
- * HttpConnection | | / \ | | SocketTube
- * write source -> SSLSubscriptionW. -> upstreamW.() -> Writer ----> write sink
- * (a publisher) | | | | (a subscriber)
- * | +---------------------+ |
- * | |
- * +---------------------------------------------+
- * }</preformatted>
- */
-public class SSLTube implements FlowTube {
-
- 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<? super List<ByteBuffer>> downReader,
- Flow.Subscriber<? super List<ByteBuffer>> downWriter) {
- assert downWriter == tube;
- assert downReader == readSubscriber;
-
- // Connect the read sink first. That's the left-hand side
- // downstream subscriber from the HttpConnection (or more
- // accurately, the SSLSubscriberWrapper that will wrap it
- // when SSLTube::connectFlows is called.
- reader.subscribe(downReader);
-
- // Connect the right hand side tube (the socket tube).
- //
- // The SSLFlowDelegate.writer publishes ByteBuffer to
- // the SocketTube for writing on the socket, and the
- // SSLFlowDelegate::upstreamReader subscribes to the
- // SocketTube to receive ByteBuffers read from the socket.
- //
- // Basically this method is equivalent to:
- // // connect the read source:
- // // subscribe the SSLFlowDelegate upstream reader
- // // to the socket tube publisher.
- // tube.subscribe(upstreamReader());
- // // connect the write sink:
- // // subscribe the socket tube write subscriber
- // // with the SSLFlowDelegate downstream writer.
- // writer.subscribe(tube);
- tube.connectFlows(FlowTube.asTubePublisher(writer),
- FlowTube.asTubeSubscriber(upstreamReader()));
-
- // Finally connect the write source. That's the left
- // hand side publisher which will push ByteBuffer for
- // writing and encryption to the SSLFlowDelegate.
- // The writeSubscription is in fact the SSLSubscriptionWrapper
- // that will wrap the subscription provided by the
- // HttpConnection publisher when SSLTube::connectFlows
- // is called.
- upstreamWriter().onSubscribe(writeSubscription);
- }
- }
-
- public CompletableFuture<String> getALPN() {
- return sslDelegate.alpn();
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
- readSubscriber.dropSubscription();
- readSubscriber.setDelegate(s);
- s.onSubscribe(readSubscription);
- }
-
- /**
- * Tells whether, or not, this FlowTube has finished receiving data.
- *
- * @return true when one of this FlowTube Subscriber's OnError or onComplete
- * methods have been invoked
- */
- @Override
- public boolean isFinished() {
- return finished;
- }
-
- private volatile Flow.Subscription readSubscription;
-
- // The DelegateWrapper wraps a subscribed {@code Flow.Subscriber} and
- // tracks the subscriber's state. In particular it makes sure that
- // onComplete/onError are not called before onSubscribed.
- final static class DelegateWrapper implements FlowTube.TubeSubscriber {
- private final FlowTube.TubeSubscriber delegate;
- private final System.Logger debug;
- volatile boolean subscribedCalled;
- volatile boolean subscribedDone;
- volatile boolean completed;
- volatile Throwable error;
- DelegateWrapper(Flow.Subscriber<? super List<ByteBuffer>> 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<ByteBuffer> item) {
- assert subscribedCalled;
- delegate.onNext(item);
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- onSubscribe(delegate::onSubscribe, subscription);
- }
-
- private void onSubscribe(Consumer<Flow.Subscription> method,
- Flow.Subscription subscription) {
- subscribedCalled = true;
- method.accept(subscription);
- Throwable x;
- boolean finished;
- synchronized (this) {
- subscribedDone = true;
- x = error;
- finished = completed;
- }
- if (x != null) {
- 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<DelegateWrapper> pendingDelegate =
- new AtomicReference<>();
- private volatile DelegateWrapper subscribed;
- private volatile boolean onCompleteReceived;
- private final AtomicReference<Throwable> errorRef
- = new AtomicReference<>();
-
- // setDelegate can be called asynchronously when the SSLTube flow
- // is connected. At this time the permanent subscriber (this class)
- // may already be subscribed (readSubscription != null) or not.
- // 1. If it's already subscribed (readSubscription != null), we
- // are going to signal the SSLFlowDelegate reader, and make sure
- // onSubscribed is called within the reader flow
- // 2. If it's not yet subscribed (readSubscription == null), then
- // we're going to wait for onSubscribe to be called.
- //
- void setDelegate(Flow.Subscriber<? super List<ByteBuffer>> delegate) {
- 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<ByteBuffer> item) {
- subscribed.onNext(item);
- }
-
- public void onErrorImpl(Throwable throwable) {
- // The following twisted logic is just here that we don't invoke
- // onError before onSubscribe. It also prevents race conditions
- // if onError is invoked concurrently with setDelegate.
- // See setDelegate.
-
- errorRef.compareAndSet(null, throwable);
- Throwable failed = errorRef.get();
- finished = true;
- 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<ByteBuffer> 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 + ")";
- }
-
-}
--- 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.
- *
- * <p> This class can be used as a synchronization aid that assists a number of
- * parties in running a task in a mutually exclusive fashion.
- *
- * <p> To run the task, a party invokes {@code runOrSchedule}. To permanently
- * prevent the task from subsequent runs, the party invokes {@code stop}.
- *
- * <p> The parties can, but do not have to, operate in different threads.
- *
- * <p> The task can be either synchronous ( completes when its {@code run}
- * method returns ), or asynchronous ( completed when its
- * {@code DeferredCompleter} is explicitly completed ).
- *
- * <p> The next run of the task will not begin until the previous run has
- * finished.
- *
- * <p> The task may invoke {@code runOrSchedule} itself, which may be a normal
- * situation.
- */
-public final class SequentialScheduler {
-
- /*
- Since the task is fixed and known beforehand, no blocking synchronization
- (locks, queues, etc.) is required. The job can be done solely using
- nonblocking primitives.
-
- The machinery below addresses two problems:
-
- 1. Running the task in a sequential order (no concurrent runs):
-
- begin, end, begin, end...
-
- 2. Avoiding indefinite recursion:
-
- begin
- end
- begin
- end
- ...
-
- Problem #1 is solved with a finite state machine with 4 states:
-
- BEGIN, AGAIN, END, and STOP.
-
- Problem #2 is solved with a "state modifier" OFFLOAD.
-
- Parties invoke `runOrSchedule()` to signal the task must run. A party
- that has invoked `runOrSchedule()` either begins the task or exploits the
- party that is either beginning the task or ending it.
-
- The party that is trying to end the task either ends it or begins it
- again.
-
- To avoid indefinite recursion, before re-running the task the
- TryEndDeferredCompleter sets the OFFLOAD bit, signalling to its "child"
- TryEndDeferredCompleter that this ("parent") TryEndDeferredCompleter is
- available and the "child" must offload the task on to the "parent". Then
- a race begins. Whichever invocation of TryEndDeferredCompleter.complete
- manages to unset OFFLOAD bit first does not do the work.
-
- There is at most 1 thread that is beginning the task and at most 2
- threads that are trying to end it: "parent" and "child". In case of a
- synchronous task "parent" and "child" are the same thread.
- */
-
- /**
- * An interface to signal the completion of a {@link RestartableTask}.
- *
- * <p> The invocation of {@code complete} completes the task. The invocation
- * of {@code complete} may restart the task, if an attempt has previously
- * been made to run the task while it was already running.
- *
- * @apiNote {@code DeferredCompleter} is useful when a task is not necessary
- * complete when its {@code run} method returns, but will complete at a
- * later time, and maybe in different thread. This type exists for
- * readability purposes at use-sites only.
- */
- public static abstract class DeferredCompleter {
-
- /** Extensible from this (outer) class ONLY. */
- private DeferredCompleter() { }
-
- /** Completes the task. Must be called once, and once only. */
- public abstract void complete();
- }
-
- /**
- * A restartable task.
- */
- @FunctionalInterface
- public interface RestartableTask {
-
- /**
- * The body of the task.
- *
- * @param taskCompleter
- * A completer that must be invoked once, and only once,
- * when this task is logically finished
- */
- void run(DeferredCompleter taskCompleter);
- }
-
- /**
- * A simple and self-contained task that completes once its {@code run}
- * method returns.
- */
- public static abstract class CompleteRestartableTask
- implements RestartableTask
- {
- @Override
- public final void run(DeferredCompleter taskCompleter) {
- try {
- run();
- } finally {
- taskCompleter.complete();
- }
- }
-
- /** The body of the task. */
- protected abstract void run();
- }
-
- /**
- * A task that runs its main loop within a synchronized block to provide
- * memory visibility between runs. Since the main loop can't run concurrently,
- * the lock shouldn't be contended and no deadlock should ever be possible.
- */
- public static final class SynchronizedRestartableTask
- extends CompleteRestartableTask {
-
- private final Runnable mainLoop;
- private final Object lock = new Object();
-
- public SynchronizedRestartableTask(Runnable mainLoop) {
- this.mainLoop = mainLoop;
- }
-
- @Override
- protected void run() {
- synchronized(lock) {
- mainLoop.run();
- }
- }
- }
-
- private static final int OFFLOAD = 1;
- private static final int AGAIN = 2;
- private static final int BEGIN = 4;
- private static final int STOP = 8;
- private static final int END = 16;
-
- private final AtomicInteger state = new AtomicInteger(END);
- private final RestartableTask restartableTask;
- private final DeferredCompleter completer;
- private final SchedulableTask schedulableTask;
-
- /**
- * An auxiliary task that starts the restartable task:
- * {@code restartableTask.run(completer)}.
- */
- private final class SchedulableTask implements Runnable {
- @Override
- public void run() {
- restartableTask.run(completer);
- }
- }
-
- public SequentialScheduler(RestartableTask restartableTask) {
- this.restartableTask = requireNonNull(restartableTask);
- this.completer = new TryEndDeferredCompleter();
- this.schedulableTask = new SchedulableTask();
- }
-
- /**
- * Runs or schedules the task to be run.
- *
- * @implSpec The recursion which is possible here must be bounded:
- *
- * <pre>{@code
- * this.runOrSchedule()
- * completer.complete()
- * this.runOrSchedule()
- * ...
- * }</pre>
- *
- * @implNote The recursion in this implementation has the maximum
- * depth of 1.
- */
- public void runOrSchedule() {
- runOrSchedule(schedulableTask, null);
- }
-
- /**
- * Executes or schedules the task to be executed in the provided executor.
- *
- * <p> This method can be used when potential executing from a calling
- * thread is not desirable.
- *
- * @param executor
- * An executor in which to execute the task, if the task needs
- * to be executed.
- *
- * @apiNote The given executor can be {@code null} in which case calling
- * {@code runOrSchedule(null)} is strictly equivalent to calling
- * {@code runOrSchedule()}.
- */
- public void runOrSchedule(Executor executor) {
- runOrSchedule(schedulableTask, executor);
- }
-
- private void runOrSchedule(SchedulableTask task, Executor executor) {
- while (true) {
- int s = state.get();
- if (s == END) {
- if (state.compareAndSet(END, BEGIN)) {
- break;
- }
- } else if ((s & BEGIN) != 0) {
- // Tries to change the state to AGAIN, preserving OFFLOAD bit
- if (state.compareAndSet(s, AGAIN | (s & OFFLOAD))) {
- return;
- }
- } else if ((s & AGAIN) != 0 || s == STOP) {
- /* In the case of AGAIN the scheduler does not provide
- happens-before relationship between actions prior to
- runOrSchedule() and actions that happen in task.run().
- The reason is that no volatile write is done in this case,
- and the call piggybacks on the call that has actually set
- AGAIN state. */
- return;
- } else {
- // Non-existent state, or the one that cannot be offloaded
- throw new InternalError(String.valueOf(s));
- }
- }
- if (executor == null) {
- task.run();
- } else {
- executor.execute(task);
- }
- }
-
- /** The only concrete {@code DeferredCompleter} implementation. */
- private class TryEndDeferredCompleter extends DeferredCompleter {
-
- @Override
- public void complete() {
- while (true) {
- int s;
- while (((s = state.get()) & OFFLOAD) != 0) {
- // Tries to offload ending of the task to the parent
- if (state.compareAndSet(s, s & ~OFFLOAD)) {
- return;
- }
- }
- while (true) {
- if ((s & OFFLOAD) != 0) {
- /* OFFLOAD bit can never be observed here. Otherwise
- it would mean there is another invocation of
- "complete" that can run the task. */
- throw new InternalError(String.valueOf(s));
- }
- if (s == BEGIN) {
- if (state.compareAndSet(BEGIN, END)) {
- return;
- }
- } else if (s == AGAIN) {
- if (state.compareAndSet(AGAIN, BEGIN | OFFLOAD)) {
- break;
- }
- } else if (s == STOP) {
- return;
- } else if (s == END) {
- throw new IllegalStateException("Duplicate completion");
- } else {
- // Non-existent state
- throw new InternalError(String.valueOf(s));
- }
- s = state.get();
- }
- restartableTask.run(completer);
- }
- }
- }
-
- /**
- * Tells whether, or not, this scheduler has been permanently stopped.
- *
- * <p> Should be used from inside the task to poll the status of the
- * scheduler, pretty much the same way as it is done for threads:
- * <pre>{@code
- * if (!Thread.currentThread().isInterrupted()) {
- * ...
- * }
- * }</pre>
- */
- public boolean isStopped() {
- return state.get() == STOP;
- }
-
- /**
- * Stops this scheduler. Subsequent invocations of {@code runOrSchedule}
- * are effectively no-ops.
- *
- * <p> If the task has already begun, this invocation will not affect it,
- * unless the task itself uses {@code isStopped()} method to check the state
- * of the handler.
- */
- public void stop() {
- state.set(STOP);
- }
-
- /**
- * Returns a new {@code SequentialScheduler} that executes the provided
- * {@code mainLoop} from within a {@link SynchronizedRestartableTask}.
- *
- * @apiNote This is equivalent to calling
- * {@code new SequentialScheduler(new SynchronizedRestartableTask(mainLoop))}
- * The main loop must not perform any blocking operation.
- *
- * @param mainLoop The main loop of the new sequential scheduler
- * @return a new {@code SequentialScheduler} that executes the provided
- * {@code mainLoop} from within a {@link SynchronizedRestartableTask}.
- */
- public static SequentialScheduler synchronizedScheduler(Runnable mainLoop) {
- return new SequentialScheduler(new SynchronizedRestartableTask(mainLoop));
- }
-}
--- 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.
- * <p>
- * Processing is done by implementing the {@link #incoming(List, boolean)} method
- * which supplies buffers from upstream. This method (or any other method)
- * can then call the outgoing() method to deliver processed buffers downstream.
- * <p>
- * Upstream error signals are delivered downstream directly. Cancellation from
- * downstream is also propagated upstream immediately.
- * <p>
- * Each SubscriberWrapper has a {@link java.util.concurrent.CompletableFuture}{@code <Void>}
- * which propagates completion/errors from downstream to upstream. Normal completion
- * can only occur after onComplete() is called, but errors can be propagated upwards
- * at any time.
- */
-public abstract class SubscriberWrapper
- implements FlowTube.TubeSubscriber, Closeable, Flow.Processor<List<ByteBuffer>,List<ByteBuffer>>
- // TODO: SSLTube Subscriber will never change? Does this really need to be a TS?
-{
- 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<? super List<ByteBuffer>> downstreamSubscriber;
- // processed byte to send to the downstream subscriber.
- private final ConcurrentLinkedQueue<List<ByteBuffer>> outputQ;
- private final CompletableFuture<Void> cf;
- private final SequentialScheduler pushScheduler;
- private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
- final AtomicLong upstreamWindow = new AtomicLong(0);
-
- /**
- * Wraps the given downstream subscriber. For each call to {@link
- * #onNext(List<ByteBuffer>) } the given filter function is invoked
- * and the list (if not empty) returned is passed downstream.
- *
- * A {@code CompletableFuture} is supplied which can be used to signal an
- * error from downstream and which terminates the wrapper or which signals
- * completion of downstream activity which can be propagated upstream. Error
- * completion can be signaled at any time, but normal completion must not be
- * signaled before onComplete() is called.
- */
- public SubscriberWrapper()
- {
- this.outputQ = new ConcurrentLinkedQueue<>();
- this.cf = new MinimalFuture<>();
- this.pushScheduler =
- SequentialScheduler.synchronizedScheduler(new DownstreamPusher());
- this.downstreamSubscription = new SubscriptionBase(pushScheduler,
- this::downstreamCompletion);
- }
-
- @Override
- public final void subscribe(Subscriber<? super List<ByteBuffer>> downstreamSubscriber) {
- Objects.requireNonNull(downstreamSubscriber);
- this.downstreamSubscriber = downstreamSubscriber;
- }
-
- /**
- * Wraps the given downstream wrapper in this. For each call to
- * {@link #onNext(List<ByteBuffer>) } the incoming() method is called.
- *
- * The {@code downstreamCF} from the downstream wrapper is linked to this
- * wrappers notifier.
- *
- * @param downstreamWrapper downstream destination
- */
- public SubscriberWrapper(Subscriber<? super List<ByteBuffer>> downstreamWrapper)
- {
- this();
- subscribe(downstreamWrapper);
- }
-
- /**
- * Delivers data to be processed by this wrapper. Generated data to be sent
- * downstream, must be provided to the {@link #outgoing(List, boolean)}}
- * method.
- *
- * @param buffers a List of ByteBuffers.
- * @param complete if true then no more data will be added to the list
- */
- protected abstract void incoming(List<ByteBuffer> buffers, boolean complete);
-
- /**
- * This method is called to determine the window size to use at any time. The
- * current window is supplied together with the current downstream queue size.
- * {@code 0} should be returned if no change is
- * required or a positive integer which will be added to the current window.
- * The default implementation maintains a downstream queue size of no greater
- * than 5. The method can be overridden if required.
- *
- * @param currentWindow the current upstream subscription window
- * @param downstreamQsize the current number of buffers waiting to be sent
- * downstream
- *
- * @return value to add to currentWindow
- */
- protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) {
- if (downstreamQsize > 5) {
- return 0;
- }
-
- if (currentWindow == 0) {
- return 1;
- } else {
- return 0;
- }
- }
-
- /**
- * Override this if anything needs to be done after the upstream subscriber
- * has subscribed
- */
- protected void onSubscribe() {
- }
-
- /**
- * Override this if anything needs to be done before checking for error
- * and processing the input queue.
- * @return
- */
- protected SchedulingAction enterScheduling() {
- return SchedulingAction.CONTINUE;
- }
-
- protected boolean signalScheduling() {
- if (downstreamCompleted || pushScheduler.isStopped()) {
- return false;
- }
- pushScheduler.runOrSchedule();
- return true;
- }
-
- /**
- * Delivers buffers of data downstream. After incoming()
- * has been called complete == true signifying completion of the upstream
- * subscription, data may continue to be delivered, up to when outgoing() is
- * called complete == true, after which, the downstream subscription is
- * completed.
- *
- * It's an error to call outgoing() with complete = true if incoming() has
- * not previously been called with it.
- */
- public void outgoing(ByteBuffer buffer, boolean complete) {
- Objects.requireNonNull(buffer);
- assert !complete || !buffer.hasRemaining();
- outgoing(List.of(buffer), complete);
- }
-
- /**
- * Sometime it might be necessary to complete the downstream subscriber
- * before the upstream completes. For instance, when an SSL server
- * sends a notify_close. In that case we should let the outgoing
- * complete before upstream us completed.
- * @return true, may be overridden by subclasses.
- */
- public boolean closing() {
- return false;
- }
-
- public void outgoing(List<ByteBuffer> buffers, boolean complete) {
- Objects.requireNonNull(buffers);
- if (complete) {
- assert Utils.remaining(buffers) == 0;
- boolean closing = closing();
- 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<Void> completion() {
- return cf;
- }
-
- /**
- * Invoked whenever it 'may' be possible to push buffers downstream.
- */
- class DownstreamPusher implements Runnable {
- @Override
- public void run() {
- try {
- run1();
- } catch (Throwable t) {
- errorCommon(t);
- }
- }
-
- private void run1() {
- if (downstreamCompleted) {
- 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<ByteBuffer> 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<ByteBuffer> 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<ByteBuffer> 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";
- }
-}
--- 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();
- }
-}
--- 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<String> action = () -> System.getProperty("test.src");
-// TESTING = AccessController.doPrivileged(action) != null;
-// } else TESTING = false;
-// }
- public static final boolean DEBUG = // Revisit: temporary dev flag.
- getBooleanProperty(DebugLogger.HTTP_NAME, false);
- public static final boolean DEBUG_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<String> DISALLOWED_HEADERS_SET;
- static {
- // A case insensitive TreeSet of strings.
- TreeSet<String> treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
- treeSet.addAll(Set.of("connection", "content-length",
- "date", "expect", "from", "host", "origin",
- "referer", "upgrade",
- "via", "warning"));
- DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet);
- }
-
- public static final Predicate<String>
- ALLOWED_HEADERS = header -> !DISALLOWED_HEADERS_SET.contains(header);
-
- private static final Predicate<String> IS_PROXY_HEADER = (k) ->
- k != null && k.length() > 6 && "proxy-".equalsIgnoreCase(k.substring(0,6));
- private static final Predicate<String> NO_PROXY_HEADER =
- IS_PROXY_HEADER.negate();
- private static final Predicate<String> ALL_HEADERS = (s) -> true;
-
- private static final Set<String> PROXY_AUTH_DISABLED_SCHEMES;
- private static final Set<String> PROXY_AUTH_TUNNEL_DISABLED_SCHEMES;
- static {
- String proxyAuthDisabled =
- getNetProperty("jdk.http.auth.proxying.disabledSchemes");
- String proxyAuthTunnelDisabled =
- getNetProperty("jdk.http.auth.tunneling.disabledSchemes");
- PROXY_AUTH_DISABLED_SCHEMES =
- proxyAuthDisabled == null ? Set.of() :
- Stream.of(proxyAuthDisabled.split(","))
- .map(String::trim)
- .filter((s) -> !s.isEmpty())
- .collect(Collectors.toUnmodifiableSet());
- PROXY_AUTH_TUNNEL_DISABLED_SCHEMES =
- proxyAuthTunnelDisabled == null ? Set.of() :
- Stream.of(proxyAuthTunnelDisabled.split(","))
- .map(String::trim)
- .filter((s) -> !s.isEmpty())
- .collect(Collectors.toUnmodifiableSet());
- }
-
- private static final String WSPACES = " \t\r\n";
- private static final boolean isAllowedForProxy(String name,
- List<String> value,
- Set<String> disabledSchemes,
- Predicate<String> allowedKeys) {
- if (!allowedKeys.test(name)) return false;
- if (disabledSchemes.isEmpty()) return true;
- if (name.equalsIgnoreCase("proxy-authorization")) {
- if (value.isEmpty()) return false;
- for (String scheme : disabledSchemes) {
- int slen = scheme.length();
- for (String v : value) {
- int vlen = v.length();
- if (vlen == slen) {
- if (v.equalsIgnoreCase(scheme)) {
- return false;
- }
- } else if (vlen > slen) {
- if (v.substring(0,slen).equalsIgnoreCase(scheme)) {
- int c = v.codePointAt(slen);
- if (WSPACES.indexOf(c) > -1
- || Character.isSpaceChar(c)
- || Character.isWhitespace(c)) {
- return false;
- }
- }
- }
- }
- }
- }
- return true;
- }
-
- public static final BiPredicate<String, List<String>> PROXY_TUNNEL_FILTER =
- (s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_TUNNEL_DISABLED_SCHEMES,
- IS_PROXY_HEADER);
- public static final BiPredicate<String, List<String>> PROXY_FILTER =
- (s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_DISABLED_SCHEMES,
- ALL_HEADERS);
- public static final BiPredicate<String, List<String>> NO_PROXY_HEADERS_FILTER =
- (n,v) -> Utils.NO_PROXY_HEADER.test(n);
-
-
- public static boolean proxyHasDisabledSchemes(boolean tunnel) {
- return tunnel ? ! PROXY_AUTH_TUNNEL_DISABLED_SCHEMES.isEmpty()
- : ! PROXY_AUTH_DISABLED_SCHEMES.isEmpty();
- }
-
- public static ByteBuffer getBuffer() {
- return ByteBuffer.allocate(BUFSIZE);
- }
-
- public static Throwable getCompletionCause(Throwable x) {
- if (!(x instanceof CompletionException)
- && !(x instanceof ExecutionException)) return x;
- final Throwable cause = x.getCause();
- if (cause == null) {
- throw new InternalError("Unexpected null cause", x);
- }
- return cause;
- }
-
- public static IOException getIOException(Throwable t) {
- if (t instanceof IOException) {
- return (IOException) t;
- }
- Throwable cause = t.getCause();
- if (cause != null) {
- return getIOException(cause);
- }
- return new IOException(t);
- }
-
- private Utils() { }
-
- /**
- * Returns the security permissions required to connect to the proxy, or
- * {@code null} if none is required or applicable.
- */
- public static URLPermission permissionForProxy(InetSocketAddress proxyAddress) {
- if (proxyAddress == null)
- return null;
-
- StringBuilder sb = new StringBuilder();
- sb.append("socket://")
- .append(proxyAddress.getHostString()).append(":")
- .append(proxyAddress.getPort());
- String urlString = sb.toString();
- return new URLPermission(urlString, "CONNECT");
- }
-
- /**
- * Returns the security permission required for the given details.
- */
- public static URLPermission permissionForServer(URI uri,
- String method,
- Stream<String> headers) {
- String urlString = new StringBuilder()
- .append(uri.getScheme()).append("://")
- .append(uri.getAuthority())
- .append(uri.getPath()).toString();
-
- StringBuilder actionStringBuilder = new StringBuilder(method);
- String collected = headers.collect(joining(","));
- if (!collected.isEmpty()) {
- actionStringBuilder.append(":").append(collected);
- }
- return new URLPermission(urlString, actionStringBuilder.toString());
- }
-
-
- // ABNF primitives defined in RFC 7230
- private static final boolean[] tchar = new boolean[256];
- private static final boolean[] fieldvchar = new boolean[256];
-
- static {
- char[] allowedTokenChars =
- ("!#$%&'*+-.^_`|~0123456789" +
- "abcdefghijklmnopqrstuvwxyz" +
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray();
- for (char c : allowedTokenChars) {
- tchar[c] = true;
- }
- for (char c = 0x21; c < 0xFF; c++) {
- fieldvchar[c] = true;
- }
- fieldvchar[0x7F] = false; // a little hole (DEL) in the range
- }
-
- /*
- * Validates a RFC 7230 field-name.
- */
- public static boolean isValidName(String token) {
- for (int i = 0; i < token.length(); i++) {
- char c = token.charAt(i);
- if (c > 255 || !tchar[c]) {
- return false;
- }
- }
- return !token.isEmpty();
- }
-
- /**
- * 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<Integer>) () ->
- NetProperties.getInteger(name, defaultValue));
- }
-
- static String getNetProperty(String name) {
- return AccessController.doPrivileged((PrivilegedAction<String>) () ->
- NetProperties.get(name));
- }
-
- static boolean getBooleanProperty(String name, boolean def) {
- return AccessController.doPrivileged((PrivilegedAction<Boolean>) () ->
- 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<ByteBuffer> currentList,
- List<ByteBuffer> buffersToAdd) {
- long accumulatedBytes = 0;
- for (ByteBuffer bufferToAdd : buffersToAdd) {
- int remaining = bufferToAdd.remaining();
- if (remaining <= 0)
- continue;
- int listSize = currentList.size();
- if (listSize == 0) {
- currentList.add(bufferToAdd);
- accumulatedBytes = remaining;
- continue;
- }
-
- ByteBuffer lastBuffer = currentList.get(listSize - 1);
- int freeSpace = lastBuffer.capacity() - lastBuffer.limit();
- if (remaining <= COPY_THRESHOLD && freeSpace >= remaining) {
- // append the new data to the unused space in the last buffer
- int position = lastBuffer.position();
- int limit = lastBuffer.limit();
- lastBuffer.position(limit);
- lastBuffer.limit(limit + remaining);
- lastBuffer.put(bufferToAdd);
- lastBuffer.position(position);
- } else {
- currentList.add(bufferToAdd);
- }
- accumulatedBytes += remaining;
- }
- return accumulatedBytes;
- }
-
- public static ByteBuffer copy(ByteBuffer src) {
- ByteBuffer dst = ByteBuffer.allocate(src.remaining());
- dst.put(src);
- dst.flip();
- return dst;
- }
-
- public static String dump(Object... objects) {
- return Arrays.toString(objects);
- }
-
- public static String stringOf(Collection<?> source) {
- // We don't know anything about toString implementation of this
- // collection, so let's create an array
- return Arrays.toString(source.toArray());
- }
-
- public static long remaining(ByteBuffer[] bufs) {
- long remain = 0;
- for (ByteBuffer buf : bufs) {
- remain += buf.remaining();
- }
- return remain;
- }
-
- public static boolean hasRemaining(List<ByteBuffer> bufs) {
- synchronized (bufs) {
- for (ByteBuffer buf : bufs) {
- if (buf.hasRemaining())
- return true;
- }
- }
- return false;
- }
-
- public static long remaining(List<ByteBuffer> bufs) {
- long remain = 0;
- synchronized (bufs) {
- for (ByteBuffer buf : bufs) {
- remain += buf.remaining();
- }
- }
- return remain;
- }
-
- public static int remaining(List<ByteBuffer> bufs, int max) {
- long remain = 0;
- synchronized (bufs) {
- for (ByteBuffer buf : bufs) {
- remain += buf.remaining();
- if (remain > max) {
- throw new IllegalArgumentException("too many bytes");
- }
- }
- }
- return (int) remain;
- }
-
- public static long remaining(ByteBufferReference[] refs) {
- long remain = 0;
- for (ByteBufferReference ref : refs) {
- remain += ref.get().remaining();
- }
- return remain;
- }
-
- public static int remaining(ByteBufferReference[] refs, int max) {
- long remain = 0;
- for (ByteBufferReference ref : refs) {
- remain += ref.get().remaining();
- if (remain > max) {
- throw new IllegalArgumentException("too many bytes");
- }
- }
- return (int) remain;
- }
-
- public static int remaining(ByteBuffer[] refs, int max) {
- long remain = 0;
- for (ByteBuffer b : refs) {
- remain += b.remaining();
- if (remain > max) {
- throw new IllegalArgumentException("too many bytes");
- }
- }
- return (int) remain;
- }
-
- public static void close(Closeable... closeables) {
- for (Closeable c : closeables) {
- try {
- c.close();
- } catch (IOException ignored) { }
- }
- }
-
- // Put all these static 'empty' singletons here
- public static final ByteBuffer EMPTY_BYTEBUFFER = ByteBuffer.allocate(0);
- public static final ByteBuffer[] EMPTY_BB_ARRAY = new ByteBuffer[0];
- public static final List<ByteBuffer> EMPTY_BB_LIST = List.of();
- public static final ByteBufferReference[] EMPTY_BBR_ARRAY = new ByteBufferReference[0];
-
- /**
- * Returns a slice of size {@code amount} from the given buffer. If the
- * buffer contains more data than {@code amount}, then the slice's capacity
- * ( and, but not just, its limit ) is set to {@code amount}. If the buffer
- * does not contain more data than {@code amount}, then the slice's capacity
- * will be the same as the given buffer's capacity.
- */
- public static ByteBuffer sliceWithLimitedCapacity(ByteBuffer buffer, int amount) {
- final int index = buffer.position() + amount;
- final int limit = buffer.limit();
- if (index != limit) {
- // additional data in the buffer
- buffer.limit(index); // ensures that the slice does not go beyond
- } else {
- // no additional data in the buffer
- buffer.limit(buffer.capacity()); // allows the slice full capacity
- }
-
- ByteBuffer newb = buffer.slice();
- buffer.position(index);
- buffer.limit(limit); // restore the original buffer's limit
- newb.limit(amount); // slices limit to amount (capacity may be greater)
- return newb;
- }
-
- /**
- * Get the Charset from the Content-encoding header. Defaults to
- * UTF_8
- */
- public static Charset charsetFrom(HttpHeaders headers) {
- String type = headers.firstValue("Content-type")
- .orElse("text/html; charset=utf-8");
- int i = type.indexOf(";");
- if (i >= 0) type = type.substring(i+1);
- try {
- HeaderParser parser = new HeaderParser(type);
- String value = parser.findValue("charset");
- if (value == null) return StandardCharsets.UTF_8;
- return Charset.forName(value);
- } catch (Throwable x) {
- Log.logTrace("Can't find charset in \"{0}\" ({1})", type, x);
- return StandardCharsets.UTF_8;
- }
- }
-
- public static UncheckedIOException unchecked(IOException e) {
- return new UncheckedIOException(e);
- }
-
- /**
- * Get a logger for debug HTTP traces.
- *
- * The logger should only be used with levels whose severity is
- * {@code <= DEBUG}. By default, this logger will forward all messages
- * logged to an internal logger named "jdk.internal.httpclient.debug".
- * In addition, if the property -Djdk.internal.httpclient.debug=true is set,
- * it will print the messages on stderr.
- * The logger will add some decoration to the printed message, in the form of
- * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
- *
- * @param dbgTag A lambda that returns a string that identifies the caller
- * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
- *
- * @return A logger for HTTP internal debug traces
- */
- public static Logger getDebugLogger(Supplier<String> dbgTag) {
- return getDebugLogger(dbgTag, DEBUG);
- }
-
- /**
- * Get a logger for debug HTTP traces.The logger should only be used
- * with levels whose severity is {@code <= DEBUG}.
- *
- * By default, this logger will forward all messages logged to an internal
- * logger named "jdk.internal.httpclient.debug".
- * In addition, if the message severity level is >= to
- * the provided {@code errLevel} it will print the messages on stderr.
- * The logger will add some decoration to the printed message, in the form of
- * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
- *
- * @apiNote To obtain a logger that will always print things on stderr in
- * addition to forwarding to the internal logger, use
- * {@code getDebugLogger(this::dbgTag, Level.ALL);}.
- * This is also equivalent to calling
- * {@code getDebugLogger(this::dbgTag, true);}.
- * To obtain a logger that will only forward to the internal logger,
- * use {@code getDebugLogger(this::dbgTag, Level.OFF);}.
- * This is also equivalent to calling
- * {@code getDebugLogger(this::dbgTag, false);}.
- *
- * @param dbgTag A lambda that returns a string that identifies the caller
- * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
- * @param errLevel The level above which messages will be also printed on
- * stderr (in addition to be forwarded to the internal logger).
- *
- * @return A logger for HTTP internal debug traces
- */
- static Logger getDebugLogger(Supplier<String> dbgTag, Level errLevel) {
- return DebugLogger.createHttpLogger(dbgTag, Level.OFF, errLevel);
- }
-
- /**
- * Get a logger for debug HTTP traces.The logger should only be used
- * with levels whose severity is {@code <= DEBUG}.
- *
- * By default, this logger will forward all messages logged to an internal
- * logger named "jdk.internal.httpclient.debug".
- * In addition, the provided boolean {@code on==true}, it will print the
- * messages on stderr.
- * The logger will add some decoration to the printed message, in the form of
- * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
- *
- * @apiNote To obtain a logger that will always print things on stderr in
- * addition to forwarding to the internal logger, use
- * {@code getDebugLogger(this::dbgTag, true);}.
- * This is also equivalent to calling
- * {@code getDebugLogger(this::dbgTag, Level.ALL);}.
- * To obtain a logger that will only forward to the internal logger,
- * use {@code getDebugLogger(this::dbgTag, false);}.
- * This is also equivalent to calling
- * {@code getDebugLogger(this::dbgTag, Level.OFF);}.
- *
- * @param dbgTag A lambda that returns a string that identifies the caller
- * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
- * @param on Whether messages should also be printed on
- * stderr (in addition to be forwarded to the internal logger).
- *
- * @return A logger for HTTP internal debug traces
- */
- public static Logger getDebugLogger(Supplier<String> dbgTag, boolean on) {
- Level errLevel = on ? Level.ALL : Level.OFF;
- return getDebugLogger(dbgTag, errLevel);
- }
-
- /**
- * Get a logger for debug HPACK traces.The logger should only be used
- * with levels whose severity is {@code <= DEBUG}.
- *
- * By default, this logger will forward all messages logged to an internal
- * logger named "jdk.internal.httpclient.hpack.debug".
- * In addition, if the message severity level is >= to
- * the provided {@code outLevel} it will print the messages on stdout.
- * The logger will add some decoration to the printed message, in the form of
- * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
- *
- * @apiNote To obtain a logger that will always print things on 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<String> 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 <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
- *
- * @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<String> dbgTag, boolean on) {
- Level outLevel = on ? Level.ALL : Level.OFF;
- return getHpackLogger(dbgTag, outLevel);
- }
-}
--- 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<ByteBuffer> headerBlocks) {
- super(streamid, flags, headerBlocks);
- }
-
- public ContinuationFrame(int streamid, ByteBuffer headersBlock) {
- this(streamid, 0, List.of(headersBlock));
- }
-
- @Override
- public int type() {
- return TYPE;
- }
-
- @Override
- int length() {
- return headerLength;
- }
-
-}
--- 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<ByteBuffer> data;
- private final int dataLength;
-
- public DataFrame(int streamid, int flags, ByteBuffer data) {
- this(streamid, flags, List.of(data));
- }
-
- public DataFrame(int streamid, int flags, List<ByteBuffer> data) {
- super(streamid, flags);
- this.data = data;
- this.dataLength = Utils.remaining(data, Integer.MAX_VALUE);
- }
-
- public DataFrame(int streamid, int flags, List<ByteBuffer> data, int padLength) {
- this(streamid, flags, data);
- if (padLength > 0) {
- setPadLength(padLength);
- }
- }
-
- @Override
- public int type() {
- return TYPE;
- }
-
- @Override
- int length() {
- return dataLength + (((flags & PADDED) != 0) ? (padLength + 1) : 0);
- }
-
- @Override
- public String flagAsString(int flag) {
- switch (flag) {
- case END_STREAM:
- return "END_STREAM";
- case PADDED:
- return "PADDED";
- }
- return super.flagAsString(flag);
- }
-
- public List<ByteBuffer> getData() {
- return data;
- }
-
- public int getDataLength() {
- return dataLength;
- }
-
- int getPadLength() {
- return padLength;
- }
-
- public void setPadLength(int padLength) {
- this.padLength = padLength;
- flags |= PADDED;
- }
-
- public int payloadLength() {
- // RFC 7540 6.1:
- // The entire DATA frame payload is included in flow control,
- // including the Pad Length and Padding fields if present
- if ((flags & PADDED) != 0) {
- return dataLength + (1 + padLength);
- } else {
- return dataLength;
- }
- }
-
-
-}
--- 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;
- }
-}
--- 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
- * <p>
- * collect buffers until frame decoding is possible,
- * all decoded frames are passed to the FrameProcessor callback in order of decoding.
- *
- * It's a stateful class due to the fact that FramesDecoder stores buffers inside.
- * Should be allocated only the single instance per connection.
- */
-public class FramesDecoder {
-
- static final 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<ByteBuffer> tailBuffers = new ArrayDeque<>();
- private int tailSize = 0;
-
- private boolean slicedToDataFrame = false;
-
- private final List<ByteBuffer> prepareToRelease = new ArrayList<>();
-
- // if true - Frame Header was parsed (9 bytes consumed) and subsequent fields have meaning
- // otherwise - stopped at frames boundary
- private boolean frameHeaderParsed = false;
- private int frameLength;
- private int frameType;
- private int frameFlags;
- private int frameStreamid;
- private boolean closed;
-
- /**
- * Creates Frame Decoder
- *
- * @param frameProcessor - callback for decoded frames
- */
- public FramesDecoder(FrameProcessor frameProcessor) {
- this(frameProcessor, 16 * 1024);
- }
-
- /**
- * Creates Frame Decoder
- * @param frameProcessor - callback for decoded frames
- * @param maxFrameSize - maxFrameSize accepted by this decoder
- */
- public FramesDecoder(FrameProcessor frameProcessor, int maxFrameSize) {
- this.frameProcessor = frameProcessor;
- this.maxFrameSize = Math.min(Math.max(16 * 1024, maxFrameSize), 16 * 1024 * 1024 - 1);
- }
-
- /** Threshold beyond which data is no longer copied into the current buffer,
- * if that buffer has enough unused space. */
- private static final int COPY_THRESHOLD = 8192;
-
- /**
- * Adds the data from the given buffer, and performs frame decoding if
- * possible. Either 1) appends the data from the given buffer to the
- * current buffer ( if there is enough unused space ), or 2) adds it to the
- * next buffer in the queue.
- *
- * If there is enough data to perform frame decoding then, all buffers are
- * decoded and the FrameProcessor is invoked.
- */
- public void decode(ByteBuffer inBoundBuffer) throws IOException {
- if (closed) {
- 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<ByteBuffer> getBuffers(boolean isDataFrame, int bytecount) {
- List<ByteBuffer> res = new ArrayList<>();
- while (bytecount > 0) {
- int remaining = currentBuffer.remaining();
- int extract = Math.min(remaining, bytecount);
- ByteBuffer extractedBuf;
- if (isDataFrame) {
- extractedBuf = Utils.sliceWithLimitedCapacity(currentBuffer, extract)
- .asReadOnlyBuffer();
- slicedToDataFrame = true;
- } else {
- // Header frames here
- // HPACK decoding should performed under lock and immediately after frame decoding.
- // in that case it is safe to release original buffer,
- // because of sliced buffer has a very short life
- extractedBuf = Utils.sliceWithLimitedCapacity(currentBuffer, extract);
- }
- res.add(extractedBuf);
- bytecount -= extract;
- nextBuffer();
- }
- return res;
- }
-
- public void close(String msg) {
- closed = true;
- tailBuffers.clear();
- int bytes = tailSize;
- ByteBuffer b = currentBuffer;
- if (b != null) {
- bytes += b.remaining();
- b.position(b.limit());
- }
- tailSize = 0;
- currentBuffer = null;
- 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<n; i++) {
- int id = getShort();
- int val = getInt();
- if (id > 0 && id <= SettingsFrame.MAX_PARAM) {
- // a known parameter. Ignore otherwise
- sf.setParameter(id, val); // TODO parameters validation
- }
- }
- return sf;
- }
-
- private Http2Frame parsePushPromiseFrame(int frameLength, int streamid, int flags) {
- // non-zero stream
- if (streamid == 0) {
- return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
- "zero streamId for PushPromiseFrame");
- }
- int padLength = 0;
- if ((flags & PushPromiseFrame.PADDED) != 0) {
- padLength = getByte();
- frameLength--;
- }
- int promisedStream = getInt() & 0x7fffffff;
- frameLength -= 4;
- if(frameLength < padLength) {
- return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
- "Padding exceeds the size remaining for the PushPromiseFrame");
- }
- PushPromiseFrame ppf = new PushPromiseFrame(streamid, flags, promisedStream,
- getBuffers(false, frameLength - padLength), padLength);
- skipBytes(padLength);
- return ppf;
- }
-
- private Http2Frame parsePingFrame(int frameLength, int streamid, int flags) {
- // only zero stream
- if (streamid != 0) {
- return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
- "non-zero streamId for PingFrame");
- }
- if(frameLength != 8) {
- return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
- "PingFrame length is "+ frameLength+", expected 8");
- }
- return new PingFrame(flags, getBytes(8));
- }
-
- private Http2Frame parseGoAwayFrame(int frameLength, int streamid, int flags) {
- // only zero stream; no flags
- if (streamid != 0) {
- return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
- "non-zero streamId for GoAwayFrame");
- }
- if (frameLength < 8) {
- return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
- "Invalid GoAway frame size");
- }
- int lastStream = getInt() & 0x7fffffff;
- int errorCode = getInt();
- byte[] debugData = getBytes(frameLength - 8);
- if (debugData.length > 0) {
- Log.logError("GoAway debugData " + new String(debugData, UTF_8));
- }
- return new GoAwayFrame(lastStream, errorCode, debugData);
- }
-
- private Http2Frame parseWindowUpdateFrame(int frameLength, int streamid, int flags) {
- // any stream; no flags
- if(frameLength != 4) {
- return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
- "WindowUpdateFrame length is "+ frameLength+", expected 4");
- }
- return new WindowUpdateFrame(streamid, getInt() & 0x7fffffff);
- }
-
- private Http2Frame parseContinuationFrame(int frameLength, int streamid, int flags) {
- // non-zero stream;
- if (streamid == 0) {
- return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
- "zero streamId for ContinuationFrame");
- }
- return new ContinuationFrame(streamid, flags, getBuffers(false, frameLength));
- }
-
-}
--- 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<ByteBuffer> encodeFrames(List<HeaderFrame> frames) {
- List<ByteBuffer> bufs = new ArrayList<>(frames.size() * 2);
- for (HeaderFrame f : frames) {
- bufs.addAll(encodeFrame(f));
- }
- return bufs;
- }
-
- public ByteBuffer encodeConnectionPreface(byte[] preface, SettingsFrame frame) {
- final int length = frame.length();
- ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length + preface.length);
- buf.put(preface);
- putSettingsFrame(buf, frame, length);
- buf.flip();
- return buf;
- }
-
- public List<ByteBuffer> encodeFrame(Http2Frame frame) {
- switch (frame.type()) {
- case DataFrame.TYPE:
- return encodeDataFrame((DataFrame) frame);
- case HeadersFrame.TYPE:
- return encodeHeadersFrame((HeadersFrame) frame);
- case PriorityFrame.TYPE:
- return encodePriorityFrame((PriorityFrame) frame);
- case ResetFrame.TYPE:
- return encodeResetFrame((ResetFrame) frame);
- case SettingsFrame.TYPE:
- return encodeSettingsFrame((SettingsFrame) frame);
- case PushPromiseFrame.TYPE:
- return encodePushPromiseFrame((PushPromiseFrame) frame);
- case PingFrame.TYPE:
- return encodePingFrame((PingFrame) frame);
- case GoAwayFrame.TYPE:
- return encodeGoAwayFrame((GoAwayFrame) frame);
- case WindowUpdateFrame.TYPE:
- return encodeWindowUpdateFrame((WindowUpdateFrame) frame);
- case ContinuationFrame.TYPE:
- return encodeContinuationFrame((ContinuationFrame) frame);
- default:
- throw new UnsupportedOperationException("Not supported frame "+frame.type()+" ("+frame.getClass().getName()+")");
- }
- }
-
- private static final int NO_FLAGS = 0;
- private static final int ZERO_STREAM = 0;
-
- private List<ByteBuffer> encodeDataFrame(DataFrame frame) {
- // non-zero stream
- assert frame.streamid() != 0;
- ByteBuffer buf = encodeDataFrameStart(frame);
- if (frame.getFlag(DataFrame.PADDED)) {
- return joinWithPadding(buf, frame.getData(), frame.getPadLength());
- } else {
- return join(buf, frame.getData());
- }
- }
-
- private ByteBuffer encodeDataFrameStart(DataFrame frame) {
- boolean isPadded = frame.getFlag(DataFrame.PADDED);
- final int length = frame.getDataLength() + (isPadded ? (frame.getPadLength() + 1) : 0);
- ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 1 : 0));
- putHeader(buf, length, DataFrame.TYPE, frame.getFlags(), frame.streamid());
- if (isPadded) {
- buf.put((byte) frame.getPadLength());
- }
- buf.flip();
- return buf;
- }
-
- private List<ByteBuffer> encodeHeadersFrame(HeadersFrame frame) {
- // non-zero stream
- assert frame.streamid() != 0;
- ByteBuffer buf = encodeHeadersFrameStart(frame);
- if (frame.getFlag(HeadersFrame.PADDED)) {
- return joinWithPadding(buf, frame.getHeaderBlock(), frame.getPadLength());
- } else {
- return join(buf, frame.getHeaderBlock());
- }
- }
-
- private ByteBuffer encodeHeadersFrameStart(HeadersFrame frame) {
- boolean isPadded = frame.getFlag(HeadersFrame.PADDED);
- boolean hasPriority = frame.getFlag(HeadersFrame.PRIORITY);
- final int length = frame.getHeaderLength() + (isPadded ? (frame.getPadLength() + 1) : 0) + (hasPriority ? 5 : 0);
- ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 1 : 0) + (hasPriority ? 5 : 0));
- putHeader(buf, length, HeadersFrame.TYPE, frame.getFlags(), frame.streamid());
- if (isPadded) {
- buf.put((byte) frame.getPadLength());
- }
- if (hasPriority) {
- putPriority(buf, frame.getExclusive(), frame.getStreamDependency(), frame.getWeight());
- }
- buf.flip();
- return buf;
- }
-
- private List<ByteBuffer> encodePriorityFrame(PriorityFrame frame) {
- // non-zero stream; no flags
- assert frame.streamid() != 0;
- final int length = 5;
- ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
- putHeader(buf, length, PriorityFrame.TYPE, NO_FLAGS, frame.streamid());
- putPriority(buf, frame.exclusive(), frame.streamDependency(), frame.weight());
- buf.flip();
- return List.of(buf);
- }
-
- private List<ByteBuffer> encodeResetFrame(ResetFrame frame) {
- // non-zero stream; no flags
- assert frame.streamid() != 0;
- final int length = 4;
- ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
- putHeader(buf, length, ResetFrame.TYPE, NO_FLAGS, frame.streamid());
- buf.putInt(frame.getErrorCode());
- buf.flip();
- return List.of(buf);
- }
-
- private List<ByteBuffer> encodeSettingsFrame(SettingsFrame frame) {
- // only zero stream
- assert frame.streamid() == 0;
- final int length = frame.length();
- ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
- putSettingsFrame(buf, frame, length);
- buf.flip();
- return List.of(buf);
- }
-
- private List<ByteBuffer> encodePushPromiseFrame(PushPromiseFrame frame) {
- // non-zero stream
- assert frame.streamid() != 0;
- boolean isPadded = frame.getFlag(PushPromiseFrame.PADDED);
- final int length = frame.getHeaderLength() + (isPadded ? 5 : 4);
- ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 5 : 4));
- putHeader(buf, length, PushPromiseFrame.TYPE, frame.getFlags(), frame.streamid());
- if (isPadded) {
- buf.put((byte) frame.getPadLength());
- }
- buf.putInt(frame.getPromisedStream());
- buf.flip();
-
- if (frame.getFlag(PushPromiseFrame.PADDED)) {
- return joinWithPadding(buf, frame.getHeaderBlock(), frame.getPadLength());
- } else {
- return join(buf, frame.getHeaderBlock());
- }
- }
-
- private List<ByteBuffer> encodePingFrame(PingFrame frame) {
- // only zero stream
- assert frame.streamid() == 0;
- final int length = 8;
- ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
- putHeader(buf, length, PingFrame.TYPE, frame.getFlags(), ZERO_STREAM);
- buf.put(frame.getData());
- buf.flip();
- return List.of(buf);
- }
-
- private List<ByteBuffer> encodeGoAwayFrame(GoAwayFrame frame) {
- // only zero stream; no flags
- assert frame.streamid() == 0;
- byte[] debugData = frame.getDebugData();
- final int length = 8 + debugData.length;
- ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
- putHeader(buf, length, GoAwayFrame.TYPE, NO_FLAGS, ZERO_STREAM);
- buf.putInt(frame.getLastStream());
- buf.putInt(frame.getErrorCode());
- if (debugData.length > 0) {
- buf.put(debugData);
- }
- buf.flip();
- return List.of(buf);
- }
-
- private List<ByteBuffer> encodeWindowUpdateFrame(WindowUpdateFrame frame) {
- // any stream; no flags
- final int length = 4;
- ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
- putHeader(buf, length, WindowUpdateFrame.TYPE, NO_FLAGS, frame.streamid);
- buf.putInt(frame.getUpdate());
- buf.flip();
- return List.of(buf);
- }
-
- private List<ByteBuffer> encodeContinuationFrame(ContinuationFrame frame) {
- // non-zero stream;
- assert frame.streamid() != 0;
- final int length = frame.getHeaderLength();
- ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE);
- putHeader(buf, length, ContinuationFrame.TYPE, frame.getFlags(), frame.streamid());
- buf.flip();
- return join(buf, frame.getHeaderBlock());
- }
-
- private List<ByteBuffer> joinWithPadding(ByteBuffer buf, List<ByteBuffer> data, int padLength) {
- int len = data.size();
- if (len == 0) return List.of(buf, getPadding(padLength));
- else if (len == 1) return List.of(buf, data.get(0), getPadding(padLength));
- else if (len == 2) return List.of(buf, data.get(0), data.get(1), getPadding(padLength));
- List<ByteBuffer> res = new ArrayList<>(len+2);
- res.add(buf);
- res.addAll(data);
- res.add(getPadding(padLength));
- return res;
- }
-
- private List<ByteBuffer> join(ByteBuffer buf, List<ByteBuffer> data) {
- int len = data.size();
- if (len == 0) return List.of(buf);
- else if (len == 1) return List.of(buf, data.get(0));
- else if (len == 2) return List.of(buf, data.get(0), data.get(1));
- List<ByteBuffer> joined = new ArrayList<>(len + 1);
- joined.add(buf);
- joined.addAll(data);
- return joined;
- }
-
- private void putSettingsFrame(ByteBuffer buf, SettingsFrame frame, int length) {
- // only zero stream;
- assert frame.streamid() == 0;
- putHeader(buf, length, SettingsFrame.TYPE, frame.getFlags(), ZERO_STREAM);
- frame.toByteBuffer(buf);
- }
-
- private void putHeader(ByteBuffer buf, int length, int type, int flags, int streamId) {
- int x = (length << 8) + type;
- buf.putInt(x);
- buf.put((byte) flags);
- buf.putInt(streamId);
- }
-
- private void putPriority(ByteBuffer buf, boolean exclusive, int streamDependency, int weight) {
- buf.putInt(exclusive ? (1 << 31) + streamDependency : streamDependency);
- buf.put((byte) weight);
- }
-
- private ByteBuffer getBuffer(int capacity) {
- return ByteBuffer.allocate(capacity);
- }
-
- public ByteBuffer getPadding(int length) {
- if (length > 255) {
- throw new IllegalArgumentException("Padding too big");
- }
- return ByteBuffer.allocate(length); // zeroed!
- }
-
-}
--- 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();
- }
-
-}
--- 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<ByteBuffer> headerBlocks;
-
- public static final int END_STREAM = 0x1;
- public static final int END_HEADERS = 0x4;
-
- public HeaderFrame(int streamid, int flags, List<ByteBuffer> headerBlocks) {
- super(streamid, flags);
- this.headerBlocks = headerBlocks;
- this.headerLength = Utils.remaining(headerBlocks, Integer.MAX_VALUE);
- }
-
- @Override
- public String flagAsString(int flag) {
- switch (flag) {
- case END_HEADERS:
- return "END_HEADERS";
- case END_STREAM:
- return "END_STREAM";
- }
- return super.flagAsString(flag);
- }
-
-
- public List<ByteBuffer> getHeaderBlock() {
- return headerBlocks;
- }
-
- int getHeaderLength() {
- return headerLength;
- }
-
- /**
- * Returns true if this block is the final block of headers.
- */
- public boolean endHeaders() {
- return getFlag(END_HEADERS);
- }
-}
--- 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<ByteBuffer> headerBlocks, int padLength) {
- super(streamid, flags, headerBlocks);
- if (padLength > 0) {
- setPadLength(padLength);
- }
- }
-
- public HeadersFrame(int streamid, int flags, List<ByteBuffer> headerBlocks) {
- super(streamid, flags, headerBlocks);
- }
-
- public HeadersFrame(int streamid, int flags, ByteBuffer headerBlock) {
- this(streamid, flags, List.of(headerBlock));
- }
-
- @Override
- public int type() {
- return TYPE;
- }
-
- @Override
- int length() {
- return headerLength
- + ((flags & PADDED) != 0 ? (1 + padLength) : 0)
- + ((flags & PRIORITY) != 0 ? 5 : 0);
- }
-
- @Override
- public String flagAsString(int flag) {
- switch (flag) {
- case END_STREAM:
- return "END_STREAM";
- case PADDED:
- return "PADDED";
- case PRIORITY:
- return "PRIORITY";
- }
- return super.flagAsString(flag);
- }
-
- public void setPadLength(int padLength) {
- this.padLength = padLength;
- flags |= PADDED;
- }
-
- int getPadLength() {
- return padLength;
- }
-
- public void setPriority(int streamDependency, boolean exclusive, int weight) {
- this.streamDependency = streamDependency;
- this.exclusive = exclusive;
- this.weight = weight;
- this.flags |= PRIORITY;
- }
-
- public int getStreamDependency() {
- return streamDependency;
- }
-
- public int getWeight() {
- return weight;
- }
-
- public boolean getExclusive() {
- return exclusive;
- }
-
-}
--- 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";
- }
-
-}
--- 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;
- }
-}
--- 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<T> extends Http2Frame {
-
- int streamDependency;
- int weight;
- boolean exclusive;
- T attachment;
-
- public static final int PRIORITY = 0x20;
-
- HttpHeaders user, system;
-
- public OutgoingHeaders(HttpHeaders hdrs1, HttpHeaders hdrs2, T attachment) {
- super(0, 0);
- this.user = hdrs2;
- this.system = hdrs1;
- this.attachment = attachment;
- }
-
- public void setPriority(int streamDependency, boolean exclusive, int weight) {
- this.streamDependency = streamDependency;
- this.exclusive = exclusive;
- this.weight = weight;
- this.flags |= PRIORITY;
- }
-
- public int getStreamDependency() {
- return streamDependency;
- }
-
- public int getWeight() {
- return weight;
- }
-
- public boolean getExclusive() {
- return exclusive;
- }
-
- public T getAttachment() {
- return attachment;
- }
-
- public HttpHeaders getUserHeaders() {
- return user;
- }
-
- public HttpHeaders getSystemHeaders() {
- return system;
- }
-
-}
--- 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();
- }
-
-}
--- 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;
- }
-
-}
--- 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<ByteBuffer> buffers, int padLength) {
- super(streamid, flags, buffers);
- this.promisedStream = promisedStream;
- if(padLength > 0 ) {
- setPadLength(padLength);
- }
- }
-
- @Override
- public int type() {
- return TYPE;
- }
-
- @Override
- int length() {
- return headerLength + ((flags & PADDED) != 0 ? 5 : 4);
- }
-
- @Override
- public String toString() {
- return super.toString() + " promisedStreamid: " + promisedStream
- + " headerLength: " + headerLength;
- }
-
- @Override
- public String flagAsString(int flag) {
- switch (flag) {
- case PADDED:
- return "PADDED";
- case END_HEADERS:
- return "END_HEADERS";
- }
- return super.flagAsString(flag);
- }
-
- public void setPadLength(int padLength) {
- this.padLength = padLength;
- flags |= PADDED;
- }
-
- public int getPadLength() {
- return padLength;
- }
-
- public int getPromisedStream() {
- return promisedStream;
- }
-
- @Override
- public boolean endHeaders() {
- return getFlag(END_HEADERS);
- }
-
-}
--- 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;
- }
-
-}
--- 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;
- }
-}
--- 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;
- }
-
-}
--- 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();
-}
--- 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<Integer> maxSizes;
- private boolean writing;
- private boolean configured;
-
- BulkSizeUpdateWriter maxHeaderTableSizes(Iterable<Integer> sizes) {
- if (configured) {
- throw new IllegalStateException("Already configured");
- }
- requireNonNull(sizes, "sizes");
- maxSizes = sizes.iterator();
- configured = true;
- return this;
- }
-
- @Override
- public boolean write(HeaderTable table, ByteBuffer destination) {
- if (!configured) {
- throw new IllegalStateException("Configure first");
- }
- while (true) {
- if (writing) {
- if (!writer.write(table, destination)) {
- return false;
- }
- writing = false;
- } else if (maxSizes.hasNext()) {
- writing = true;
- writer.reset();
- writer.maxHeaderTableSize(maxSizes.next());
- } else {
- configured = false;
- return true;
- }
- }
- }
-
- @Override
- public BulkSizeUpdateWriter reset() {
- maxSizes = null;
- writing = false;
- configured = false;
- return this;
- }
-}
--- 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.
- *
- * <p> Typical lifecycle looks like this:
- *
- * <p> {@link #Decoder(int) new Decoder}
- * ({@link #setMaxCapacity(int) setMaxCapacity}?
- * {@link #decode(ByteBuffer, boolean, DecodingCallback) decode})*
- *
- * @apiNote
- *
- * <p> The design intentions behind Decoder were to facilitate flexible and
- * incremental style of processing.
- *
- * <p> {@code Decoder} does not require a complete header block in a single
- * {@code ByteBuffer}. The header block can be spread across many buffers of any
- * size and decoded one-by-one the way it makes most sense for the user. This
- * way also allows not to limit the size of the header block.
- *
- * <p> Headers are delivered to the {@linkplain DecodingCallback callback} as
- * soon as they become decoded. Using the callback also gives the user a freedom
- * to decide how headers are processed. The callback does not limit the number
- * of headers decoded during single decoding operation.
- *
- * @since 9
- */
-public final class Decoder {
-
- private final Logger logger;
- private static final AtomicLong DECODERS_IDS = new AtomicLong();
-
- @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.
- *
- * <p> The value has to be agreed between decoder and encoder out-of-band,
- * e.g. by a protocol that uses HPACK
- * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
- *
- * @param capacity
- * a non-negative integer
- *
- * @throws IllegalArgumentException
- * if capacity is negative
- */
- public Decoder(int capacity) {
- id = DECODERS_IDS.incrementAndGet();
- logger = HPACK.getLogger().subLogger("Decoder#" + id);
- if (logger.isLoggable(NORMAL)) {
- logger.log(NORMAL, () -> format("new decoder with maximum table size %s",
- capacity));
- }
- if (logger.isLoggable(NORMAL)) {
- /* To correlate with logging outside HPACK, knowing
- hashCode/toString is important */
- logger.log(NORMAL, () -> {
- String hashCode = Integer.toHexString(
- System.identityHashCode(this));
- return format("toString='%s', identityHashCode=%s",
- toString(), hashCode);
- });
- }
- setMaxCapacity0(capacity);
- table = new 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.
- *
- * <p> The value has to be agreed between decoder and encoder out-of-band,
- * e.g. by a protocol that uses HPACK
- * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
- *
- * @param capacity
- * a non-negative integer
- *
- * @throws IllegalArgumentException
- * if capacity is negative
- */
- public void setMaxCapacity(int capacity) {
- if (logger.isLoggable(NORMAL)) {
- logger.log(NORMAL, () -> format("setting maximum table size to %s",
- capacity));
- }
- setMaxCapacity0(capacity);
- }
-
- private void setMaxCapacity0(int capacity) {
- if (capacity < 0) {
- throw new IllegalArgumentException("capacity >= 0: " + capacity);
- }
- // FIXME: await capacity update if less than what was prior to it
- this.capacity = capacity;
- }
-
- /**
- * Decodes a header block from the given buffer to the given callback.
- *
- * <p> Suppose a header block is represented by a sequence of
- * {@code ByteBuffer}s in the form of {@code Iterator<ByteBuffer>}. And the
- * consumer of decoded headers is represented by the callback. Then to
- * decode the header block, the following approach might be used:
- *
- * <pre>{@code
- * while (buffers.hasNext()) {
- * ByteBuffer input = buffers.next();
- * decoder.decode(input, callback, !buffers.hasNext());
- * }
- * }</pre>
- *
- * <p> The decoder reads as much as possible of the header block from the
- * given buffer, starting at the buffer's position, and increments its
- * position to reflect the bytes read. The buffer's mark and limit will not
- * be modified.
- *
- * <p> Once the method is invoked with {@code endOfHeaderBlock == true}, the
- * current header block is deemed ended, and inconsistencies, if any, are
- * reported immediately by throwing an {@code IOException}.
- *
- * <p> Each callback method is called only after the implementation has
- * processed the corresponding bytes. If the bytes revealed a decoding
- * error, the callback method is not called.
- *
- * <p> In addition to exceptions thrown directly by the method, any
- * exceptions thrown from the {@code callback} will bubble up.
- *
- * @apiNote The method asks for {@code endOfHeaderBlock} flag instead of
- * returning it for two reasons. The first one is that the user of the
- * decoder always knows which chunk is the last. The second one is to throw
- * the most detailed exception possible, which might be useful for
- * diagnosing issues.
- *
- * @implNote This implementation is not atomic in respect to decoding
- * errors. In other words, if the decoding operation has thrown a decoding
- * error, the decoder is no longer usable.
- *
- * @param headerBlock
- * the chunk of the header block, may be empty
- * @param endOfHeaderBlock
- * true if the chunk is the final (or the only one) in the sequence
- *
- * @param consumer
- * the callback
- * @throws IOException
- * in case of a decoding error
- * @throws NullPointerException
- * if either headerBlock or consumer are null
- */
- public void decode(ByteBuffer headerBlock,
- boolean endOfHeaderBlock,
- DecodingCallback consumer) throws IOException {
- requireNonNull(headerBlock, "headerBlock");
- requireNonNull(consumer, "consumer");
- if (logger.isLoggable(NORMAL)) {
- logger.log(NORMAL, () -> format("reading %s, end of header block? %s",
- headerBlock, endOfHeaderBlock));
- }
- while (headerBlock.hasRemaining()) {
- proceed(headerBlock, consumer);
- }
- if (endOfHeaderBlock && state != State.READY) {
- logger.log(NORMAL, () -> format("unexpected end of %s representation",
- state));
- throw new IOException("Unexpected end of header block");
- }
- }
-
- private void proceed(ByteBuffer input, DecodingCallback action)
- throws IOException {
- switch (state) {
- case READY:
- resumeReady(input);
- break;
- case INDEXED:
- resumeIndexed(input, action);
- break;
- case LITERAL:
- resumeLiteral(input, action);
- break;
- case LITERAL_WITH_INDEXING:
- resumeLiteralWithIndexing(input, action);
- break;
- case LITERAL_NEVER_INDEXED:
- resumeLiteralNeverIndexed(input, action);
- break;
- case SIZE_UPDATE:
- resumeSizeUpdate(input, action);
- break;
- default:
- throw new InternalError("Unexpected decoder state: " + state);
- }
- }
-
- private void resumeReady(ByteBuffer input) {
- int b = input.get(input.position()) & 0xff; // absolute read
- State s = states[b];
- if (logger.isLoggable(EXTRA)) {
- logger.log(EXTRA, () -> format("next binary representation %s (first byte 0x%02x)",
- s, b));
- }
- switch (s) {
- case INDEXED:
- integerReader.configure(7);
- state = State.INDEXED;
- firstValueIndex = true;
- break;
- case LITERAL:
- state = State.LITERAL;
- firstValueIndex = (b & 0b0000_1111) != 0;
- if (firstValueIndex) {
- integerReader.configure(4);
- }
- break;
- case LITERAL_WITH_INDEXING:
- state = State.LITERAL_WITH_INDEXING;
- firstValueIndex = (b & 0b0011_1111) != 0;
- if (firstValueIndex) {
- integerReader.configure(6);
- }
- break;
- case LITERAL_NEVER_INDEXED:
- state = State.LITERAL_NEVER_INDEXED;
- firstValueIndex = (b & 0b0000_1111) != 0;
- if (firstValueIndex) {
- integerReader.configure(4);
- }
- break;
- case SIZE_UPDATE:
- integerReader.configure(5);
- state = State.SIZE_UPDATE;
- firstValueIndex = true;
- break;
- default:
- throw new InternalError(String.valueOf(s));
- }
- if (!firstValueIndex) {
- input.get(); // advance, next stop: "String Literal"
- }
- }
-
- // 0 1 2 3 4 5 6 7
- // +---+---+---+---+---+---+---+---+
- // | 1 | Index (7+) |
- // +---+---------------------------+
- //
- private void resumeIndexed(ByteBuffer input, DecodingCallback action)
- throws IOException {
- if (!integerReader.read(input)) {
- return;
- }
- intValue = integerReader.get();
- integerReader.reset();
- if (logger.isLoggable(NORMAL)) {
- logger.log(NORMAL, () -> format("indexed %s", intValue));
- }
- try {
- 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;
- }
-}
--- 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}.
- *
- * <p> Methods of the callback are never called by a decoder with any of the
- * arguments being {@code null}.
- *
- * @apiNote
- *
- * <p> The callback provides methods for all possible
- * <a href="https://tools.ietf.org/html/rfc7541#section-6">binary representations</a>.
- * This could be useful for implementing an intermediary, logging, debugging,
- * etc.
- *
- * <p> The callback is an interface in order to interoperate with lambdas (in
- * the most common use case):
- * <pre>{@code
- * DecodingCallback callback = (name, value) -> System.out.println(name + ", " + value);
- * }</pre>
- *
- * <p> 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.
- *
- * <p> 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.
- *
- * <p> Value sensitivity must be considered, for example, when implementing
- * an intermediary. A {@code value} is sensitive if it was represented as <a
- * href="https://tools.ietf.org/html/rfc7541#section-6.2.3">Literal Header
- * Field Never Indexed</a>.
- *
- * <p> 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
- *
- * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.1">Indexed
- * Header Field</a> decoded.
- *
- * @implSpec
- *
- * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.2.2">Literal
- * Header Field without Indexing</a> decoded, where a {@code name} was
- * referred by an {@code index}.
- *
- * @implSpec
- *
- * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.2.2">Literal
- * Header Field without Indexing</a> decoded, where both a {@code name} and
- * a {@code value} were literal.
- *
- * @implSpec
- *
- * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">Literal
- * Header Field Never Indexed</a> decoded, where a {@code name}
- * was referred by an {@code index}.
- *
- * @implSpec
- *
- * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">Literal
- * Header Field Never Indexed</a> decoded, where both a {@code
- * name} and a {@code value} were literal.
- *
- * @implSpec
- *
- * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.2.1">Literal
- * Header Field with Incremental Indexing</a> decoded, where a {@code name}
- * was referred by an {@code index}.
- *
- * @implSpec
- *
- * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.2.1">Literal
- * Header Field with Incremental Indexing</a> decoded, where both a {@code
- * name} and a {@code value} were literal.
- *
- * @implSpec
- *
- * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.3">Dynamic Table
- * Size Update</a> decoded.
- *
- * @implSpec
- *
- * <p> The default implementation does nothing.
- *
- * @param capacity
- * new capacity of the header table
- */
- default void onSizeUpdate(int capacity) { }
-}
--- 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.
- *
- * <p> Typical lifecycle looks like this:
- *
- * <p> {@link #Encoder(int) new Encoder}
- * ({@link #setMaxCapacity(int) setMaxCapacity}?
- * {@link #encode(ByteBuffer) encode})*
- *
- * <p> Suppose headers are represented by {@code Map<String, List<String>>}.
- * A supplier and a consumer of {@link ByteBuffer}s in forms of
- * {@code Supplier<ByteBuffer>} and {@code Consumer<ByteBuffer>} respectively.
- * Then to encode headers, the following approach might be used:
- *
- * <pre>{@code
- * for (Map.Entry<String, List<String>> 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);
- * }
- * }
- * }</pre>
- *
- * <p> Though the specification <a href="https://tools.ietf.org/html/rfc7541#section-2">does not define</a>
- * how an encoder is to be implemented, a default implementation is provided by
- * the method {@link #header(CharSequence, CharSequence, boolean)}.
- *
- * <p> 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
- *
- * <p> 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.
- *
- * <p> 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.
- *
- * <p> 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
- *
- * <p> 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.
- *
- * <p> The value has to be agreed between decoder and encoder out-of-band,
- * e.g. by a protocol that uses HPACK
- * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
- *
- * @param 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)}.
- *
- * <p> 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.
- *
- * <p> If the {@code value} is sensitive (think security, secrecy, etc.)
- * this encoder will compress it using a special representation
- * (see <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">6.2.3. Literal Header Field Never Indexed</a>).
- *
- * <p> 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.
- *
- * <p> The value has to be agreed between decoder and encoder out-of-band,
- * e.g. by a protocol that uses HPACK
- * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
- *
- * <p> May be called any number of times after or before a complete header
- * has been encoded.
- *
- * <p> 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.
- *
- * <p> Default implementation does not add anything to the headers table,
- * hence this method returns {@code 0}.
- *
- * <p> 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.
- *
- * <p> 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.
- *
- * <p> 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<Integer> 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");
- }
- }
-}
--- 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<String, Level> logLevels =
- Map.of("NORMAL", NORMAL, "EXTRA", EXTRA);
-
- static {
- String PROPERTY = "jdk.internal.httpclient.hpack.log.level";
-
- String value = AccessController.doPrivileged(
- (PrivilegedAction<String>) () -> 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<String> 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);
- }
-
- }
-}
--- 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<String, LinkedHashMap<String, Integer>> 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<String, Integer> 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<String, Integer> 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<x, Integer>. 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<String, Map<String, Long>> map;
- private final CircularBuffer<HeaderField> buffer;
- private long counter = 1;
-
- Table(int capacity) {
- buffer = new CircularBuffer<>(capacity);
- map = new HashMap<>(capacity);
- }
-
- void add(HeaderField f) {
- buffer.add(f);
- Map<String, Long> 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<String, Long> 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<String, Long> 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<E> {
-
- 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;
- }
- }
-}
--- 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.
- *
- * <p> 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;
- }
- }
-}
--- 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;
- }
- }
-}
--- 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;
- }
-}
--- 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;
- }
-}
--- 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;
- }
-}
--- 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;
- }
-}
--- 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);
- }
-}
--- 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();
- }
-}
--- 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);
- }
-}
--- 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;
- }
-}
--- 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;
- }
-}
--- 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;
- }
-}
--- 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
- * <a href="https://tools.ietf.org/html/rfc7541">RFC 7541</a>.
- *
- * <p> Headers can be decoded and encoded by {@link java.net.http.internal.hpack.Decoder}
- * and {@link java.net.http.internal.hpack.Encoder} respectively.
- *
- * <p> Instances of these classes are not safe for use by multiple threads.
- */
-package java.net.http.internal.hpack;
--- 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> proxySelector;
- private final Collection<Pair<String, String>> headers;
- private final Collection<String> 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> proxySelector,
- Collection<Pair<String, String>> headers,
- Collection<String> 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<String> 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<WebSocket> 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<Pair<String, String>> getHeaders() { return headers; }
-
- Collection<String> getSubprotocols() { return subprotocols; }
-
- Duration getConnectTimeout() { return timeout; }
-
- Optional<ProxySelector> 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;
- }
-}
--- 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);
- }
-}
--- 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);
- }
-}
--- 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);
- }
- }
-}
--- 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));
- }
- }
-}
--- 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);
-}
--- 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<String> 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<String> 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<String, String> 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<String> createRequestSubprotocols(
- Collection<String> subprotocols)
- {
- LinkedHashSet<String> 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<Result> send() {
- PrivilegedAction<CompletableFuture<Result>> 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<Result> 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<String> 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<String> 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<String> values = responseHeaders.allValues(headerName);
- if (!values.isEmpty()) {
- throw checkFailed(format("Response field '%s' present: %s",
- headerName,
- stringOf(values)));
- }
- }
-
- private static Optional<String> requireAtMostOne(HttpHeaders responseHeaders,
- String headerName)
- {
- List<String> 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<String> 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<ProxySelector> selector, URI uri) {
- if (!selector.isPresent()) {
- return null;
- }
- URI requestURI = createRequestURI(uri); // Based on the HTTP scheme
- List<Proxy> 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<String> 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);
- }
- }
-}
--- 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;
- }
- }
-}
--- 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.
- *
- * <p> Once shutdown for reading then further reads on the channel will
- * return {@code null}, the end-of-stream indication. If the input side of
- * the connection is already shutdown then invoking this method has no
- * effect.
- *
- * @throws ClosedChannelException
- * If this channel is closed
- * @throws IOException
- * If some other I/O error occurs
- */
- void shutdownInput() throws IOException;
-
- /**
- * Shutdown the connection for writing without closing the channel.
- *
- * <p> Once shutdown for writing then further attempts to write to the
- * channel will throw {@link ClosedChannelException}. If the output side of
- * the connection is already shutdown then invoking this method has no
- * effect.
- *
- * @throws ClosedChannelException
- * If this channel is closed
- * @throws IOException
- * If some other I/O error occurs
- */
- void shutdownOutput() throws IOException;
-
- /**
- * Closes this channel.
- *
- * @throws IOException
- * If an I/O error occurs
- */
- @Override
- void close() throws IOException;
-}
--- 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;
- }
- }
-}
--- 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<T> sendMessage(...)
- * (3) CompletableFuture<T> 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<T> used is Transport<WebSocket>. 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<K> returned from Transport, to CompletableFuture<V>
- * needed by the said class.
- */
-public interface Transport<T> {
-
- CompletableFuture<T> sendText(CharSequence message, boolean isLast);
-
- CompletableFuture<T> sendBinary(ByteBuffer message, boolean isLast);
-
- CompletableFuture<T> sendPing(ByteBuffer message);
-
- CompletableFuture<T> sendPong(ByteBuffer message);
-
- CompletableFuture<T> 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;
-}
--- 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 {
-
- <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
- MessageStreamConsumer consumer);
-}
--- 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 <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
- MessageStreamConsumer consumer) {
- return new TransportImpl<T>(sendResultSupplier, consumer, channel);
- }
-}
--- 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<T> implements Transport<T> {
-
- /* This flag is used solely for assertions */
- private final AtomicBoolean busy = new AtomicBoolean();
- private OutgoingMessage message;
- private Consumer<Exception> completionHandler;
- private final RawChannel channel;
- private final RawChannel.RawEvent writeEvent = createWriteEvent();
- private final SequentialScheduler sendScheduler = new SequentialScheduler(new SendTask());
- private final Queue<Pair<OutgoingMessage, CompletableFuture<T>>>
- queue = new ConcurrentLinkedQueue<>();
- private final OutgoingMessage.Context context = new OutgoingMessage.Context();
- private final Supplier<T> 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<T> 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<Exception> 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<Exception> 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<T> 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<T> sendBinary(ByteBuffer message,
- boolean isLast) {
- return enqueue(new OutgoingMessage.Binary(message, isLast));
- }
-
- public CompletableFuture<T> sendPing(ByteBuffer message) {
- OutgoingMessage.Ping m;
- try {
- m = new OutgoingMessage.Ping(message);
- } catch (IllegalArgumentException e) {
- return failedFuture(e);
- }
- return enqueue(m);
- }
-
- public CompletableFuture<T> sendPong(ByteBuffer message) {
- OutgoingMessage.Pong m;
- try {
- m = new OutgoingMessage.Pong(message);
- } catch (IllegalArgumentException e) {
- return failedFuture(e);
- }
- return enqueue(m);
- }
-
- public CompletableFuture<T> 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<T> enqueue(OutgoingMessage m) {
- CompletableFuture<T> 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<OutgoingMessage, CompletableFuture<T>> p = queue.poll();
- if (p == null) {
- taskCompleter.complete();
- return;
- }
- OutgoingMessage message = p.first;
- CompletableFuture<T> cf = p.second;
- try {
- if (!message.contextualize(context)) { // Do not send the message
- cf.complete(resultSupplier.get());
- repeat(taskCompleter);
- return;
- }
- Consumer<Exception> 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();
- }
- }
- }
- }
- }
-}
--- 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();
- }
-}
--- 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> 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<Throwable> error = new AtomicReference<>();
-
- private final URI uri;
- private final String subprotocol;
- private final Listener listener;
-
- private final AtomicBoolean outstandingSend = new AtomicBoolean();
- private final Transport<WebSocket> transport;
- private final SequentialScheduler receiveScheduler = new SequentialScheduler(new ReceiveTask());
- private final Demand demand = new Demand();
-
- public static CompletableFuture<WebSocket> newInstanceAsync(BuilderImpl b) {
- Function<Result, WebSocket> 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<WebSocket> sendText(CharSequence message,
- boolean isLast) {
- Objects.requireNonNull(message);
- if (!outstandingSend.compareAndSet(false, true)) {
- return failedFuture(new IllegalStateException("Send pending"));
- }
- CompletableFuture<WebSocket> cf = transport.sendText(message, isLast);
- return cf.whenComplete((r, e) -> outstandingSend.set(false));
- }
-
- @Override
- public CompletableFuture<WebSocket> sendBinary(ByteBuffer message,
- boolean isLast) {
- Objects.requireNonNull(message);
- if (!outstandingSend.compareAndSet(false, true)) {
- return failedFuture(new IllegalStateException("Send pending"));
- }
- CompletableFuture<WebSocket> 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<WebSocket> sendPing(ByteBuffer message) {
- return transport.sendPing(message);
- }
-
- @Override
- public CompletableFuture<WebSocket> sendPong(ByteBuffer message) {
- return transport.sendPong(message);
- }
-
- @Override
- public CompletableFuture<WebSocket> 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<WebSocket> 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<WebSocket> 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<WebSocket> transport() {
- return transport;
- }
-}
--- 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);
-}
--- /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<String> getALPN() {
+ assert connected();
+ return getConnectionFlow().getALPN();
+ }
+
+ final SSLEngine getEngine() { return engine; }
+
+ private static SSLParameters createSSLParameters(HttpClientImpl client,
+ String serverName,
+ String[] alpn) {
+ SSLParameters sslp = client.sslParameters();
+ SSLParameters sslParameters = Utils.copySSLParameters(sslp);
+ if (alpn != null) {
+ Log.logSSL("AbstractAsyncSSLConnection: Setting application protocols: {0}",
+ Arrays.toString(alpn));
+ sslParameters.setApplicationProtocols(alpn);
+ } else {
+ Log.logSSL("AbstractAsyncSSLConnection: no applications set!");
+ }
+ if (serverName != null) {
+ sslParameters.setServerNames(List.of(new SNIHostName(serverName)));
+ }
+ return sslParameters;
+ }
+
+ private static SSLEngine createEngine(SSLContext context,
+ SSLParameters sslParameters) {
+ SSLEngine engine = context.createSSLEngine();
+ engine.setUseClientMode(true);
+ engine.setSSLParameters(sslParameters);
+ return engine;
+ }
+
+ @Override
+ final boolean isSecure() {
+ return true;
+ }
+
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ static final class SSLConnectionChannel extends DetachedConnectionChannel {
+ final DetachedConnectionChannel delegate;
+ final SSLDelegate sslDelegate;
+ SSLConnectionChannel(DetachedConnectionChannel delegate, SSLDelegate sslDelegate) {
+ this.delegate = delegate;
+ this.sslDelegate = sslDelegate;
+ }
+
+ SocketChannel channel() {
+ return delegate.channel();
+ }
+
+ @Override
+ ByteBuffer read() throws IOException {
+ SSLDelegate.WrapperResult r = sslDelegate.recvData(ByteBuffer.allocate(8192));
+ // TODO: check for closure
+ int n = r.result.bytesProduced();
+ if (n > 0) {
+ return r.buf;
+ } else if (n == 0) {
+ return Utils.EMPTY_BYTEBUFFER;
+ } else {
+ return null;
+ }
+ }
+ @Override
+ long write(ByteBuffer[] buffers, int start, int number) throws IOException {
+ long l = SSLDelegate.countBytes(buffers, start, number);
+ SSLDelegate.WrapperResult r = sslDelegate.sendData(buffers, start, number);
+ if (r.result.getStatus() == SSLEngineResult.Status.CLOSED) {
+ if (l > 0) {
+ throw new IOException("SSLHttpConnection closed");
+ }
+ }
+ return l;
+ }
+ @Override
+ public void shutdownInput() throws IOException {
+ delegate.shutdownInput();
+ }
+ @Override
+ public void shutdownOutput() throws IOException {
+ delegate.shutdownOutput();
+ }
+ @Override
+ public void close() {
+ delegate.close();
+ }
+ }
+
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ @Override
+ DetachedConnectionChannel detachChannel() {
+ assert client() != null;
+ DetachedConnectionChannel detachedChannel = plainConnection().detachChannel();
+ SSLDelegate sslDelegate = new SSLDelegate(engine,
+ detachedChannel.channel());
+ return new SSLConnectionChannel(detachedChannel, sslDelegate);
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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; }
+
+}
--- /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;
+ }
+}
--- /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<Void> connectAsync() {
+ return plainConnection
+ .connectAsync()
+ .thenApply( unused -> {
+ // create the SSLTube wrapping the SocketTube, with the given engine
+ flow = new SSLTube(engine,
+ client().theExecutor(),
+ plainConnection.getConnectionFlow());
+ return null; } );
+ }
+
+ @Override
+ boolean connected() {
+ return plainConnection.connected();
+ }
+
+ @Override
+ HttpPublisher publisher() { return writePublisher; }
+
+ @Override
+ boolean isProxied() {
+ return false;
+ }
+
+ @Override
+ SocketChannel channel() {
+ return plainConnection.channel();
+ }
+
+ @Override
+ ConnectionPool.CacheKey cacheKey() {
+ return ConnectionPool.cacheKey(address, null);
+ }
+
+ @Override
+ public void close() {
+ plainConnection.close();
+ }
+
+ @Override
+ void shutdownInput() throws IOException {
+ debug.log(Level.DEBUG, "plainConnection.channel().shutdownInput()");
+ plainConnection.channel().shutdownInput();
+ }
+
+ @Override
+ void shutdownOutput() throws IOException {
+ debug.log(Level.DEBUG, "plainConnection.channel().shutdownOutput()");
+ plainConnection.channel().shutdownOutput();
+ }
+
+ @Override
+ SSLTube getConnectionFlow() {
+ return flow;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<Void> connectAsync() {
+ debug.log(Level.DEBUG, "Connecting plain tunnel connection");
+ // This will connect the PlainHttpConnection flow, so that
+ // its HttpSubscriber and HttpPublisher are subscribed to the
+ // SocketTube
+ return plainConnection
+ .connectAsync()
+ .thenApply( unused -> {
+ debug.log(Level.DEBUG, "creating SSLTube");
+ // create the SSLTube wrapping the SocketTube, with the given engine
+ flow = new SSLTube(engine,
+ client().theExecutor(),
+ plainConnection.getConnectionFlow());
+ return null;} );
+ }
+
+ @Override
+ boolean isTunnel() { return true; }
+
+ @Override
+ boolean connected() {
+ return plainConnection.connected(); // && sslDelegate.connected();
+ }
+
+ @Override
+ HttpPublisher publisher() { return writePublisher; }
+
+ @Override
+ public String toString() {
+ return "AsyncSSLTunnelConnection: " + super.toString();
+ }
+
+ @Override
+ PlainTunnelingConnection plainConnection() {
+ return plainConnection;
+ }
+
+ @Override
+ ConnectionPool.CacheKey cacheKey() {
+ return ConnectionPool.cacheKey(address, plainConnection.proxyAddr);
+ }
+
+ @Override
+ public void close() {
+ plainConnection.close();
+ }
+
+ @Override
+ void shutdownInput() throws IOException {
+ plainConnection.channel().shutdownInput();
+ }
+
+ @Override
+ void shutdownOutput() throws IOException {
+ plainConnection.channel().shutdownOutput();
+ }
+
+ @Override
+ SocketChannel channel() {
+ return plainConnection.channel();
+ }
+
+ @Override
+ boolean isProxied() {
+ return true;
+ }
+
+ @Override
+ SSLTube getConnectionFlow() {
+ return flow;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<? super IOException> errorHandler;
+ AsyncTriggerEvent(Consumer<? super IOException> errorHandler,
+ Runnable trigger) {
+ super(0);
+ this.trigger = Objects.requireNonNull(trigger);
+ this.errorHandler = Objects.requireNonNull(errorHandler);
+ }
+ /** Returns null */
+ @Override
+ public SelectableChannel channel() { return null; }
+ /** Returns 0 */
+ @Override
+ public int interestOps() { return 0; }
+ @Override
+ public void handle() { trigger.run(); }
+ @Override
+ public void abort(IOException ioe) { errorHandler.accept(ioe); }
+ @Override
+ public boolean repeating() { return false; }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<String> BASIC_DUMMY =
+ List.of("Basic " + Base64.getEncoder()
+ .encodeToString("o:o".getBytes(ISO_8859_1)));
+
+ // A public no-arg constructor is required by FilterFactory
+ public AuthenticationFilter() {}
+
+ private PasswordAuthentication getCredentials(String header,
+ boolean proxy,
+ HttpRequestImpl req)
+ throws IOException
+ {
+ HttpClientImpl client = exchange.client();
+ java.net.Authenticator auth =
+ client.authenticator()
+ .orElseThrow(() -> new IOException("No authenticator set"));
+ URI uri = req.uri();
+ HeaderParser parser = new HeaderParser(header);
+ String authscheme = parser.findKey(0);
+
+ String realm = parser.findValue("realm");
+ java.net.Authenticator.RequestorType rtype = proxy ? PROXY : SERVER;
+ URL url = toURL(uri, req.method(), proxy);
+
+ // needs to be instance method in Authenticator
+ return auth.requestPasswordAuthenticationInstance(uri.getHost(),
+ null,
+ uri.getPort(),
+ uri.getScheme(),
+ realm,
+ authscheme,
+ url,
+ rtype
+ );
+ }
+
+ private URL toURL(URI uri, String method, boolean proxy)
+ throws MalformedURLException
+ {
+ if (proxy && "CONNECT".equalsIgnoreCase(method)
+ && "socket".equalsIgnoreCase(uri.getScheme())) {
+ return null; // proxy tunneling
+ }
+ return uri.toURL();
+ }
+
+ private URI getProxyURI(HttpRequestImpl r) {
+ InetSocketAddress proxy = r.proxy();
+ if (proxy == null) {
+ return null;
+ }
+
+ // our own private scheme for proxy URLs
+ // eg. proxy.http://host:port/
+ String scheme = "proxy." + r.uri().getScheme();
+ try {
+ return new URI(scheme,
+ null,
+ proxy.getHostString(),
+ proxy.getPort(),
+ null,
+ null,
+ null);
+ } catch (URISyntaxException e) {
+ throw new InternalError(e);
+ }
+ }
+
+ @Override
+ public void request(HttpRequestImpl r, MultiExchange<?> e) throws IOException {
+ // use preemptive authentication if an entry exists.
+ Cache cache = getCache(e);
+ this.exchange = e;
+
+ // Proxy
+ if (exchange.proxyauth == null) {
+ URI proxyURI = getProxyURI(r);
+ if (proxyURI != null) {
+ CacheEntry ca = cache.get(proxyURI, true);
+ if (ca != null) {
+ exchange.proxyauth = new AuthInfo(true, ca.scheme, null, ca);
+ addBasicCredentials(r, true, ca.value);
+ }
+ }
+ }
+
+ // Server
+ if (exchange.serverauth == null) {
+ CacheEntry ca = cache.get(r.uri(), false);
+ if (ca != null) {
+ exchange.serverauth = new AuthInfo(true, ca.scheme, null, ca);
+ addBasicCredentials(r, false, ca.value);
+ }
+ }
+ }
+
+ // TODO: refactor into per auth scheme class
+ private static void addBasicCredentials(HttpRequestImpl r,
+ boolean proxy,
+ PasswordAuthentication pw) {
+ String hdrname = proxy ? "Proxy-Authorization" : "Authorization";
+ StringBuilder sb = new StringBuilder(128);
+ sb.append(pw.getUserName()).append(':').append(pw.getPassword());
+ String s = encoder.encodeToString(sb.toString().getBytes(ISO_8859_1));
+ String value = "Basic " + s;
+ if (proxy) {
+ if (r.isConnect()) {
+ if (!Utils.PROXY_TUNNEL_FILTER
+ .test(hdrname, List.of(value))) {
+ Log.logError("{0} disabled", hdrname);
+ return;
+ }
+ } else if (r.proxy() != null) {
+ if (!Utils.PROXY_FILTER
+ .test(hdrname, List.of(value))) {
+ Log.logError("{0} disabled", hdrname);
+ return;
+ }
+ }
+ }
+ r.setSystemHeader(hdrname, value);
+ }
+
+ // Information attached to a HttpRequestImpl relating to authentication
+ static class AuthInfo {
+ final boolean fromcache;
+ final String scheme;
+ int retries;
+ PasswordAuthentication credentials; // used in request
+ CacheEntry cacheEntry; // if used
+
+ AuthInfo(boolean fromcache,
+ String scheme,
+ PasswordAuthentication credentials) {
+ this.fromcache = fromcache;
+ this.scheme = scheme;
+ this.credentials = credentials;
+ this.retries = 1;
+ }
+
+ AuthInfo(boolean fromcache,
+ String scheme,
+ PasswordAuthentication credentials,
+ CacheEntry ca) {
+ this(fromcache, scheme, credentials);
+ assert credentials == null || (ca != null && ca.value == null);
+ cacheEntry = ca;
+ }
+
+ AuthInfo retryWithCredentials(PasswordAuthentication pw) {
+ // If the info was already in the cache we need to create a new
+ // instance with fromCache==false so that it's put back in the
+ // cache if authentication succeeds
+ AuthInfo res = fromcache ? new AuthInfo(false, scheme, pw) : this;
+ res.credentials = Objects.requireNonNull(pw);
+ res.retries = retries;
+ return res;
+ }
+
+ }
+
+ @Override
+ public HttpRequestImpl response(Response r) throws IOException {
+ Cache cache = getCache(exchange);
+ int status = r.statusCode();
+ HttpHeaders hdrs = r.headers();
+ HttpRequestImpl req = r.request();
+
+ if (status != UNAUTHORIZED && status != PROXY_UNAUTHORIZED) {
+ // check if any authentication succeeded for first time
+ if (exchange.serverauth != null && !exchange.serverauth.fromcache) {
+ AuthInfo au = exchange.serverauth;
+ cache.store(au.scheme, req.uri(), false, au.credentials);
+ }
+ if (exchange.proxyauth != null && !exchange.proxyauth.fromcache) {
+ AuthInfo au = exchange.proxyauth;
+ cache.store(au.scheme, req.uri(), false, au.credentials);
+ }
+ return null;
+ }
+
+ boolean proxy = status == PROXY_UNAUTHORIZED;
+ String authname = proxy ? "Proxy-Authenticate" : "WWW-Authenticate";
+ String authval = hdrs.firstValue(authname).orElseThrow(() -> {
+ return new IOException("Invalid auth header");
+ });
+ HeaderParser parser = new HeaderParser(authval);
+ String scheme = parser.findKey(0);
+
+ // TODO: Need to generalise from Basic only. Delegate to a provider class etc.
+
+ if (!scheme.equalsIgnoreCase("Basic")) {
+ return null; // error gets returned to app
+ }
+
+ if (proxy) {
+ if (r.isConnectResponse) {
+ if (!Utils.PROXY_TUNNEL_FILTER
+ .test("Proxy-Authorization", BASIC_DUMMY)) {
+ Log.logError("{0} disabled", "Proxy-Authorization");
+ return null;
+ }
+ } else if (req.proxy() != null) {
+ if (!Utils.PROXY_FILTER
+ .test("Proxy-Authorization", BASIC_DUMMY)) {
+ Log.logError("{0} disabled", "Proxy-Authorization");
+ return null;
+ }
+ }
+ }
+
+ AuthInfo au = proxy ? exchange.proxyauth : exchange.serverauth;
+ if (au == null) {
+ // if no authenticator, let the user deal with 407/401
+ if (!exchange.client().authenticator().isPresent()) return null;
+
+ PasswordAuthentication pw = getCredentials(authval, proxy, req);
+ if (pw == null) {
+ throw new IOException("No credentials provided");
+ }
+ // No authentication in request. Get credentials from user
+ au = new AuthInfo(false, "Basic", pw);
+ if (proxy) {
+ exchange.proxyauth = au;
+ } else {
+ exchange.serverauth = au;
+ }
+ addBasicCredentials(req, proxy, pw);
+ return req;
+ } else if (au.retries > retry_limit) {
+ throw new IOException("too many authentication attempts. Limit: " +
+ Integer.toString(retry_limit));
+ } else {
+ // we sent credentials, but they were rejected
+ if (au.fromcache) {
+ cache.remove(au.cacheEntry);
+ }
+
+ // if no authenticator, let the user deal with 407/401
+ if (!exchange.client().authenticator().isPresent()) return null;
+
+ // try again
+ PasswordAuthentication pw = getCredentials(authval, proxy, req);
+ if (pw == null) {
+ throw new IOException("No credentials provided");
+ }
+ au = au.retryWithCredentials(pw);
+ if (proxy) {
+ exchange.proxyauth = au;
+ } else {
+ exchange.serverauth = au;
+ }
+ addBasicCredentials(req, proxy, au.credentials);
+ au.retries++;
+ return req;
+ }
+ }
+
+ // Use a WeakHashMap to make it possible for the HttpClient to
+ // be garbaged collected when no longer referenced.
+ static final WeakHashMap<HttpClientImpl,Cache> caches = new WeakHashMap<>();
+
+ static synchronized Cache getCache(MultiExchange<?> exchange) {
+ HttpClientImpl client = exchange.client();
+ Cache c = caches.get(client);
+ if (c == null) {
+ c = new Cache();
+ caches.put(client, c);
+ }
+ return c;
+ }
+
+ // Note: Make sure that Cache and CacheEntry do not keep any strong
+ // reference to the HttpClient: it would prevent the client being
+ // GC'ed when no longer referenced.
+ static class Cache {
+ final LinkedList<CacheEntry> entries = new LinkedList<>();
+
+ synchronized CacheEntry get(URI uri, boolean proxy) {
+ for (CacheEntry entry : entries) {
+ if (entry.equalsKey(uri, proxy)) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ synchronized void remove(String authscheme, URI domain, boolean proxy) {
+ for (CacheEntry entry : entries) {
+ if (entry.equalsKey(domain, proxy)) {
+ entries.remove(entry);
+ }
+ }
+ }
+
+ synchronized void remove(CacheEntry entry) {
+ entries.remove(entry);
+ }
+
+ synchronized void store(String authscheme,
+ URI domain,
+ boolean proxy,
+ PasswordAuthentication value) {
+ remove(authscheme, domain, proxy);
+ entries.add(new CacheEntry(authscheme, domain, proxy, value));
+ }
+ }
+
+ static class CacheEntry {
+ final String root;
+ final String scheme;
+ final boolean proxy;
+ final PasswordAuthentication value;
+
+ CacheEntry(String authscheme,
+ URI uri,
+ boolean proxy,
+ PasswordAuthentication value) {
+ this.scheme = authscheme;
+ this.root = uri.resolve(".").toString(); // remove extraneous components
+ this.proxy = proxy;
+ this.value = value;
+ }
+
+ public PasswordAuthentication value() {
+ return value;
+ }
+
+ public boolean equalsKey(URI uri, boolean proxy) {
+ if (this.proxy != proxy) {
+ return false;
+ }
+ String other = uri.toString();
+ return other.startsWith(root);
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<T> implements BodySubscriber<T>
+{
+ /** The downstream consumer of the data. */
+ private final BodySubscriber<T> downstreamSubscriber;
+ /** The amount of data to be accumulate before pushing downstream. */
+ private final int bufferSize;
+
+ /** The subscription, created lazily. */
+ private volatile Flow.Subscription subscription;
+ /** The downstream subscription, created lazily. */
+ private volatile DownstreamSubscription downstreamSubscription;
+
+ /** Must be held when accessing the internal buffers. */
+ private final Object buffersLock = new Object();
+ /** The internal buffers holding the buffered data. */
+ private ArrayList<ByteBuffer> internalBuffers;
+ /** The actual accumulated remaining bytes in internalBuffers. */
+ private int accumulatedBytes;
+
+ /** Holds the Throwable from upstream's onError. */
+ private volatile Throwable throwable;
+
+ /** State of the buffering subscriber:
+ * 1) [UNSUBSCRIBED] when initially created
+ * 2) [ACTIVE] when subscribed and can receive data
+ * 3) [ERROR | CANCELLED | COMPLETE] (terminal state)
+ */
+ static final int UNSUBSCRIBED = 0x01;
+ static final int ACTIVE = 0x02;
+ static final int ERROR = 0x04;
+ static final int CANCELLED = 0x08;
+ static final int COMPLETE = 0x10;
+
+ private volatile int state;
+
+ public BufferingSubscriber(BodySubscriber<T> downstreamSubscriber,
+ int bufferSize) {
+ this.downstreamSubscriber = Objects.requireNonNull(downstreamSubscriber);
+ this.bufferSize = bufferSize;
+ synchronized (buffersLock) {
+ internalBuffers = new ArrayList<>();
+ }
+ state = UNSUBSCRIBED;
+ }
+
+ /** Returns the number of bytes remaining in the given buffers. */
+ private static final long remaining(List<ByteBuffer> buffers) {
+ return buffers.stream().mapToLong(ByteBuffer::remaining).sum();
+ }
+
+ /**
+ * Tells whether, or not, there is at least a sufficient number of bytes
+ * accumulated in the internal buffers. If the subscriber is COMPLETE, and
+ * has some buffered data, then there is always enough ( to pass downstream ).
+ */
+ private final boolean hasEnoughAccumulatedBytes() {
+ assert Thread.holdsLock(buffersLock);
+ return accumulatedBytes >= bufferSize
+ || (state == COMPLETE && accumulatedBytes > 0);
+ }
+
+ /**
+ * Returns a new, unmodifiable, List<ByteBuffer> containing exactly the
+ * amount of data as required before pushing downstream. The amount of data
+ * may be less than required ( bufferSize ), in the case where the subscriber
+ * is COMPLETE.
+ */
+ private List<ByteBuffer> fromInternalBuffers() {
+ assert Thread.holdsLock(buffersLock);
+ int leftToFill = bufferSize;
+ int state = this.state;
+ assert (state == ACTIVE || state == CANCELLED)
+ ? accumulatedBytes >= leftToFill : true;
+ List<ByteBuffer> dsts = new ArrayList<>();
+
+ ListIterator<ByteBuffer> itr = internalBuffers.listIterator();
+ while (itr.hasNext()) {
+ ByteBuffer b = itr.next();
+ if (b.remaining() <= leftToFill) {
+ itr.remove();
+ if (b.position() != 0)
+ b = b.slice(); // ensure position = 0 when propagated
+ dsts.add(b);
+ leftToFill -= b.remaining();
+ accumulatedBytes -= b.remaining();
+ if (leftToFill == 0)
+ break;
+ } else {
+ int prevLimit = b.limit();
+ b.limit(b.position() + leftToFill);
+ ByteBuffer slice = b.slice();
+ dsts.add(slice);
+ b.limit(prevLimit);
+ b.position(b.position() + leftToFill);
+ accumulatedBytes -= leftToFill;
+ leftToFill = 0;
+ break;
+ }
+ }
+ assert (state == ACTIVE || state == CANCELLED)
+ ? leftToFill == 0 : state == COMPLETE;
+ assert (state == ACTIVE || state == CANCELLED)
+ ? remaining(dsts) == bufferSize : state == COMPLETE;
+ assert accumulatedBytes >= 0;
+ assert dsts.stream().noneMatch(b -> b.position() != 0);
+ return Collections.unmodifiableList(dsts);
+ }
+
+ /** Subscription that is passed to the downstream subscriber. */
+ private class DownstreamSubscription implements Flow.Subscription {
+ private final AtomicBoolean cancelled = new AtomicBoolean(); // false
+ private final Demand demand = new Demand();
+ private volatile boolean illegalArg;
+
+ @Override
+ public void request(long n) {
+ if (cancelled.get() || illegalArg) {
+ return;
+ }
+ if (n <= 0L) {
+ // pass the "bad" value upstream so the Publisher can deal with
+ // it appropriately, i.e. invoke onError
+ illegalArg = true;
+ subscription.request(n);
+ return;
+ }
+
+ demand.increase(n);
+
+ pushDemanded();
+ }
+
+ private final SequentialScheduler pushDemandedScheduler =
+ new SequentialScheduler(new PushDemandedTask());
+
+ void pushDemanded() {
+ if (cancelled.get())
+ return;
+ pushDemandedScheduler.runOrSchedule();
+ }
+
+ class PushDemandedTask extends SequentialScheduler.CompleteRestartableTask {
+ @Override
+ public void run() {
+ try {
+ Throwable t = throwable;
+ if (t != null) {
+ pushDemandedScheduler.stop(); // stop the demand scheduler
+ downstreamSubscriber.onError(t);
+ return;
+ }
+
+ while (true) {
+ List<ByteBuffer> item;
+ synchronized (buffersLock) {
+ if (cancelled.get())
+ return;
+ if (!hasEnoughAccumulatedBytes())
+ break;
+ if (!demand.tryDecrement())
+ break;
+ item = fromInternalBuffers();
+ }
+ assert item != null;
+
+ downstreamSubscriber.onNext(item);
+ }
+ if (cancelled.get())
+ return;
+
+ // complete only if all data consumed
+ boolean complete;
+ synchronized (buffersLock) {
+ complete = state == COMPLETE && internalBuffers.isEmpty();
+ }
+ if (complete) {
+ assert internalBuffers.isEmpty();
+ pushDemandedScheduler.stop(); // stop the demand scheduler
+ downstreamSubscriber.onComplete();
+ return;
+ }
+ } catch (Throwable t) {
+ cancel(); // cancel if there is any error
+ throw t;
+ }
+
+ boolean requestMore = false;
+ synchronized (buffersLock) {
+ if (!hasEnoughAccumulatedBytes() && !demand.isFulfilled()) {
+ // request more upstream data
+ requestMore = true;
+ }
+ }
+ if (requestMore)
+ subscription.request(1);
+ }
+ }
+
+ @Override
+ public void cancel() {
+ if (cancelled.compareAndExchange(false, true))
+ return; // already cancelled
+
+ state = CANCELLED; // set CANCELLED state of upstream subscriber
+ subscription.cancel(); // cancel upstream subscription
+ pushDemandedScheduler.stop(); // stop the demand scheduler
+ }
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ Objects.requireNonNull(subscription);
+ if (this.subscription != null) {
+ subscription.cancel();
+ return;
+ }
+
+ int s = this.state;
+ assert s == UNSUBSCRIBED;
+ state = ACTIVE;
+ this.subscription = subscription;
+ downstreamSubscription = new DownstreamSubscription();
+ downstreamSubscriber.onSubscribe(downstreamSubscription);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ Objects.requireNonNull(item);
+
+ int s = state;
+ if (s == CANCELLED)
+ return;
+
+ if (s != ACTIVE)
+ throw new InternalError("onNext on inactive subscriber");
+
+ synchronized (buffersLock) {
+ internalBuffers.addAll(item);
+ accumulatedBytes += remaining(item);
+ }
+
+ downstreamSubscription.pushDemanded();
+ }
+
+ @Override
+ public void onError(Throwable incomingThrowable) {
+ Objects.requireNonNull(incomingThrowable);
+ int s = state;
+ assert s == ACTIVE : "Expected ACTIVE, got:" + s;
+ state = ERROR;
+ Throwable t = this.throwable;
+ assert t == null : "Expected null, got:" + t;
+ this.throwable = incomingThrowable;
+ downstreamSubscription.pushDemanded();
+ }
+
+ @Override
+ public void onComplete() {
+ int s = state;
+ assert s == ACTIVE : "Expected ACTIVE, got:" + s;
+ state = COMPLETE;
+ downstreamSubscription.pushDemanded();
+ }
+
+ @Override
+ public CompletionStage<T> getBody() {
+ return downstreamSubscriber.getBody();
+ }
+}
--- /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<CacheKey,LinkedList<HttpConnection>> plainPool;
+ private final HashMap<CacheKey,LinkedList<HttpConnection>> sslPool;
+ private final ExpiryList expiryList;
+ private final String dbgTag; // used for debug
+ boolean stopped;
+
+ /**
+ * Entries in connection pool are keyed by destination address and/or
+ * proxy address:
+ * case 1: plain TCP not via proxy (destination only)
+ * case 2: plain TCP via proxy (proxy only)
+ * case 3: SSL not via proxy (destination only)
+ * case 4: SSL over tunnel (destination and proxy)
+ */
+ static class CacheKey {
+ final InetSocketAddress proxy;
+ final InetSocketAddress destination;
+
+ CacheKey(InetSocketAddress destination, InetSocketAddress proxy) {
+ this.proxy = proxy;
+ this.destination = destination;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final CacheKey other = (CacheKey) obj;
+ if (!Objects.equals(this.proxy, other.proxy)) {
+ return false;
+ }
+ if (!Objects.equals(this.destination, other.destination)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(proxy, destination);
+ }
+ }
+
+ ConnectionPool(long clientId) {
+ this("ConnectionPool("+clientId+")");
+ }
+
+ /**
+ * There should be one of these per HttpClient.
+ */
+ private ConnectionPool(String tag) {
+ dbgTag = tag;
+ plainPool = new HashMap<>();
+ sslPool = new HashMap<>();
+ expiryList = new ExpiryList();
+ }
+
+ final String dbgString() {
+ return dbgTag;
+ }
+
+ synchronized void start() {
+ assert !stopped : "Already stopped";
+ }
+
+ static CacheKey cacheKey(InetSocketAddress destination,
+ InetSocketAddress proxy)
+ {
+ return new CacheKey(destination, proxy);
+ }
+
+ synchronized HttpConnection getConnection(boolean secure,
+ InetSocketAddress addr,
+ InetSocketAddress proxy) {
+ if (stopped) return null;
+ CacheKey key = new CacheKey(addr, proxy);
+ HttpConnection c = secure ? findConnection(key, sslPool)
+ : findConnection(key, plainPool);
+ //System.out.println ("getConnection returning: " + c);
+ return c;
+ }
+
+ /**
+ * Returns the connection to the pool.
+ */
+ void returnToPool(HttpConnection conn) {
+ returnToPool(conn, Instant.now(), KEEP_ALIVE);
+ }
+
+ // Called also by whitebox tests
+ void returnToPool(HttpConnection conn, Instant now, long keepAlive) {
+
+ // Don't call registerCleanupTrigger while holding a lock,
+ // but register it before the connection is added to the pool,
+ // since we don't want to trigger the cleanup if the connection
+ // is not in the pool.
+ CleanupTrigger cleanup = registerCleanupTrigger(conn);
+
+ // it's possible that cleanup may have been called.
+ synchronized(this) {
+ if (cleanup.isDone()) {
+ return;
+ } else if (stopped) {
+ conn.close();
+ return;
+ }
+ if (conn instanceof PlainHttpConnection) {
+ putConnection(conn, plainPool);
+ } else {
+ assert conn.isSecure();
+ putConnection(conn, sslPool);
+ }
+ expiryList.add(conn, now, keepAlive);
+ }
+ //System.out.println("Return to pool: " + conn);
+ }
+
+ private CleanupTrigger registerCleanupTrigger(HttpConnection conn) {
+ // Connect the connection flow to a pub/sub pair that will take the
+ // connection out of the pool and close it if anything happens
+ // while the connection is sitting in the pool.
+ CleanupTrigger cleanup = new CleanupTrigger(conn);
+ FlowTube flow = conn.getConnectionFlow();
+ debug.log(Level.DEBUG, "registering %s", cleanup);
+ flow.connectFlows(cleanup, cleanup);
+ return cleanup;
+ }
+
+ private HttpConnection
+ findConnection(CacheKey key,
+ HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
+ LinkedList<HttpConnection> l = pool.get(key);
+ if (l == null || l.isEmpty()) {
+ return null;
+ } else {
+ HttpConnection c = l.removeFirst();
+ expiryList.remove(c);
+ return c;
+ }
+ }
+
+ /* called from cache cleaner only */
+ private boolean
+ removeFromPool(HttpConnection c,
+ HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
+ //System.out.println("cacheCleaner removing: " + c);
+ assert Thread.holdsLock(this);
+ CacheKey k = c.cacheKey();
+ List<HttpConnection> l = pool.get(k);
+ if (l == null || l.isEmpty()) {
+ pool.remove(k);
+ return false;
+ }
+ return l.remove(c);
+ }
+
+ private void
+ putConnection(HttpConnection c,
+ HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
+ CacheKey key = c.cacheKey();
+ LinkedList<HttpConnection> l = pool.get(key);
+ if (l == null) {
+ l = new LinkedList<>();
+ pool.put(key, l);
+ }
+ l.add(c);
+ }
+
+ /**
+ * Purge expired connection and return the number of milliseconds
+ * in which the next connection is scheduled to expire.
+ * If no connections are scheduled to be purged return 0.
+ * @return the delay in milliseconds in which the next connection will
+ * expire.
+ */
+ long purgeExpiredConnectionsAndReturnNextDeadline() {
+ if (!expiryList.purgeMaybeRequired()) return 0;
+ return purgeExpiredConnectionsAndReturnNextDeadline(Instant.now());
+ }
+
+ // Used for whitebox testing
+ long purgeExpiredConnectionsAndReturnNextDeadline(Instant now) {
+ long nextPurge = 0;
+
+ // We may be in the process of adding new elements
+ // to the expiry list - but those elements will not
+ // have outlast their keep alive timer yet since we're
+ // just adding them.
+ if (!expiryList.purgeMaybeRequired()) return nextPurge;
+
+ List<HttpConnection> closelist;
+ synchronized (this) {
+ closelist = expiryList.purgeUntil(now);
+ for (HttpConnection c : closelist) {
+ if (c instanceof PlainHttpConnection) {
+ boolean wasPresent = removeFromPool(c, plainPool);
+ assert wasPresent;
+ } else {
+ boolean wasPresent = removeFromPool(c, sslPool);
+ assert wasPresent;
+ }
+ }
+ nextPurge = now.until(
+ expiryList.nextExpiryDeadline().orElse(now),
+ ChronoUnit.MILLIS);
+ }
+ closelist.forEach(this::close);
+ return nextPurge;
+ }
+
+ private void close(HttpConnection c) {
+ try {
+ c.close();
+ } catch (Throwable e) {} // ignore
+ }
+
+ void stop() {
+ List<HttpConnection> closelist = Collections.emptyList();
+ try {
+ synchronized (this) {
+ stopped = true;
+ closelist = expiryList.stream()
+ .map(e -> e.connection)
+ .collect(Collectors.toList());
+ expiryList.clear();
+ plainPool.clear();
+ sslPool.clear();
+ }
+ } finally {
+ closelist.forEach(this::close);
+ }
+ }
+
+ static final class ExpiryEntry {
+ final HttpConnection connection;
+ final Instant expiry; // absolute time in seconds of expiry time
+ ExpiryEntry(HttpConnection connection, Instant expiry) {
+ this.connection = connection;
+ this.expiry = expiry;
+ }
+ }
+
+ /**
+ * Manages a LinkedList of sorted ExpiryEntry. The entry with the closer
+ * deadline is at the tail of the list, and the entry with the farther
+ * deadline is at the head. In the most common situation, new elements
+ * will need to be added at the head (or close to it), and expired elements
+ * will need to be purged from the tail.
+ */
+ private static final class ExpiryList {
+ private final LinkedList<ExpiryEntry> list = new LinkedList<>();
+ private volatile boolean mayContainEntries;
+
+ // A loosely accurate boolean whose value is computed
+ // at the end of each operation performed on ExpiryList;
+ // Does not require synchronizing on the ConnectionPool.
+ boolean purgeMaybeRequired() {
+ return mayContainEntries;
+ }
+
+ // Returns the next expiry deadline
+ // should only be called while holding a synchronization
+ // lock on the ConnectionPool
+ Optional<Instant> nextExpiryDeadline() {
+ if (list.isEmpty()) return Optional.empty();
+ else return Optional.of(list.getLast().expiry);
+ }
+
+ // should only be called while holding a synchronization
+ // lock on the ConnectionPool
+ void add(HttpConnection conn) {
+ add(conn, Instant.now(), KEEP_ALIVE);
+ }
+
+ // Used by whitebox test.
+ void add(HttpConnection conn, Instant now, long keepAlive) {
+ Instant then = now.truncatedTo(ChronoUnit.SECONDS)
+ .plus(keepAlive, ChronoUnit.SECONDS);
+
+ // Elements with the farther deadline are at the head of
+ // the list. It's more likely that the new element will
+ // have the farthest deadline, and will need to be inserted
+ // at the head of the list, so we're using an ascending
+ // list iterator to find the right insertion point.
+ ListIterator<ExpiryEntry> li = list.listIterator();
+ while (li.hasNext()) {
+ ExpiryEntry entry = li.next();
+
+ if (then.isAfter(entry.expiry)) {
+ li.previous();
+ // insert here
+ li.add(new ExpiryEntry(conn, then));
+ mayContainEntries = true;
+ return;
+ }
+ }
+ // last (or first) element of list (the last element is
+ // the first when the list is empty)
+ list.add(new ExpiryEntry(conn, then));
+ mayContainEntries = true;
+ }
+
+ // should only be called while holding a synchronization
+ // lock on the ConnectionPool
+ void remove(HttpConnection c) {
+ if (c == null || list.isEmpty()) return;
+ ListIterator<ExpiryEntry> li = list.listIterator();
+ while (li.hasNext()) {
+ ExpiryEntry e = li.next();
+ if (e.connection.equals(c)) {
+ li.remove();
+ mayContainEntries = !list.isEmpty();
+ return;
+ }
+ }
+ }
+
+ // should only be called while holding a synchronization
+ // lock on the ConnectionPool.
+ // Purge all elements whose deadline is before now (now included).
+ List<HttpConnection> purgeUntil(Instant now) {
+ if (list.isEmpty()) return Collections.emptyList();
+
+ List<HttpConnection> closelist = new ArrayList<>();
+
+ // elements with the closest deadlines are at the tail
+ // of the queue, so we're going to use a descending iterator
+ // to remove them, and stop when we find the first element
+ // that has not expired yet.
+ Iterator<ExpiryEntry> li = list.descendingIterator();
+ while (li.hasNext()) {
+ ExpiryEntry entry = li.next();
+ // use !isAfter instead of isBefore in order to
+ // remove the entry if its expiry == now
+ if (!entry.expiry.isAfter(now)) {
+ li.remove();
+ HttpConnection c = entry.connection;
+ closelist.add(c);
+ } else break; // the list is sorted
+ }
+ mayContainEntries = !list.isEmpty();
+ return closelist;
+ }
+
+ // should only be called while holding a synchronization
+ // lock on the ConnectionPool
+ java.util.stream.Stream<ExpiryEntry> stream() {
+ return list.stream();
+ }
+
+ // should only be called while holding a synchronization
+ // lock on the ConnectionPool
+ void clear() {
+ list.clear();
+ mayContainEntries = false;
+ }
+ }
+
+ void cleanup(HttpConnection c, Throwable error) {
+ debug.log(Level.DEBUG,
+ "%s : ConnectionPool.cleanup(%s)",
+ String.valueOf(c.getConnectionFlow()),
+ error);
+ synchronized(this) {
+ if (c instanceof PlainHttpConnection) {
+ removeFromPool(c, plainPool);
+ } else {
+ assert c.isSecure();
+ removeFromPool(c, sslPool);
+ }
+ expiryList.remove(c);
+ }
+ c.close();
+ }
+
+ /**
+ * An object that subscribes to the flow while the connection is in
+ * the pool. Anything that comes in will cause the connection to be closed
+ * and removed from the pool.
+ */
+ private final class CleanupTrigger implements
+ FlowTube.TubeSubscriber, FlowTube.TubePublisher,
+ Flow.Subscription {
+
+ private final HttpConnection connection;
+ private volatile boolean done;
+
+ public CleanupTrigger(HttpConnection connection) {
+ this.connection = connection;
+ }
+
+ public boolean isDone() { return done;}
+
+ private void triggerCleanup(Throwable error) {
+ done = true;
+ cleanup(connection, error);
+ }
+
+ @Override public void request(long n) {}
+ @Override public void cancel() {}
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ subscription.request(1);
+ }
+ @Override
+ public void onError(Throwable error) { triggerCleanup(error); }
+ @Override
+ public void onComplete() { triggerCleanup(null); }
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ triggerCleanup(new IOException("Data received while in pool"));
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+ subscriber.onSubscribe(this);
+ }
+
+ @Override
+ public String toString() {
+ return "CleanupTrigger(" + connection.getConnectionFlow() + ")";
+ }
+
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<CookieHandler> cookieHandlerOpt = client.cookieHandler();
+ if (cookieHandlerOpt.isPresent()) {
+ CookieHandler cookieHandler = cookieHandlerOpt.get();
+ Map<String,List<String>> userheaders = r.getUserHeaders().map();
+ Map<String,List<String>> cookies = cookieHandler.get(r.uri(), userheaders);
+
+ // add the returned cookies
+ HttpHeadersImpl systemHeaders = r.getSystemHeaders();
+ if (cookies.isEmpty()) {
+ Log.logTrace("Request: no cookie to add for {0}",
+ r.uri());
+ } else {
+ Log.logTrace("Request: adding cookies for {0}",
+ r.uri());
+ }
+ for (String hdrname : cookies.keySet()) {
+ List<String> vals = cookies.get(hdrname);
+ for (String val : vals) {
+ systemHeaders.addHeader(hdrname, val);
+ }
+ }
+ } else {
+ Log.logTrace("Request: No cookie manager found for {0}",
+ r.uri());
+ }
+ }
+
+ @Override
+ public HttpRequestImpl response(Response r) throws IOException {
+ HttpHeaders hdrs = r.headers();
+ HttpRequestImpl request = r.request();
+ Exchange<?> e = r.exchange;
+ Log.logTrace("Response: processing cookies for {0}", request.uri());
+ Optional<CookieHandler> cookieHandlerOpt = e.client().cookieHandler();
+ if (cookieHandlerOpt.isPresent()) {
+ CookieHandler cookieHandler = cookieHandlerOpt.get();
+ Log.logTrace("Response: parsing cookies from {0}", hdrs.map());
+ cookieHandler.put(request.uri(), hdrs.map());
+ } else {
+ Log.logTrace("Response: No cookie manager found for {0}",
+ request.uri());
+ }
+ return null;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<T> {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+
+ final HttpRequestImpl request;
+ final HttpClientImpl client;
+ volatile ExchangeImpl<T> exchImpl;
+ volatile CompletableFuture<? extends ExchangeImpl<T>> exchangeCF;
+ volatile CompletableFuture<Void> bodyIgnored;
+
+ // used to record possible cancellation raised before the exchImpl
+ // has been established.
+ private volatile IOException failed;
+ final AccessControlContext acc;
+ final MultiExchange<T> multi;
+ final Executor parentExecutor;
+ boolean upgrading; // to HTTP/2
+ final PushGroup<T> pushGroup;
+ final String dbgTag;
+
+ Exchange(HttpRequestImpl request, MultiExchange<T> multi) {
+ this.request = request;
+ this.upgrading = false;
+ this.client = multi.client();
+ this.multi = multi;
+ this.acc = multi.acc;
+ this.parentExecutor = multi.executor;
+ this.pushGroup = multi.pushGroup;
+ this.dbgTag = "Exchange";
+ }
+
+ /* If different AccessControlContext to be used */
+ Exchange(HttpRequestImpl request,
+ MultiExchange<T> multi,
+ AccessControlContext acc)
+ {
+ this.request = request;
+ this.acc = acc;
+ this.upgrading = false;
+ this.client = multi.client();
+ this.multi = multi;
+ this.parentExecutor = multi.executor;
+ this.pushGroup = multi.pushGroup;
+ this.dbgTag = "Exchange";
+ }
+
+ PushGroup<T> getPushGroup() {
+ return pushGroup;
+ }
+
+ Executor executor() {
+ return parentExecutor;
+ }
+
+ public HttpRequestImpl request() {
+ return request;
+ }
+
+ HttpClientImpl client() {
+ return client;
+ }
+
+
+ public CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler) {
+ // If we received a 407 while establishing the exchange
+ // there will be no body to read: bodyIgnored will be true,
+ // and exchImpl will be null (if we were trying to establish
+ // an HTTP/2 tunnel through an HTTP/1.1 proxy)
+ if (bodyIgnored != null) return MinimalFuture.completedFuture(null);
+
+ // The connection will not be returned to the pool in the case of WebSocket
+ return exchImpl.readBodyAsync(handler, !request.isWebSocket(), parentExecutor)
+ .whenComplete((r,t) -> exchImpl.completed());
+ }
+
+ /**
+ * Called after a redirect or similar kind of retry where a body might
+ * be sent but we don't want it. Should send a RESET in h2. For http/1.1
+ * we can consume small quantity of data, or close the connection in
+ * other cases.
+ */
+ public CompletableFuture<Void> ignoreBody() {
+ if (bodyIgnored != null) return bodyIgnored;
+ return exchImpl.ignoreBody();
+ }
+
+ /**
+ * Called when a new exchange is created to replace this exchange.
+ * At this point it is guaranteed that readBody/readBodyAsync will
+ * not be called.
+ */
+ public void released() {
+ ExchangeImpl<?> impl = exchImpl;
+ if (impl != null) impl.released();
+ // Don't set exchImpl to null here. We need to keep
+ // it alive until it's replaced by a Stream in wrapForUpgrade.
+ // Setting it to null here might get it GC'ed too early, because
+ // the Http1Response is now only weakly referenced by the Selector.
+ }
+
+ public void cancel() {
+ // cancel can be called concurrently before or at the same time
+ // that the exchange impl is being established.
+ // In that case we won't be able to propagate the cancellation
+ // right away
+ if (exchImpl != null) {
+ exchImpl.cancel();
+ } else {
+ // no impl - can't cancel impl yet.
+ // call cancel(IOException) instead which takes care
+ // of race conditions between impl/cancel.
+ cancel(new IOException("Request cancelled"));
+ }
+ }
+
+ public void cancel(IOException cause) {
+ // If the impl is non null, propagate the exception right away.
+ // Otherwise record it so that it can be propagated once the
+ // exchange impl has been established.
+ ExchangeImpl<?> impl = exchImpl;
+ if (impl != null) {
+ // propagate the exception to the impl
+ debug.log(Level.DEBUG, "Cancelling exchImpl: %s", exchImpl);
+ impl.cancel(cause);
+ } else {
+ // no impl yet. record the exception
+ failed = cause;
+ // now call checkCancelled to recheck the impl.
+ // if the failed state is set and the impl is not null, reset
+ // the failed state and propagate the exception to the impl.
+ checkCancelled();
+ }
+ }
+
+ // This method will raise an exception if one was reported and if
+ // it is possible to do so. If the exception can be raised, then
+ // the failed state will be reset. Otherwise, the failed state
+ // will persist until the exception can be raised and the failed state
+ // can be cleared.
+ // Takes care of possible race conditions.
+ private void checkCancelled() {
+ ExchangeImpl<?> impl = null;
+ IOException cause = null;
+ CompletableFuture<? extends ExchangeImpl<T>> cf = null;
+ if (failed != null) {
+ synchronized(this) {
+ cause = failed;
+ impl = exchImpl;
+ cf = exchangeCF;
+ }
+ }
+ if (cause == null) return;
+ if (impl != null) {
+ // The exception is raised by propagating it to the impl.
+ debug.log(Level.DEBUG, "Cancelling exchImpl: %s", impl);
+ impl.cancel(cause);
+ failed = null;
+ } else {
+ Log.logTrace("Exchange: request [{0}/timeout={1}ms] no impl is set."
+ + "\n\tCan''t cancel yet with {2}",
+ request.uri(),
+ request.timeout().isPresent() ?
+ // calling duration.toMillis() can throw an exception.
+ // this is just debugging, we don't care if it overflows.
+ (request.timeout().get().getSeconds() * 1000
+ + request.timeout().get().getNano() / 1000000) : -1,
+ cause);
+ if (cf != null) cf.completeExceptionally(cause);
+ }
+ }
+
+ public void h2Upgrade() {
+ upgrading = true;
+ request.setH2Upgrade(client.client2());
+ }
+
+ synchronized IOException getCancelCause() {
+ return failed;
+ }
+
+ // get/set the exchange impl, solving race condition issues with
+ // potential concurrent calls to cancel() or cancel(IOException)
+ private CompletableFuture<? extends ExchangeImpl<T>>
+ establishExchange(HttpConnection connection) {
+ if (debug.isLoggable(Level.DEBUG)) {
+ debug.log(Level.DEBUG,
+ "establishing exchange for %s,%n\t proxy=%s",
+ request,
+ request.proxy());
+ }
+ // check if we have been cancelled first.
+ Throwable t = getCancelCause();
+ checkCancelled();
+ if (t != null) {
+ return MinimalFuture.failedFuture(t);
+ }
+
+ CompletableFuture<? extends ExchangeImpl<T>> cf, res;
+ cf = ExchangeImpl.get(this, connection);
+ // We should probably use a VarHandle to get/set exchangeCF
+ // instead - as we need CAS semantics.
+ synchronized (this) { exchangeCF = cf; };
+ res = cf.whenComplete((r,x) -> {
+ synchronized(Exchange.this) {
+ if (exchangeCF == cf) exchangeCF = null;
+ }
+ });
+ checkCancelled();
+ return res.thenCompose((eimpl) -> {
+ // recheck for cancelled, in case of race conditions
+ exchImpl = eimpl;
+ IOException tt = getCancelCause();
+ checkCancelled();
+ if (tt != null) {
+ return MinimalFuture.failedFuture(tt);
+ } else {
+ // Now we're good to go. Because exchImpl is no longer
+ // null cancel() will be able to propagate directly to
+ // the impl after this point ( if needed ).
+ return MinimalFuture.completedFuture(eimpl);
+ } });
+ }
+
+ // Completed HttpResponse will be null if response succeeded
+ // will be a non null responseAsync if expect continue returns an error
+
+ public CompletableFuture<Response> responseAsync() {
+ return responseAsyncImpl(null);
+ }
+
+ CompletableFuture<Response> responseAsyncImpl(HttpConnection connection) {
+ SecurityException e = checkPermissions();
+ if (e != null) {
+ return MinimalFuture.failedFuture(e);
+ } else {
+ return responseAsyncImpl0(connection);
+ }
+ }
+
+ // check whether the headersSentCF was completed exceptionally with
+ // ProxyAuthorizationRequired. If so the Response embedded in the
+ // exception is returned. Otherwise we proceed.
+ private CompletableFuture<Response> checkFor407(ExchangeImpl<T> ex, Throwable t,
+ Function<ExchangeImpl<T>,CompletableFuture<Response>> andThen) {
+ t = Utils.getCompletionCause(t);
+ if (t instanceof ProxyAuthenticationRequired) {
+ bodyIgnored = MinimalFuture.completedFuture(null);
+ Response proxyResponse = ((ProxyAuthenticationRequired)t).proxyResponse;
+ Response syntheticResponse = new Response(request, this,
+ proxyResponse.headers, proxyResponse.statusCode,
+ proxyResponse.version, true);
+ return MinimalFuture.completedFuture(syntheticResponse);
+ } else if (t != null) {
+ return MinimalFuture.failedFuture(t);
+ } else {
+ return andThen.apply(ex);
+ }
+ }
+
+ // After sending the request headers, if no ProxyAuthorizationRequired
+ // was raised and the expectContinue flag is on, we need to wait
+ // for the 100-Continue response
+ private CompletableFuture<Response> expectContinue(ExchangeImpl<T> ex) {
+ assert request.expectContinue();
+ return ex.getResponseAsync(parentExecutor)
+ .thenCompose((Response r1) -> {
+ Log.logResponse(r1::toString);
+ int rcode = r1.statusCode();
+ if (rcode == 100) {
+ Log.logTrace("Received 100-Continue: sending body");
+ CompletableFuture<Response> cf =
+ exchImpl.sendBodyAsync()
+ .thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
+ cf = wrapForUpgrade(cf);
+ cf = wrapForLog(cf);
+ return cf;
+ } else {
+ Log.logTrace("Expectation failed: Received {0}",
+ rcode);
+ if (upgrading && rcode == 101) {
+ IOException failed = new IOException(
+ "Unable to handle 101 while waiting for 100");
+ return MinimalFuture.failedFuture(failed);
+ }
+ return exchImpl.readBodyAsync(this::ignoreBody, false, parentExecutor)
+ .thenApply(v -> r1);
+ }
+ });
+ }
+
+ // After sending the request headers, if no ProxyAuthorizationRequired
+ // was raised and the expectContinue flag is off, we can immediately
+ // send the request body and proceed.
+ private CompletableFuture<Response> sendRequestBody(ExchangeImpl<T> ex) {
+ assert !request.expectContinue();
+ CompletableFuture<Response> cf = ex.sendBodyAsync()
+ .thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
+ cf = wrapForUpgrade(cf);
+ cf = wrapForLog(cf);
+ return cf;
+ }
+
+ CompletableFuture<Response> responseAsyncImpl0(HttpConnection connection) {
+ Function<ExchangeImpl<T>, CompletableFuture<Response>> after407Check;
+ bodyIgnored = null;
+ if (request.expectContinue()) {
+ request.addSystemHeader("Expect", "100-Continue");
+ Log.logTrace("Sending Expect: 100-Continue");
+ // wait for 100-Continue before sending body
+ after407Check = this::expectContinue;
+ } else {
+ // send request body and proceed.
+ after407Check = this::sendRequestBody;
+ }
+ // The ProxyAuthorizationRequired can be triggered either by
+ // establishExchange (case of HTTP/2 SSL tunelling through HTTP/1.1 proxy
+ // or by sendHeaderAsync (case of HTTP/1.1 SSL tunelling through HTTP/1.1 proxy
+ // Therefore we handle it with a call to this checkFor407(...) after these
+ // two places.
+ Function<ExchangeImpl<T>, CompletableFuture<Response>> afterExch407Check =
+ (ex) -> ex.sendHeadersAsync()
+ .handle((r,t) -> this.checkFor407(r, t, after407Check))
+ .thenCompose(Function.identity());
+ return establishExchange(connection)
+ .handle((r,t) -> this.checkFor407(r,t, afterExch407Check))
+ .thenCompose(Function.identity());
+ }
+
+ private CompletableFuture<Response> wrapForUpgrade(CompletableFuture<Response> cf) {
+ if (upgrading) {
+ return cf.thenCompose(r -> checkForUpgradeAsync(r, exchImpl));
+ }
+ return cf;
+ }
+
+ private CompletableFuture<Response> wrapForLog(CompletableFuture<Response> cf) {
+ if (Log.requests()) {
+ return cf.thenApply(response -> {
+ Log.logResponse(response::toString);
+ return response;
+ });
+ }
+ return cf;
+ }
+
+ HttpResponse.BodySubscriber<T> ignoreBody(int status, HttpHeaders hdrs) {
+ return HttpResponse.BodySubscriber.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<Response>
+ checkForUpgradeAsync(Response resp,
+ ExchangeImpl<T> ex) {
+
+ int rcode = resp.statusCode();
+ if (upgrading && (rcode == 101)) {
+ Http1Exchange<T> e = (Http1Exchange<T>)ex;
+ // check for 101 switching protocols
+ // 101 responses are not supposed to contain a body.
+ // => should we fail if there is one?
+ debug.log(Level.DEBUG, "Upgrading async %s", e.connection());
+ return e.readBodyAsync(this::ignoreBody, false, parentExecutor)
+ .thenCompose((T v) -> {// v is null
+ debug.log(Level.DEBUG, "Ignored body");
+ // we pass e::getBuffer to allow the ByteBuffers to accumulate
+ // while we build the Http2Connection
+ return Http2Connection.createAsync(e.connection(),
+ client.client2(),
+ this, e::drainLeftOverBytes)
+ .thenCompose((Http2Connection c) -> {
+ boolean cached = c.offerConnection();
+ Stream<T> s = c.getStream(1);
+
+ if (s == null) {
+ // s can be null if an exception occurred
+ // asynchronously while sending the preface.
+ Throwable t = c.getRecordedCause();
+ IOException ioe;
+ if (t != null) {
+ if (!cached)
+ c.close();
+ ioe = new IOException("Can't get stream 1: " + t, t);
+ } else {
+ ioe = new IOException("Can't get stream 1");
+ }
+ return MinimalFuture.failedFuture(ioe);
+ }
+ exchImpl.released();
+ Throwable t;
+ // There's a race condition window where an external
+ // thread (SelectorManager) might complete the
+ // exchange in timeout at the same time where we're
+ // trying to switch the exchange impl.
+ // 'failed' will be reset to null after
+ // exchImpl.cancel() has completed, so either we
+ // will observe failed != null here, or we will
+ // observe e.getCancelCause() != null, or the
+ // timeout exception will be routed to 's'.
+ // Either way, we need to relay it to s.
+ synchronized (this) {
+ exchImpl = s;
+ t = failed;
+ }
+ // Check whether the HTTP/1.1 was cancelled.
+ if (t == null) t = e.getCancelCause();
+ // if HTTP/1.1 exchange was timed out, don't
+ // try to go further.
+ if (t instanceof HttpTimeoutException) {
+ s.cancelImpl(t);
+ return MinimalFuture.failedFuture(t);
+ }
+ debug.log(Level.DEBUG, "Getting response async %s", s);
+ return s.getResponseAsync(null);
+ });}
+ );
+ }
+ return MinimalFuture.completedFuture(resp);
+ }
+
+ private URI getURIForSecurityCheck() {
+ URI u;
+ String method = request.method();
+ InetSocketAddress authority = request.authority();
+ URI uri = request.uri();
+
+ // CONNECT should be restricted at API level
+ if (method.equalsIgnoreCase("CONNECT")) {
+ try {
+ u = new URI("socket",
+ null,
+ authority.getHostString(),
+ authority.getPort(),
+ null,
+ null,
+ null);
+ } catch (URISyntaxException e) {
+ throw new InternalError(e); // shouldn't happen
+ }
+ } else {
+ u = uri;
+ }
+ return u;
+ }
+
+ /**
+ * Returns the security permission required for the given details.
+ * If method is CONNECT, then uri must be of form "scheme://host:port"
+ */
+ private static URLPermission permissionForServer(URI uri,
+ String method,
+ Map<String, List<String>> headers) {
+ if (method.equals("CONNECT")) {
+ return new URLPermission(uri.toString(), "CONNECT");
+ } else {
+ return Utils.permissionForServer(uri, method, headers.keySet().stream());
+ }
+ }
+
+ /**
+ * Performs the necessary security permission checks required to retrieve
+ * the response. Returns a security exception representing the denied
+ * permission, or null if all checks pass or there is no security manager.
+ */
+ private SecurityException checkPermissions() {
+ String method = request.method();
+ SecurityManager sm = System.getSecurityManager();
+ if (sm == null || method.equals("CONNECT")) {
+ // tunneling will have a null acc, which is fine. The proxy
+ // permission check will have already been preformed.
+ return null;
+ }
+
+ HttpHeaders userHeaders = request.getUserHeaders();
+ URI u = getURIForSecurityCheck();
+ URLPermission p = permissionForServer(u, method, userHeaders.map());
+
+ try {
+ assert acc != null;
+ sm.checkPermission(p, acc);
+ } catch (SecurityException e) {
+ return e;
+ }
+ ProxySelector ps = client.proxySelector();
+ if (ps != null) {
+ if (!method.equals("CONNECT")) {
+ // a non-tunneling HTTP proxy. Need to check access
+ URLPermission proxyPerm = permissionForProxy(request.proxy());
+ if (proxyPerm != null) {
+ try {
+ sm.checkPermission(proxyPerm, acc);
+ } catch (SecurityException e) {
+ return e;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ HttpClient.Version version() {
+ return multi.version();
+ }
+
+ String dbgString() {
+ return dbgTag;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<T> {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ private static final System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("ExchangeImpl"::toString, DEBUG);
+
+ final Exchange<T> exchange;
+
+ ExchangeImpl(Exchange<T> e) {
+ // e == null means a http/2 pushed stream
+ this.exchange = e;
+ }
+
+ final Exchange<T> getExchange() {
+ return exchange;
+ }
+
+
+ /**
+ * Returns the {@link HttpConnection} instance to which this exchange is
+ * assigned.
+ */
+ abstract HttpConnection connection();
+
+ /**
+ * Initiates a new exchange and assigns it to a connection if one exists
+ * already. connection usually null.
+ */
+ static <U> CompletableFuture<? extends ExchangeImpl<U>>
+ get(Exchange<U> exchange, HttpConnection connection)
+ {
+ if (exchange.version() == HTTP_1_1) {
+ DEBUG_LOGGER.log(Level.DEBUG, "get: HTTP/1.1: new Http1Exchange");
+ return createHttp1Exchange(exchange, connection);
+ } else {
+ Http2ClientImpl c2 = exchange.client().client2(); // TODO: improve
+ HttpRequestImpl request = exchange.request();
+ CompletableFuture<Http2Connection> c2f = c2.getConnectionFor(request);
+ DEBUG_LOGGER.log(Level.DEBUG, "get: Trying to get HTTP/2 connection");
+ return c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection))
+ .thenCompose(Function.identity());
+ }
+ }
+
+ private static <U> CompletableFuture<? extends ExchangeImpl<U>>
+ createExchangeImpl(Http2Connection c,
+ Throwable t,
+ Exchange<U> exchange,
+ HttpConnection connection)
+ {
+ DEBUG_LOGGER.log(Level.DEBUG, "handling HTTP/2 connection creation result");
+ boolean secure = exchange.request().secure();
+ if (t != null) {
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "handling HTTP/2 connection creation failed: %s",
+ (Object)t);
+ t = Utils.getCompletionCause(t);
+ if (t instanceof Http2Connection.ALPNException) {
+ Http2Connection.ALPNException ee = (Http2Connection.ALPNException)t;
+ AbstractAsyncSSLConnection as = ee.getConnection();
+ DEBUG_LOGGER.log(Level.DEBUG, "downgrading to HTTP/1.1 with: %s", as);
+ CompletableFuture<? extends ExchangeImpl<U>> ex =
+ createHttp1Exchange(exchange, as);
+ return ex;
+ } else {
+ DEBUG_LOGGER.log(Level.DEBUG, "HTTP/2 connection creation failed "
+ + "with unexpected exception: %s", (Object)t);
+ return MinimalFuture.failedFuture(t);
+ }
+ }
+ if (secure && c== null) {
+ DEBUG_LOGGER.log(Level.DEBUG, "downgrading to HTTP/1.1 ");
+ CompletableFuture<? extends ExchangeImpl<U>> ex =
+ createHttp1Exchange(exchange, null);
+ return ex;
+ }
+ if (c == null) {
+ // no existing connection. Send request with HTTP 1 and then
+ // upgrade if successful
+ DEBUG_LOGGER.log(Level.DEBUG, "new Http1Exchange, try to upgrade");
+ return createHttp1Exchange(exchange, connection)
+ .thenApply((e) -> {
+ exchange.h2Upgrade();
+ return e;
+ });
+ } else {
+ DEBUG_LOGGER.log(Level.DEBUG, "creating HTTP/2 streams");
+ Stream<U> s = c.createStream(exchange);
+ CompletableFuture<? extends ExchangeImpl<U>> ex = MinimalFuture.completedFuture(s);
+ return ex;
+ }
+ }
+
+ private static <T> CompletableFuture<Http1Exchange<T>>
+ createHttp1Exchange(Exchange<T> ex, HttpConnection as)
+ {
+ try {
+ return MinimalFuture.completedFuture(new Http1Exchange<>(ex, as));
+ } catch (Throwable e) {
+ return MinimalFuture.failedFuture(e);
+ }
+ }
+
+ /* The following methods have separate HTTP/1.1 and HTTP/2 implementations */
+
+ abstract CompletableFuture<ExchangeImpl<T>> sendHeadersAsync();
+
+ /** Sends a request body, after request headers have been sent. */
+ abstract CompletableFuture<ExchangeImpl<T>> sendBodyAsync();
+
+ abstract CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
+ boolean returnConnectionToPool,
+ Executor executor);
+
+ /**
+ * Ignore/consume the body.
+ */
+ abstract CompletableFuture<Void> ignoreBody();
+
+ /** Gets the response headers. Completes before body is read. */
+ abstract CompletableFuture<Response> getResponseAsync(Executor executor);
+
+
+ /** Cancels a request. Not currently exposed through API. */
+ abstract void cancel();
+
+ /**
+ * Cancels a request with a cause. Not currently exposed through API.
+ */
+ abstract void cancel(IOException cause);
+
+ /**
+ * Called when the exchange is released, so that cleanup actions may be
+ * performed - such as deregistering callbacks.
+ * Typically released is called during upgrade, when an HTTP/2 stream
+ * takes over from an Http1Exchange, or when a new exchange is created
+ * during a multi exchange before the final response body was received.
+ */
+ abstract void released();
+
+ /**
+ * Called when the exchange is completed, so that cleanup actions may be
+ * performed - such as deregistering callbacks.
+ * Typically, completed is called at the end of the exchange, when the
+ * final response body has been received (or an error has caused the
+ * completion of the exchange).
+ */
+ abstract void completed();
+
+ /**
+ * Returns true if this exchange was canceled.
+ * @return true if this exchange was canceled.
+ */
+ abstract boolean isCanceled();
+
+ /**
+ * Returns the cause for which this exchange was canceled, if available.
+ * @return the cause for which this exchange was canceled, if available.
+ */
+ abstract Throwable getCancelCause();
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<Class<? extends HeaderFilter>> filterClasses = new LinkedList<>();
+
+ public void addFilter(Class<? extends HeaderFilter> type) {
+ filterClasses.add(type);
+ }
+
+ List<HeaderFilter> getFilterChain() {
+ List<HeaderFilter> l = new LinkedList<>();
+ for (Class<? extends HeaderFilter> clazz : filterClasses) {
+ try {
+ // Requires a public no arg constructor.
+ HeaderFilter headerFilter = clazz.getConstructor().newInstance();
+ l.add(headerFilter);
+ } catch (ReflectiveOperationException e) {
+ throw new InternalError(e);
+ }
+ }
+ return l;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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;
+}
--- /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<String> {
+ int index;
+ boolean returnsValue; // or key
+
+ ParserIterator (boolean returnValue) {
+ returnsValue = returnValue;
+ }
+ @Override
+ public boolean hasNext () {
+ return index<nkeys;
+ }
+ @Override
+ public String next () {
+ if (index >= nkeys) {
+ throw new NoSuchElementException();
+ }
+ return tab[index++][returnsValue?1:0];
+ }
+ }
+
+ public Iterator<String> keys () {
+ return new ParserIterator (false);
+ }
+
+// public Iterator<String> values () {
+// return new ParserIterator (true);
+// }
+
+ @Override
+ public String toString () {
+ Iterator<String> k = keys();
+ StringBuilder sb = new StringBuilder();
+ sb.append("{size=").append(asize).append(" nkeys=").append(nkeys)
+ .append(' ');
+ for (int i=0; k.hasNext(); i++) {
+ String key = k.next();
+ String val = findValue (i);
+ if (val != null && "".equals (val)) {
+ val = null;
+ }
+ sb.append(" {").append(key).append(val == null ? "" : "," + val)
+ .append('}');
+ if (k.hasNext()) {
+ sb.append (',');
+ }
+ }
+ sb.append (" }");
+ return sb.toString();
+ }
+
+// public int findInt(String k, int Default) {
+// try {
+// return Integer.parseInt(findValue(k, String.valueOf(Default)));
+// } catch (Throwable t) {
+// return Default;
+// }
+// }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<ByteBuffer> queue
+ = new ConcurrentLinkedDeque<>();
+ private final SequentialScheduler scheduler =
+ SequentialScheduler.synchronizedScheduler(this::flush);
+ private final Executor executor;
+ private final Http1TubeSubscriber subscriber = new Http1TubeSubscriber();
+ private final AtomicReference<Http1AsyncDelegate> pendingDelegateRef;
+ private final AtomicLong received = new AtomicLong();
+ final AtomicBoolean canRequestMore = new AtomicBoolean();
+
+ private volatile Throwable error;
+ private volatile Http1AsyncDelegate delegate;
+ // This reference is only used to prevent early GC of the exchange.
+ private volatile Http1Exchange<?> owner;
+ // Only used for checking whether we run on the selector manager thread.
+ private final HttpClientImpl client;
+ private boolean retry;
+
+ public Http1AsyncReceiver(Executor executor, Http1Exchange<?> owner) {
+ this.pendingDelegateRef = new AtomicReference<>();
+ this.executor = executor;
+ this.owner = owner;
+ this.client = owner.client;
+ }
+
+ // This is the main loop called by the SequentialScheduler.
+ // It attempts to empty the queue until the scheduler is stopped,
+ // or the delegate is unregistered, or the delegate is unable to
+ // process the data (because it's not ready or already done), which
+ // it signals by returning 'true';
+ private void flush() {
+ ByteBuffer buf;
+ try {
+ assert !client.isSelectorThread() :
+ "Http1AsyncReceiver::flush should not run in the selector: "
+ + Thread.currentThread().getName();
+
+ // First check whether we have a pending delegate that has
+ // just subscribed, and if so, create a Subscription for it
+ // and call onSubscribe.
+ handlePendingDelegate();
+
+ // Then start emptying the queue, if possible.
+ while ((buf = queue.peek()) != null) {
+ Http1AsyncDelegate delegate = this.delegate;
+ debug.log(Level.DEBUG, "Got %s bytes for delegate %s",
+ buf.remaining(), delegate);
+ if (!hasDemand(delegate)) {
+ // The scheduler will be invoked again later when the demand
+ // becomes positive.
+ return;
+ }
+
+ assert delegate != null;
+ debug.log(Level.DEBUG, "Forwarding %s bytes to delegate %s",
+ buf.remaining(), delegate);
+ // The delegate has demand: feed it the next buffer.
+ if (!delegate.tryAsyncReceive(buf)) {
+ final long remaining = buf.remaining();
+ debug.log(Level.DEBUG, () -> {
+ // If the scheduler is stopped, the queue may already
+ // be empty and the reference may already be released.
+ String remstr = scheduler.isStopped() ? "" :
+ " remaining in ref: "
+ + remaining;
+ remstr = remstr
+ + " total remaining: " + remaining();
+ return "Delegate done: " + remaining;
+ });
+ canRequestMore.set(false);
+ // The last buffer parsed may have remaining unparsed bytes.
+ // Don't take it out of the queue.
+ return; // done.
+ }
+
+ // removed parsed buffer from queue, and continue with next
+ // if available
+ ByteBuffer parsed = queue.remove();
+ canRequestMore.set(queue.isEmpty());
+ assert parsed == buf;
+ }
+
+ // queue is empty: let's see if we should request more
+ checkRequestMore();
+
+ } catch (Throwable t) {
+ Throwable x = error;
+ if (x == null) error = t; // will be handled in the finally block
+ debug.log(Level.DEBUG, "Unexpected error caught in flush()", t);
+ } finally {
+ // Handles any pending error.
+ // The most recently subscribed delegate will get the error.
+ checkForErrors();
+ }
+ }
+
+ /**
+ * Must be called from within the scheduler main loop.
+ * Handles any pending errors by calling delegate.onReadError().
+ * If the error can be forwarded to the delegate, stops the scheduler.
+ */
+ private void checkForErrors() {
+ // Handles any pending error.
+ // The most recently subscribed delegate will get the error.
+ // If the delegate is null, the error will be handled by the next
+ // delegate that subscribes.
+ // If the queue is not empty, wait until it it is empty before
+ // handling the error.
+ Http1AsyncDelegate delegate = pendingDelegateRef.get();
+ if (delegate == null) delegate = this.delegate;
+ Throwable x = error;
+ if (delegate != null && x != null && queue.isEmpty()) {
+ // forward error only after emptying the queue.
+ final Object captured = delegate;
+ debug.log(Level.DEBUG, () -> "flushing " + x
+ + "\n\t delegate: " + captured
+ + "\t\t queue.isEmpty: " + queue.isEmpty());
+ scheduler.stop();
+ delegate.onReadError(x);
+ }
+ }
+
+ /**
+ * Must be called from within the scheduler main loop.
+ * Figure out whether more data should be requested from the
+ * Http1TubeSubscriber.
+ */
+ private void checkRequestMore() {
+ Http1AsyncDelegate delegate = this.delegate;
+ boolean more = this.canRequestMore.get();
+ boolean hasDemand = hasDemand(delegate);
+ debug.log(Level.DEBUG, () -> "checkRequestMore: "
+ + "canRequestMore=" + more + ", hasDemand=" + hasDemand
+ + (delegate == null ? ", delegate=null" : ""));
+ if (hasDemand) {
+ subscriber.requestMore();
+ }
+ }
+
+ /**
+ * Must be called from within the scheduler main loop.
+ * Return true if the delegate is not null and has some demand.
+ * @param delegate The Http1AsyncDelegate delegate
+ * @return true if the delegate is not null and has some demand
+ */
+ private boolean hasDemand(Http1AsyncDelegate delegate) {
+ if (delegate == null) return false;
+ AbstractSubscription subscription = delegate.subscription();
+ long demand = subscription.demand().get();
+ debug.log(Level.DEBUG, "downstream subscription demand is %s", demand);
+ return demand > 0;
+ }
+
+ /**
+ * Must be called from within the scheduler main loop.
+ * Handles pending delegate subscription.
+ * Return true if there was some pending delegate subscription and a new
+ * delegate was subscribed, false otherwise.
+ *
+ * @return true if there was some pending delegate subscription and a new
+ * delegate was subscribed, false otherwise.
+ */
+ private boolean handlePendingDelegate() {
+ Http1AsyncDelegate pending = pendingDelegateRef.get();
+ if (pending != null && pendingDelegateRef.compareAndSet(pending, null)) {
+ Http1AsyncDelegate delegate = this.delegate;
+ if (delegate != null) unsubscribe(delegate);
+ Runnable cancel = () -> {
+ debug.log(Level.DEBUG, "Downstream subscription cancelled by %s", pending);
+ // The connection should be closed, as some data may
+ // be left over in the stream.
+ try {
+ setRetryOnError(false);
+ onReadError(new IOException("subscription cancelled"));
+ unsubscribe(pending);
+ } finally {
+ Http1Exchange<?> exchg = owner;
+ stop();
+ if (exchg != null) exchg.connection().close();
+ }
+ };
+ // The subscription created by a delegate is only loosely
+ // coupled with the upstream subscription. This is partly because
+ // the header/body parser work with a flow of ByteBuffer, whereas
+ // we have a flow List<ByteBuffer> upstream.
+ Http1AsyncDelegateSubscription subscription =
+ new Http1AsyncDelegateSubscription(scheduler, cancel);
+ pending.onSubscribe(subscription);
+ this.delegate = delegate = pending;
+ final Object captured = delegate;
+ debug.log(Level.DEBUG, () -> "delegate is now " + captured
+ + ", demand=" + subscription.demand().get()
+ + ", canRequestMore=" + canRequestMore.get()
+ + ", queue.isEmpty=" + queue.isEmpty());
+ return true;
+ }
+ return false;
+ }
+
+ synchronized void setRetryOnError(boolean retry) {
+ this.retry = retry;
+ }
+
+ void clear() {
+ debug.log(Level.DEBUG, "cleared");
+ this.pendingDelegateRef.set(null);
+ this.delegate = null;
+ this.owner = null;
+ }
+
+ void subscribe(Http1AsyncDelegate delegate) {
+ synchronized(this) {
+ pendingDelegateRef.set(delegate);
+ }
+ if (queue.isEmpty()) {
+ canRequestMore.set(true);
+ }
+ debug.log(Level.DEBUG, () ->
+ "Subscribed pending " + delegate + " queue.isEmpty: "
+ + queue.isEmpty());
+ // Everything may have been received already. Make sure
+ // we parse it.
+ if (client.isSelectorThread()) {
+ scheduler.runOrSchedule(executor);
+ } else {
+ scheduler.runOrSchedule();
+ }
+ }
+
+ // Used for debugging only!
+ long remaining() {
+ return Utils.remaining(queue.toArray(Utils.EMPTY_BB_ARRAY));
+ }
+
+ void unsubscribe(Http1AsyncDelegate delegate) {
+ synchronized(this) {
+ if (this.delegate == delegate) {
+ debug.log(Level.DEBUG, "Unsubscribed %s", delegate);
+ this.delegate = null;
+ }
+ }
+ }
+
+ // Callback: Consumer of ByteBuffer
+ private void asyncReceive(ByteBuffer buf) {
+ debug.log(Level.DEBUG, "Putting %s bytes into the queue", buf.remaining());
+ received.addAndGet(buf.remaining());
+ queue.offer(buf);
+
+ // This callback is called from within the selector thread.
+ // Use an executor here to avoid doing the heavy lifting in the
+ // selector.
+ scheduler.runOrSchedule(executor);
+ }
+
+ // Callback: Consumer of Throwable
+ void onReadError(Throwable ex) {
+ Http1AsyncDelegate delegate;
+ Throwable recorded;
+ debug.log(Level.DEBUG, "onError: %s", (Object) ex);
+ synchronized (this) {
+ delegate = this.delegate;
+ recorded = error;
+ if (recorded == null) {
+ // retry is set to true by HttpExchange when the connection is
+ // already connected, which means it's been retrieved from
+ // the pool.
+ if (retry && (ex instanceof IOException)) {
+ // could be either EOFException, or
+ // IOException("connection reset by peer), or
+ // SSLHandshakeException resulting from the server having
+ // closed the SSL session.
+ if (received.get() == 0) {
+ // If we receive such an exception before having
+ // received any byte, then in this case, we will
+ // throw ConnectionExpiredException
+ // to try & force a retry of the request.
+ retry = false;
+ ex = new ConnectionExpiredException(
+ "subscription is finished", ex);
+ }
+ }
+ error = ex;
+ }
+ final Throwable t = (recorded == null ? ex : recorded);
+ debug.log(Level.DEBUG, () -> "recorded " + t
+ + "\n\t delegate: " + delegate
+ + "\t\t queue.isEmpty: " + queue.isEmpty(), ex);
+ }
+ if (queue.isEmpty() || pendingDelegateRef.get() != null) {
+ // This callback is called from within the selector thread.
+ // Use an executor here to avoid doing the heavy lifting in the
+ // selector.
+ scheduler.runOrSchedule(executor);
+ }
+ }
+
+ void stop() {
+ debug.log(Level.DEBUG, "stopping");
+ scheduler.stop();
+ delegate = null;
+ owner = null;
+ }
+
+ /**
+ * Returns the TubeSubscriber for reading from the connection flow.
+ * @return the TubeSubscriber for reading from the connection flow.
+ */
+ TubeSubscriber subscriber() {
+ return subscriber;
+ }
+
+ /**
+ * A simple tube subscriber for reading from the connection flow.
+ */
+ final class Http1TubeSubscriber implements TubeSubscriber {
+ volatile Flow.Subscription subscription;
+ volatile boolean completed;
+ volatile boolean dropped;
+
+ public void onSubscribe(Flow.Subscription subscription) {
+ // supports being called multiple time.
+ // doesn't cancel the previous subscription, since that is
+ // most probably the same as the new subscription.
+ assert this.subscription == null || dropped == false;
+ this.subscription = subscription;
+ dropped = false;
+ canRequestMore.set(true);
+ if (delegate != null) {
+ scheduler.runOrSchedule(executor);
+ }
+ }
+
+ void requestMore() {
+ Flow.Subscription s = subscription;
+ if (s == null) return;
+ if (canRequestMore.compareAndSet(true, false)) {
+ if (!completed && !dropped) {
+ debug.log(Level.DEBUG,
+ "Http1TubeSubscriber: requesting one more from upstream");
+ s.request(1);
+ return;
+ }
+ }
+ debug.log(Level.DEBUG, "Http1TubeSubscriber: no need to request more");
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ canRequestMore.set(item.isEmpty());
+ for (ByteBuffer buffer : item) {
+ asyncReceive(buffer);
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ onReadError(throwable);
+ completed = true;
+ }
+
+ @Override
+ public void onComplete() {
+ onReadError(new EOFException("EOF reached while reading"));
+ completed = true;
+ }
+
+ public void dropSubscription() {
+ debug.log(Level.DEBUG, "Http1TubeSubscriber: dropSubscription");
+ // we could probably set subscription to null here...
+ // then we might not need the 'dropped' boolean?
+ dropped = true;
+ }
+
+ }
+
+ // Drains the content of the queue into a single ByteBuffer.
+ // The scheduler must be permanently stopped before calling drain().
+ ByteBuffer drain(ByteBuffer initial) {
+ // Revisit: need to clean that up.
+ //
+ ByteBuffer b = initial = (initial == null ? Utils.EMPTY_BYTEBUFFER : initial);
+ assert scheduler.isStopped();
+
+ if (queue.isEmpty()) return b;
+
+ // sanity check: we shouldn't have queued the same
+ // buffer twice.
+ ByteBuffer[] qbb = queue.toArray(new ByteBuffer[queue.size()]);
+ assert java.util.stream.Stream.of(qbb)
+ .collect(Collectors.toSet())
+ .size() == qbb.length : debugQBB(qbb);
+
+ // compute the number of bytes in the queue, the number of bytes
+ // in the initial buffer
+ // TODO: will need revisiting - as it is not guaranteed that all
+ // data will fit in single BB!
+ int size = Utils.remaining(qbb, Integer.MAX_VALUE);
+ int remaining = b.remaining();
+ int free = b.capacity() - b.position() - remaining;
+ debug.log(Level.DEBUG,
+ "Flushing %s bytes from queue into initial buffer (remaining=%s, free=%s)",
+ size, remaining, free);
+
+ // check whether the initial buffer has enough space
+ if (size > free) {
+ debug.log(Level.DEBUG,
+ "Allocating new buffer for initial: %s", (size + remaining));
+ // allocates a new buffer and copy initial to it
+ b = ByteBuffer.allocate(size + remaining);
+ Utils.copy(initial, b);
+ assert b.position() == remaining;
+ b.flip();
+ assert b.position() == 0;
+ assert b.limit() == remaining;
+ assert b.remaining() == remaining;
+ }
+
+ // store position and limit
+ int pos = b.position();
+ int limit = b.limit();
+ assert limit - pos == remaining;
+ assert b.capacity() >= remaining + size
+ : "capacity: " + b.capacity()
+ + ", remaining: " + b.remaining()
+ + ", size: " + size;
+
+ // prepare to copy the content of the queue
+ b.position(limit);
+ b.limit(pos + remaining + size);
+ assert b.remaining() >= size :
+ "remaining: " + b.remaining() + ", size: " + size;
+
+ // copy the content of the queue
+ int count = 0;
+ for (int i=0; i<qbb.length; i++) {
+ ByteBuffer b2 = qbb[i];
+ int r = b2.remaining();
+ assert b.remaining() >= r : "need at least " + r + " only "
+ + b.remaining() + " available";
+ int copied = Utils.copy(b2, b);
+ assert copied == r : "copied="+copied+" available="+r;
+ assert b2.remaining() == 0;
+ count += copied;
+ }
+ assert count == size;
+ assert b.position() == pos + remaining + size :
+ "b.position="+b.position()+" != "+pos+"+"+remaining+"+"+size;
+
+ // reset limit and position
+ b.limit(limit+size);
+ b.position(pos);
+
+ // we can clear the refs
+ queue.clear();
+ final ByteBuffer bb = b;
+ debug.log(Level.DEBUG, () -> "Initial buffer now has " + bb.remaining()
+ + " pos=" + bb.position() + " limit=" + bb.limit());
+
+ return b;
+ }
+
+ private String debugQBB(ByteBuffer[] qbb) {
+ StringBuilder msg = new StringBuilder();
+ List<ByteBuffer> lbb = Arrays.asList(qbb);
+ Set<ByteBuffer> sbb = new HashSet<>(Arrays.asList(qbb));
+
+ int uniquebb = sbb.size();
+ msg.append("qbb: ").append(lbb.size())
+ .append(" (unique: ").append(uniquebb).append("), ")
+ .append("duplicates: ");
+ String sep = "";
+ for (ByteBuffer b : lbb) {
+ if (!sbb.remove(b)) {
+ msg.append(sep)
+ .append(String.valueOf(b))
+ .append("[remaining=")
+ .append(b.remaining())
+ .append(", position=")
+ .append(b.position())
+ .append(", capacity=")
+ .append(b.capacity())
+ .append("]");
+ sep = ", ";
+ }
+ }
+ return msg.toString();
+ }
+
+ volatile String dbgTag;
+ String dbgString() {
+ String tag = dbgTag;
+ if (tag == null) {
+ String flowTag = null;
+ Http1Exchange<?> exchg = owner;
+ Object flow = (exchg != null)
+ ? exchg.connection().getConnectionFlow()
+ : null;
+ flowTag = tag = flow == null ? null: (String.valueOf(flow));
+ if (flowTag != null) {
+ dbgTag = tag = flowTag + " Http1AsyncReceiver";
+ } else {
+ tag = "Http1AsyncReceiver";
+ }
+ }
+ return tag;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<T> extends ExchangeImpl<T> {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ private static final System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("Http1Exchange"::toString, DEBUG);
+
+ final HttpRequestImpl request; // main request
+ final Http1Request requestAction;
+ private volatile Http1Response<T> response;
+ final HttpConnection connection;
+ final HttpClientImpl client;
+ final Executor executor;
+ private final Http1AsyncReceiver asyncReceiver;
+
+ /** Records a possible cancellation raised before any operation
+ * has been initiated, or an error received while sending the request. */
+ private Throwable failed;
+ private final List<CompletableFuture<?>> operations; // used for cancel
+
+ /** Must be held when operating on any internal state or data. */
+ private final Object lock = new Object();
+
+ /** Holds the outgoing data, either the headers or a request body part. Or
+ * an error from the request body publisher. At most there can be ~2 pieces
+ * of outgoing data ( onComplete|onError can be invoked without demand ).*/
+ final ConcurrentLinkedDeque<DataPair> outgoing = new ConcurrentLinkedDeque<>();
+
+ /** The write publisher, responsible for writing the complete request ( both
+ * headers and body ( if any ). */
+ private final Http1Publisher writePublisher = new Http1Publisher();
+
+ /** Completed when the header have been published, or there is an error */
+ private final CompletableFuture<ExchangeImpl<T>> headersSentCF = new MinimalFuture<>();
+ /** Completed when the body has been published, or there is an error */
+ private final CompletableFuture<ExchangeImpl<T>> bodySentCF = new MinimalFuture<>();
+
+ /** The subscriber to the request's body published. Maybe null. */
+ private volatile Http1BodySubscriber bodySubscriber;
+
+ enum State { INITIAL,
+ HEADERS,
+ BODY,
+ ERROR, // terminal state
+ COMPLETING,
+ COMPLETED } // terminal state
+
+ private State state = State.INITIAL;
+
+ /** A carrier for either data or an error. Used to carry data, and communicate
+ * errors from the request ( both headers and body ) to the exchange. */
+ static class DataPair {
+ Throwable throwable;
+ List<ByteBuffer> data;
+ DataPair(List<ByteBuffer> data, Throwable throwable){
+ this.data = data;
+ this.throwable = throwable;
+ }
+ @Override
+ public String toString() {
+ return "DataPair [data=" + data + ", throwable=" + throwable + "]";
+ }
+ }
+
+ /** An abstract supertype for HTTP/1.1 body subscribers. There are two
+ * concrete implementations: {@link Http1Request.StreamSubscriber}, and
+ * {@link Http1Request.FixedContentSubscriber}, for receiving chunked and
+ * fixed length bodies, respectively. */
+ static abstract class Http1BodySubscriber implements Flow.Subscriber<ByteBuffer> {
+ protected volatile Flow.Subscription subscription;
+ protected volatile boolean complete;
+
+ /** Final sentinel in the stream of request body. */
+ static final List<ByteBuffer> COMPLETED = List.of(ByteBuffer.allocate(0));
+
+ void request(long n) {
+ DEBUG_LOGGER.log(Level.DEBUG, () ->
+ "Http1BodySubscriber requesting " + n + ", from " + subscription);
+ subscription.request(n);
+ }
+
+ static Http1BodySubscriber completeSubscriber() {
+ return new Http1BodySubscriber() {
+ @Override public void onSubscribe(Flow.Subscription subscription) { error(); }
+ @Override public void onNext(ByteBuffer item) { error(); }
+ @Override public void onError(Throwable throwable) { error(); }
+ @Override public void onComplete() { error(); }
+ private void error() {
+ throw new InternalError("should not reach here");
+ }
+ };
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "HTTP/1.1 " + request.toString();
+ }
+
+ HttpRequestImpl request() {
+ return request;
+ }
+
+ Http1Exchange(Exchange<T> exchange, HttpConnection connection)
+ throws IOException
+ {
+ super(exchange);
+ this.request = exchange.request();
+ this.client = exchange.client();
+ this.executor = exchange.executor();
+ this.operations = new LinkedList<>();
+ operations.add(headersSentCF);
+ operations.add(bodySentCF);
+ if (connection != null) {
+ this.connection = connection;
+ } else {
+ InetSocketAddress addr = request.getAddress();
+ this.connection = HttpConnection.getConnection(addr, client, request, HTTP_1_1);
+ }
+ this.requestAction = new Http1Request(request, this);
+ this.asyncReceiver = new Http1AsyncReceiver(executor, this);
+ asyncReceiver.subscribe(new InitialErrorReceiver());
+ }
+
+ /** An initial receiver that handles no data, but cancels the request if
+ * it receives an error. Will be replaced when reading response body. */
+ final class InitialErrorReceiver implements Http1AsyncReceiver.Http1AsyncDelegate {
+ volatile AbstractSubscription s;
+ @Override
+ public boolean tryAsyncReceive(ByteBuffer ref) {
+ return false; // no data has been processed, leave it in the queue
+ }
+
+ @Override
+ public void onReadError(Throwable ex) {
+ cancelImpl(ex);
+ }
+
+ @Override
+ public void onSubscribe(AbstractSubscription s) {
+ this.s = s;
+ }
+
+ public AbstractSubscription subscription() {
+ return s;
+ }
+ }
+
+ @Override
+ HttpConnection connection() {
+ return connection;
+ }
+
+ private void connectFlows(HttpConnection connection) {
+ FlowTube tube = connection.getConnectionFlow();
+ debug.log(Level.DEBUG, "%s connecting flows", tube);
+
+ // Connect the flow to our Http1TubeSubscriber:
+ // asyncReceiver.subscriber().
+ tube.connectFlows(writePublisher,
+ asyncReceiver.subscriber());
+ }
+
+ @Override
+ CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
+ // create the response before sending the request headers, so that
+ // the response can set the appropriate receivers.
+ debug.log(Level.DEBUG, "Sending headers only");
+ if (response == null) {
+ response = new Http1Response<>(connection, this, asyncReceiver);
+ }
+
+ debug.log(Level.DEBUG, "response created in advance");
+ // If the first attempt to read something triggers EOF, or
+ // IOException("channel reset by peer"), we're going to retry.
+ // Instruct the asyncReceiver to throw ConnectionExpiredException
+ // to force a retry.
+ asyncReceiver.setRetryOnError(true);
+
+ CompletableFuture<Void> connectCF;
+ if (!connection.connected()) {
+ debug.log(Level.DEBUG, "initiating connect async");
+ connectCF = connection.connectAsync();
+ synchronized (lock) {
+ operations.add(connectCF);
+ }
+ } else {
+ connectCF = new MinimalFuture<>();
+ connectCF.complete(null);
+ }
+
+ return connectCF
+ .thenCompose(unused -> {
+ CompletableFuture<Void> cf = new MinimalFuture<>();
+ try {
+ connectFlows(connection);
+
+ debug.log(Level.DEBUG, "requestAction.headers");
+ List<ByteBuffer> data = requestAction.headers();
+ synchronized (lock) {
+ state = State.HEADERS;
+ }
+ debug.log(Level.DEBUG, "setting outgoing with headers");
+ assert outgoing.isEmpty() : "Unexpected outgoing:" + outgoing;
+ appendToOutgoing(data);
+ cf.complete(null);
+ return cf;
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG, "Failed to send headers: %s", t);
+ connection.close();
+ cf.completeExceptionally(t);
+ return cf;
+ } })
+ .thenCompose(unused -> headersSentCF);
+ }
+
+ @Override
+ CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
+ assert headersSentCF.isDone();
+ try {
+ bodySubscriber = requestAction.continueRequest();
+ if (bodySubscriber == null) {
+ bodySubscriber = Http1BodySubscriber.completeSubscriber();
+ appendToOutgoing(Http1BodySubscriber.COMPLETED);
+ } else {
+ bodySubscriber.request(1); // start
+ }
+ } catch (Throwable t) {
+ connection.close();
+ bodySentCF.completeExceptionally(t);
+ }
+ return bodySentCF;
+ }
+
+ @Override
+ CompletableFuture<Response> getResponseAsync(Executor executor) {
+ CompletableFuture<Response> cf = response.readHeadersAsync(executor);
+ Throwable cause;
+ synchronized (lock) {
+ operations.add(cf);
+ cause = failed;
+ failed = null;
+ }
+
+ if (cause != null) {
+ Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms]"
+ + "\n\tCompleting exceptionally with {2}\n",
+ request.uri(),
+ request.timeout().isPresent() ?
+ // calling duration.toMillis() can throw an exception.
+ // this is just debugging, we don't care if it overflows.
+ (request.timeout().get().getSeconds() * 1000
+ + request.timeout().get().getNano() / 1000000) : -1,
+ cause);
+ boolean acknowledged = cf.completeExceptionally(cause);
+ debug.log(Level.DEBUG,
+ () -> acknowledged
+ ? ("completed response with " + cause)
+ : ("response already completed, ignoring " + cause));
+ }
+ return cf;
+ }
+
+ @Override
+ CompletableFuture<T> readBodyAsync(BodyHandler<T> handler,
+ boolean returnConnectionToPool,
+ Executor executor)
+ {
+ BodySubscriber<T> bs = handler.apply(response.responseCode(),
+ response.responseHeaders());
+ CompletableFuture<T> bodyCF = response.readBody(bs,
+ returnConnectionToPool,
+ executor);
+ return bodyCF;
+ }
+
+ @Override
+ CompletableFuture<Void> ignoreBody() {
+ return response.ignoreBody(executor);
+ }
+
+ ByteBuffer drainLeftOverBytes() {
+ synchronized (lock) {
+ asyncReceiver.stop();
+ return asyncReceiver.drain(Utils.EMPTY_BYTEBUFFER);
+ }
+ }
+
+ void released() {
+ Http1Response<T> resp = this.response;
+ if (resp != null) resp.completed();
+ asyncReceiver.clear();
+ }
+
+ void completed() {
+ Http1Response<T> resp = this.response;
+ if (resp != null) resp.completed();
+ }
+
+ /**
+ * Cancel checks to see if request and responseAsync finished already.
+ * If not it closes the connection and completes all pending operations
+ */
+ @Override
+ void cancel() {
+ cancelImpl(new IOException("Request cancelled"));
+ }
+
+ /**
+ * Cancel checks to see if request and responseAsync finished already.
+ * If not it closes the connection and completes all pending operations
+ */
+ @Override
+ void cancel(IOException cause) {
+ cancelImpl(cause);
+ }
+
+ private void cancelImpl(Throwable cause) {
+ LinkedList<CompletableFuture<?>> toComplete = null;
+ int count = 0;
+ synchronized (lock) {
+ if (failed == null)
+ failed = cause;
+ if (requestAction != null && requestAction.finished()
+ && response != null && response.finished()) {
+ return;
+ }
+ connection.close(); // TODO: ensure non-blocking if holding the lock
+ writePublisher.writeScheduler.stop();
+ if (operations.isEmpty()) {
+ Log.logTrace("Http1Exchange: request [{0}/timeout={1}ms] no pending operation."
+ + "\n\tCan''t cancel yet with {2}",
+ request.uri(),
+ request.timeout().isPresent() ?
+ // calling duration.toMillis() can throw an exception.
+ // this is just debugging, we don't care if it overflows.
+ (request.timeout().get().getSeconds() * 1000
+ + request.timeout().get().getNano() / 1000000) : -1,
+ cause);
+ } else {
+ for (CompletableFuture<?> cf : operations) {
+ if (!cf.isDone()) {
+ if (toComplete == null) toComplete = new LinkedList<>();
+ toComplete.add(cf);
+ count++;
+ }
+ }
+ operations.clear();
+ }
+ }
+ Log.logError("Http1Exchange.cancel: count=" + count);
+ if (toComplete != null) {
+ // We might be in the selector thread in case of timeout, when
+ // the SelectorManager calls purgeTimeoutsAndReturnNextDeadline()
+ // There may or may not be other places that reach here
+ // from the SelectorManager thread, so just make sure we
+ // don't complete any CF from within the selector manager
+ // thread.
+ Executor exec = client.isSelectorThread()
+ ? executor
+ : this::runInline;
+ while (!toComplete.isEmpty()) {
+ CompletableFuture<?> cf = toComplete.poll();
+ exec.execute(() -> {
+ if (cf.completeExceptionally(cause)) {
+ debug.log(Level.DEBUG, "completed cf with %s",
+ (Object) cause);
+ }
+ });
+ }
+ }
+ }
+
+ private void runInline(Runnable run) {
+ assert !client.isSelectorThread();
+ run.run();
+ }
+
+ /** Returns true if this exchange was canceled. */
+ boolean isCanceled() {
+ synchronized (lock) {
+ return failed != null;
+ }
+ }
+
+ /** Returns the cause for which this exchange was canceled, if available. */
+ Throwable getCancelCause() {
+ synchronized (lock) {
+ return failed;
+ }
+ }
+
+ /** Convenience for {@link #appendToOutgoing(DataPair)}, with just a Throwable. */
+ void appendToOutgoing(Throwable throwable) {
+ appendToOutgoing(new DataPair(null, throwable));
+ }
+
+ /** Convenience for {@link #appendToOutgoing(DataPair)}, with just data. */
+ void appendToOutgoing(List<ByteBuffer> item) {
+ appendToOutgoing(new DataPair(item, null));
+ }
+
+ private void appendToOutgoing(DataPair dp) {
+ debug.log(Level.DEBUG, "appending to outgoing " + dp);
+ outgoing.add(dp);
+ writePublisher.writeScheduler.runOrSchedule();
+ }
+
+ /** Tells whether, or not, there is any outgoing data that can be published,
+ * or if there is an error. */
+ private boolean hasOutgoing() {
+ return !outgoing.isEmpty();
+ }
+
+ // Invoked only by the publisher
+ // ALL tasks should execute off the Selector-Manager thread
+ /** Returns the next portion of the HTTP request, or the error. */
+ private DataPair getOutgoing() {
+ final Executor exec = client.theExecutor();
+ final DataPair dp = outgoing.pollFirst();
+
+ if (dp == null) // publisher has not published anything yet
+ return null;
+
+ synchronized (lock) {
+ if (dp.throwable != null) {
+ state = State.ERROR;
+ exec.execute(() -> {
+ connection.close();
+ headersSentCF.completeExceptionally(dp.throwable);
+ bodySentCF.completeExceptionally(dp.throwable);
+ });
+ return dp;
+ }
+
+ switch (state) {
+ case HEADERS:
+ state = State.BODY;
+ // completeAsync, since dependent tasks should run in another thread
+ debug.log(Level.DEBUG, "initiating completion of headersSentCF");
+ headersSentCF.completeAsync(() -> this, exec);
+ break;
+ case BODY:
+ if (dp.data == Http1BodySubscriber.COMPLETED) {
+ state = State.COMPLETING;
+ debug.log(Level.DEBUG, "initiating completion of bodySentCF");
+ bodySentCF.completeAsync(() -> this, exec);
+ } else {
+ debug.log(Level.DEBUG, "requesting more body from the subscriber");
+ exec.execute(() -> bodySubscriber.request(1));
+ }
+ break;
+ case INITIAL:
+ case ERROR:
+ case COMPLETING:
+ case COMPLETED:
+ default:
+ assert false : "Unexpected state:" + state;
+ }
+
+ return dp;
+ }
+ }
+
+ /** A Publisher of HTTP/1.1 headers and request body. */
+ final class Http1Publisher implements FlowTube.TubePublisher {
+
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString);
+ volatile Flow.Subscriber<? super List<ByteBuffer>> subscriber;
+ volatile boolean cancelled;
+ final Http1WriteSubscription subscription = new Http1WriteSubscription();
+ final Demand demand = new Demand();
+ final SequentialScheduler writeScheduler =
+ SequentialScheduler.synchronizedScheduler(new WriteTask());
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
+ assert state == State.INITIAL;
+ Objects.requireNonNull(s);
+ assert subscriber == null;
+
+ subscriber = s;
+ debug.log(Level.DEBUG, "got subscriber: %s", s);
+ s.onSubscribe(subscription);
+ }
+
+ volatile String dbgTag;
+ String dbgString() {
+ String tag = dbgTag;
+ Object flow = connection.getConnectionFlow();
+ if (tag == null && flow != null) {
+ dbgTag = tag = "Http1Publisher(" + flow + ")";
+ } else if (tag == null) {
+ tag = "Http1Publisher(?)";
+ }
+ return tag;
+ }
+
+ final class WriteTask implements Runnable {
+ @Override
+ public void run() {
+ assert state != State.COMPLETED : "Unexpected state:" + state;
+ debug.log(Level.DEBUG, "WriteTask");
+ if (subscriber == null) {
+ debug.log(Level.DEBUG, "no subscriber yet");
+ return;
+ }
+ debug.log(Level.DEBUG, () -> "hasOutgoing = " + hasOutgoing());
+ while (hasOutgoing() && demand.tryDecrement()) {
+ DataPair dp = getOutgoing();
+
+ if (dp.throwable != null) {
+ debug.log(Level.DEBUG, "onError");
+ // Do not call the subscriber's onError, it is not required.
+ writeScheduler.stop();
+ } else {
+ List<ByteBuffer> data = dp.data;
+ if (data == Http1BodySubscriber.COMPLETED) {
+ synchronized (lock) {
+ assert state == State.COMPLETING : "Unexpected state:" + state;
+ state = State.COMPLETED;
+ }
+ debug.log(Level.DEBUG,
+ "completed, stopping %s", writeScheduler);
+ writeScheduler.stop();
+ // Do nothing more. Just do not publish anything further.
+ // The next Subscriber will eventually take over.
+
+ } else {
+ debug.log(Level.DEBUG, () ->
+ "onNext with " + Utils.remaining(data) + " bytes");
+ subscriber.onNext(data);
+ }
+ }
+ }
+ }
+ }
+
+ final class Http1WriteSubscription implements Flow.Subscription {
+
+ @Override
+ public void request(long n) {
+ if (cancelled)
+ return; //no-op
+ demand.increase(n);
+ debug.log(Level.DEBUG,
+ "subscription request(%d), demand=%s", n, demand);
+ writeScheduler.runOrSchedule(client.theExecutor());
+ }
+
+ @Override
+ public void cancel() {
+ debug.log(Level.DEBUG, "subscription cancelled");
+ if (cancelled)
+ return; //no-op
+ cancelled = true;
+ writeScheduler.stop();
+ }
+ }
+ }
+
+ String dbgString() {
+ return "Http1Exchange";
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<String,List<String>> privateMap = new HashMap<>();
+
+ enum State { STATUS_LINE,
+ STATUS_LINE_FOUND_CR,
+ STATUS_LINE_END,
+ STATUS_LINE_END_CR,
+ HEADER,
+ HEADER_FOUND_CR,
+ HEADER_FOUND_LF,
+ HEADER_FOUND_CR_LF,
+ HEADER_FOUND_CR_LF_CR,
+ FINISHED }
+
+ private State state = State.STATUS_LINE;
+
+ /** Returns the status-line. */
+ String statusLine() { return statusLine; }
+
+ /** Returns the response code. */
+ int responseCode() { return responseCode; }
+
+ /** Returns the headers, possibly empty. */
+ HttpHeaders headers() { assert state == State.FINISHED; return headers; }
+
+ /**
+ * Parses HTTP/1.X status-line and headers from the given bytes. Must be
+ * called successive times, with additional data, until returns true.
+ *
+ * All given ByteBuffers will be consumed, until ( possibly ) the last one
+ * ( when true is returned ), which may not be fully consumed.
+ *
+ * @param input the ( partial ) header data
+ * @return true iff the end of the headers block has been reached
+ */
+ boolean parse(ByteBuffer input) throws ProtocolException {
+ requireNonNull(input, "null input");
+
+ while (input.hasRemaining() && state != State.FINISHED) {
+ switch (state) {
+ case STATUS_LINE:
+ readResumeStatusLine(input);
+ break;
+ case STATUS_LINE_FOUND_CR:
+ readStatusLineFeed(input);
+ break;
+ case STATUS_LINE_END:
+ maybeStartHeaders(input);
+ break;
+ case STATUS_LINE_END_CR:
+ maybeEndHeaders(input);
+ break;
+ case HEADER:
+ readResumeHeader(input);
+ break;
+ // fallthrough
+ case HEADER_FOUND_CR:
+ case HEADER_FOUND_LF:
+ resumeOrLF(input);
+ break;
+ case HEADER_FOUND_CR_LF:
+ resumeOrSecondCR(input);
+ break;
+ case HEADER_FOUND_CR_LF_CR:
+ resumeOrEndHeaders(input);
+ break;
+ default:
+ throw new InternalError(
+ "Unexpected state: " + String.valueOf(state));
+ }
+ }
+
+ return state == State.FINISHED;
+ }
+
+ private void readResumeStatusLine(ByteBuffer input) {
+ char c = 0;
+ while (input.hasRemaining() && (c =(char)input.get()) != CR) {
+ sb.append(c);
+ }
+
+ if (c == CR) {
+ state = State.STATUS_LINE_FOUND_CR;
+ }
+ }
+
+ private void readStatusLineFeed(ByteBuffer input) throws ProtocolException {
+ char c = (char)input.get();
+ if (c != LF) {
+ throw protocolException("Bad trailing char, \"%s\", when parsing status-line, \"%s\"",
+ c, sb.toString());
+ }
+
+ statusLine = sb.toString();
+ sb = new StringBuilder();
+ if (!statusLine.startsWith("HTTP/1.")) {
+ throw protocolException("Invalid status line: \"%s\"", statusLine);
+ }
+ if (statusLine.length() < 12) {
+ throw protocolException("Invalid status line: \"%s\"", statusLine);
+ }
+ responseCode = Integer.parseInt(statusLine.substring(9, 12));
+
+ state = State.STATUS_LINE_END;
+ }
+
+ private void maybeStartHeaders(ByteBuffer input) {
+ assert state == State.STATUS_LINE_END;
+ assert sb.length() == 0;
+ char c = (char)input.get();
+ if (c == CR) {
+ state = State.STATUS_LINE_END_CR;
+ } else {
+ sb.append(c);
+ state = State.HEADER;
+ }
+ }
+
+ private void maybeEndHeaders(ByteBuffer input) throws ProtocolException {
+ assert state == State.STATUS_LINE_END_CR;
+ assert sb.length() == 0;
+ char c = (char)input.get();
+ if (c == LF) {
+ headers = ImmutableHeaders.of(privateMap);
+ privateMap = null;
+ state = State.FINISHED; // no headers
+ } else {
+ throw protocolException("Unexpected \"%s\", after status-line CR", c);
+ }
+ }
+
+ private void readResumeHeader(ByteBuffer input) {
+ assert state == State.HEADER;
+ assert input.hasRemaining();
+ while (input.hasRemaining()) {
+ char c = (char)input.get();
+ if (c == CR) {
+ state = State.HEADER_FOUND_CR;
+ break;
+ } else if (c == LF) {
+ state = State.HEADER_FOUND_LF;
+ break;
+ }
+
+ if (c == HT)
+ c = SP;
+ sb.append(c);
+ }
+ }
+
+ private void addHeaderFromString(String headerString) {
+ assert sb.length() == 0;
+ int idx = headerString.indexOf(':');
+ if (idx == -1)
+ return;
+ String name = headerString.substring(0, idx).trim();
+ if (name.isEmpty())
+ return;
+ String value = headerString.substring(idx + 1, headerString.length()).trim();
+
+ privateMap.computeIfAbsent(name.toLowerCase(Locale.US),
+ k -> new ArrayList<>()).add(value);
+ }
+
+ private void resumeOrLF(ByteBuffer input) {
+ assert state == State.HEADER_FOUND_CR || state == State.HEADER_FOUND_LF;
+ char c = (char)input.get();
+ if (c == LF && state == State.HEADER_FOUND_CR) {
+ // header value will be flushed by
+ // resumeOrSecondCR if next line does not
+ // begin by SP or HT
+ state = State.HEADER_FOUND_CR_LF;
+ } else if (c == SP || c == HT) {
+ sb.append(SP); // parity with MessageHeaders
+ state = State.HEADER;
+ } else {
+ sb = new StringBuilder();
+ sb.append(c);
+ state = State.HEADER;
+ }
+ }
+
+ private void resumeOrSecondCR(ByteBuffer input) {
+ assert state == State.HEADER_FOUND_CR_LF;
+ char c = (char)input.get();
+ if (c == CR) {
+ if (sb.length() > 0) {
+ // no continuation line - flush
+ // previous header value.
+ String headerString = sb.toString();
+ sb = new StringBuilder();
+ addHeaderFromString(headerString);
+ }
+ state = State.HEADER_FOUND_CR_LF_CR;
+ } else if (c == SP || c == HT) {
+ assert sb.length() != 0;
+ sb.append(SP); // continuation line
+ state = State.HEADER;
+ } else {
+ if (sb.length() > 0) {
+ // no continuation line - flush
+ // previous header value.
+ String headerString = sb.toString();
+ sb = new StringBuilder();
+ addHeaderFromString(headerString);
+ }
+ sb.append(c);
+ state = State.HEADER;
+ }
+ }
+
+ private void resumeOrEndHeaders(ByteBuffer input) throws ProtocolException {
+ assert state == State.HEADER_FOUND_CR_LF_CR;
+ char c = (char)input.get();
+ if (c == LF) {
+ state = State.FINISHED;
+ headers = ImmutableHeaders.of(privateMap);
+ privateMap = null;
+ } else {
+ throw protocolException("Unexpected \"%s\", after CR LF CR", c);
+ }
+ }
+
+ private ProtocolException protocolException(String format, Object... args) {
+ return new ProtocolException(format(format, args));
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<String,List<String>> filter =
+ connection.headerFilter(request);
+
+ // If we're sending this request through a tunnel,
+ // then don't send any preemptive proxy-* headers that
+ // the authentication filter may have saved in its
+ // cache.
+ collectHeaders1(sb, systemHeaders, filter);
+
+ // If we're sending this request through a tunnel,
+ // don't send any user-supplied proxy-* headers
+ // to the target server.
+ collectHeaders1(sb, userHeaders, filter);
+ sb.append("\r\n");
+ }
+
+ private void collectHeaders1(StringBuilder sb, HttpHeaders headers,
+ BiPredicate<String, List<String>> filter) {
+ for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) {
+ String key = entry.getKey();
+ List<String> values = entry.getValue();
+ if (!filter.test(key, values)) continue;
+ for (String value : values) {
+ sb.append(key).append(": ").append(value).append("\r\n");
+ }
+ }
+ }
+
+ private String getPathAndQuery(URI uri) {
+ String path = uri.getPath();
+ String query = uri.getQuery();
+ if (path == null || path.equals("")) {
+ path = "/";
+ }
+ if (query == null) {
+ query = "";
+ }
+ if (query.equals("")) {
+ return path;
+ } else {
+ return path + "?" + query;
+ }
+ }
+
+ private String authorityString(InetSocketAddress addr) {
+ return addr.getHostString() + ":" + addr.getPort();
+ }
+
+ private String hostString() {
+ URI uri = request.uri();
+ int port = uri.getPort();
+ String host = uri.getHost();
+
+ boolean defaultPort;
+ if (port == -1) {
+ defaultPort = true;
+ } else if (request.secure()) {
+ defaultPort = port == 443;
+ } else {
+ defaultPort = port == 80;
+ }
+
+ if (defaultPort) {
+ return host;
+ } else {
+ return host + ":" + Integer.toString(port);
+ }
+ }
+
+ private String requestURI() {
+ URI uri = request.uri();
+ String method = request.method();
+
+ if ((request.proxy() == null && !method.equals("CONNECT"))
+ || request.isWebSocket()) {
+ return getPathAndQuery(uri);
+ }
+ if (request.secure()) {
+ if (request.method().equals("CONNECT")) {
+ // use authority for connect itself
+ return authorityString(request.authority());
+ } else {
+ // requests over tunnel do not require full URL
+ return getPathAndQuery(uri);
+ }
+ }
+ if (request.method().equals("CONNECT")) {
+ // use authority for connect itself
+ return authorityString(request.authority());
+ }
+
+ return uri == null? authorityString(request.authority()) : uri.toString();
+ }
+
+ private boolean finished;
+
+ synchronized boolean finished() {
+ return finished;
+ }
+
+ synchronized void setFinished() {
+ finished = true;
+ }
+
+ List<ByteBuffer> headers() {
+ if (Log.requests() && request != null) {
+ Log.logRequest(request.toString());
+ }
+ String uriString = requestURI();
+ StringBuilder sb = new StringBuilder(64);
+ sb.append(request.method())
+ .append(' ')
+ .append(uriString)
+ .append(" HTTP/1.1\r\n");
+
+ URI uri = request.uri();
+ if (uri != null) {
+ systemHeaders.setHeader("Host", hostString());
+ }
+ if (requestPublisher == null) {
+ // Not a user request, or maybe a method, e.g. GET, with no body.
+ contentLength = 0;
+ } else {
+ contentLength = requestPublisher.contentLength();
+ }
+
+ if (contentLength == 0) {
+ systemHeaders.setHeader("Content-Length", "0");
+ } else if (contentLength > 0) {
+ systemHeaders.setHeader("Content-Length", Long.toString(contentLength));
+ streaming = false;
+ } else {
+ streaming = true;
+ systemHeaders.setHeader("Transfer-encoding", "chunked");
+ }
+ collectHeaders0(sb);
+ String hs = sb.toString();
+ logHeaders(hs);
+ ByteBuffer b = ByteBuffer.wrap(hs.getBytes(US_ASCII));
+ return List.of(b);
+ }
+
+ Http1BodySubscriber continueRequest() {
+ Http1BodySubscriber subscriber;
+ if (streaming) {
+ subscriber = new StreamSubscriber();
+ requestPublisher.subscribe(subscriber);
+ } else {
+ if (contentLength == 0)
+ return null;
+
+ subscriber = new FixedContentSubscriber();
+ requestPublisher.subscribe(subscriber);
+ }
+ return subscriber;
+ }
+
+ class StreamSubscriber extends Http1BodySubscriber {
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ if (this.subscription != null) {
+ Throwable t = new IllegalStateException("already subscribed");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ this.subscription = subscription;
+ }
+ }
+
+ @Override
+ public void onNext(ByteBuffer item) {
+ Objects.requireNonNull(item);
+ if (complete) {
+ Throwable t = new IllegalStateException("subscription already completed");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ int chunklen = item.remaining();
+ ArrayList<ByteBuffer> l = new ArrayList<>(3);
+ l.add(getHeader(chunklen));
+ l.add(item);
+ l.add(ByteBuffer.wrap(CRLF));
+ http1Exchange.appendToOutgoing(l);
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ if (complete)
+ return;
+
+ subscription.cancel();
+ http1Exchange.appendToOutgoing(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ if (complete) {
+ Throwable t = new IllegalStateException("subscription already completed");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ ArrayList<ByteBuffer> l = new ArrayList<>(2);
+ l.add(ByteBuffer.wrap(EMPTY_CHUNK_BYTES));
+ l.add(ByteBuffer.wrap(CRLF));
+ complete = true;
+ //setFinished();
+ http1Exchange.appendToOutgoing(l);
+ http1Exchange.appendToOutgoing(COMPLETED);
+ setFinished(); // TODO: before or after,? does it matter?
+
+ }
+ }
+ }
+
+ class FixedContentSubscriber extends Http1BodySubscriber {
+
+ private volatile long contentWritten;
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ if (this.subscription != null) {
+ Throwable t = new IllegalStateException("already subscribed");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ this.subscription = subscription;
+ }
+ }
+
+ @Override
+ public void onNext(ByteBuffer item) {
+ debug.log(Level.DEBUG, "onNext");
+ Objects.requireNonNull(item);
+ if (complete) {
+ Throwable t = new IllegalStateException("subscription already completed");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ long writing = item.remaining();
+ long written = (contentWritten += writing);
+
+ if (written > contentLength) {
+ subscription.cancel();
+ String msg = connection.getConnectionFlow()
+ + " [" + Thread.currentThread().getName() +"] "
+ + "Too many bytes in request body. Expected: "
+ + contentLength + ", got: " + written;
+ http1Exchange.appendToOutgoing(new IOException(msg));
+ } else {
+ http1Exchange.appendToOutgoing(List.of(item));
+ }
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ debug.log(Level.DEBUG, "onError");
+ if (complete) // TODO: error?
+ return;
+
+ subscription.cancel();
+ http1Exchange.appendToOutgoing(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ debug.log(Level.DEBUG, "onComplete");
+ if (complete) {
+ Throwable t = new IllegalStateException("subscription already completed");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ complete = true;
+ long written = contentWritten;
+ if (contentLength > written) {
+ subscription.cancel();
+ Throwable t = new IOException(connection.getConnectionFlow()
+ + " [" + Thread.currentThread().getName() +"] "
+ + "Too few bytes returned by the publisher ("
+ + written + "/"
+ + contentLength + ")");
+ http1Exchange.appendToOutgoing(t);
+ } else {
+ http1Exchange.appendToOutgoing(COMPLETED);
+ }
+ }
+ }
+ }
+
+ private static final byte[] CRLF = {'\r', '\n'};
+ private static final byte[] EMPTY_CHUNK_BYTES = {'0', '\r', '\n'};
+
+ /** Returns a header for a particular chunk size */
+ private static ByteBuffer getHeader(int size) {
+ String hexStr = Integer.toHexString(size);
+ byte[] hexBytes = hexStr.getBytes(US_ASCII);
+ byte[] header = new byte[hexStr.length()+2];
+ System.arraycopy(hexBytes, 0, header, 0, hexBytes.length);
+ header[hexBytes.length] = CRLF[0];
+ header[hexBytes.length+1] = CRLF[1];
+ return ByteBuffer.wrap(header);
+ }
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this::toString, DEBUG);
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<T> {
+
+ private volatile ResponseContent content;
+ private final HttpRequestImpl request;
+ private Response response;
+ private final HttpConnection connection;
+ private HttpHeaders headers;
+ private int responseCode;
+ private final Http1Exchange<T> exchange;
+ private boolean return2Cache; // return connection to cache when finished
+ private final HeadersReader headersReader; // used to read the headers
+ private final BodyReader bodyReader; // used to read the body
+ private final Http1AsyncReceiver asyncReceiver;
+ private volatile EOFException eof;
+ // max number of bytes of (fixed length) body to ignore on redirect
+ private final static int MAX_IGNORE = 1024;
+
+ // Revisit: can we get rid of this?
+ static enum State {INITIAL, READING_HEADERS, READING_BODY, DONE}
+ private volatile State readProgress = State.INITIAL;
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ final System.Logger debug = Utils.getDebugLogger(this.getClass()::getSimpleName, DEBUG);
+
+
+ Http1Response(HttpConnection conn,
+ Http1Exchange<T> exchange,
+ Http1AsyncReceiver asyncReceiver) {
+ this.readProgress = State.INITIAL;
+ this.request = exchange.request();
+ this.exchange = exchange;
+ this.connection = conn;
+ this.asyncReceiver = asyncReceiver;
+ headersReader = new HeadersReader(this::advance);
+ bodyReader = new BodyReader(this::advance);
+ }
+
+ public CompletableFuture<Response> readHeadersAsync(Executor executor) {
+ debug.log(Level.DEBUG, () -> "Reading Headers: (remaining: "
+ + asyncReceiver.remaining() +") " + readProgress);
+ // with expect continue we will resume reading headers + body.
+ asyncReceiver.unsubscribe(bodyReader);
+ bodyReader.reset();
+ Http1HeaderParser hd = new Http1HeaderParser();
+ readProgress = State.READING_HEADERS;
+ headersReader.start(hd);
+ asyncReceiver.subscribe(headersReader);
+ CompletableFuture<State> cf = headersReader.completion();
+ assert cf != null : "parsing not started";
+
+ Function<State, Response> lambda = (State completed) -> {
+ assert completed == State.READING_HEADERS;
+ debug.log(Level.DEBUG, () ->
+ "Reading Headers: creating Response object;"
+ + " state is now " + readProgress);
+ asyncReceiver.unsubscribe(headersReader);
+ responseCode = hd.responseCode();
+ headers = hd.headers();
+
+ response = new Response(request,
+ exchange.getExchange(),
+ headers,
+ responseCode,
+ HTTP_1_1);
+ return response;
+ };
+
+ if (executor != null) {
+ return cf.thenApplyAsync(lambda, executor);
+ } else {
+ return cf.thenApply(lambda);
+ }
+ }
+
+ private boolean finished;
+
+ synchronized void completed() {
+ finished = true;
+ }
+
+ synchronized boolean finished() {
+ return finished;
+ }
+
+ int fixupContentLen(int clen) {
+ if (request.method().equalsIgnoreCase("HEAD")) {
+ return 0;
+ }
+ if (clen == -1) {
+ if (headers.firstValue("Transfer-encoding").orElse("")
+ .equalsIgnoreCase("chunked")) {
+ return -1;
+ }
+ return 0;
+ }
+ return clen;
+ }
+
+ /**
+ * Read up to MAX_IGNORE bytes discarding
+ */
+ public CompletableFuture<Void> ignoreBody(Executor executor) {
+ int clen = (int)headers.firstValueAsLong("Content-Length").orElse(-1);
+ if (clen == -1 || clen > MAX_IGNORE) {
+ connection.close();
+ return MinimalFuture.completedFuture(null); // not treating as error
+ } else {
+ return readBody(HttpResponse.BodySubscriber.discard(), true, executor);
+ }
+ }
+
+ public <U> CompletableFuture<U> readBody(HttpResponse.BodySubscriber<U> p,
+ boolean return2Cache,
+ Executor executor) {
+ this.return2Cache = return2Cache;
+ final HttpResponse.BodySubscriber<U> pusher = p;
+
+ final CompletableFuture<U> cf = new MinimalFuture<>();
+
+ int clen0 = (int)headers.firstValueAsLong("Content-Length").orElse(-1);
+
+ final int clen = fixupContentLen(clen0);
+
+ // expect-continue reads headers and body twice.
+ // if we reach here, we must reset the headersReader state.
+ asyncReceiver.unsubscribe(headersReader);
+ headersReader.reset();
+
+ executor.execute(() -> {
+ try {
+ HttpClientImpl client = connection.client();
+ content = new ResponseContent(
+ connection, clen, headers, pusher,
+ this::onFinished
+ );
+ if (cf.isCompletedExceptionally()) {
+ // if an error occurs during subscription
+ connection.close();
+ return;
+ }
+ // increment the reference count on the HttpClientImpl
+ // to prevent the SelectorManager thread from exiting until
+ // the body is fully read.
+ client.reference();
+ bodyReader.start(content.getBodyParser(
+ (t) -> {
+ try {
+ if (t != null) {
+ pusher.onError(t);
+ connection.close();
+ if (!cf.isDone())
+ cf.completeExceptionally(t);
+ }
+ } finally {
+ // decrement the reference count on the HttpClientImpl
+ // to allow the SelectorManager thread to exit if no
+ // other operation is pending and the facade is no
+ // longer referenced.
+ client.unreference();
+ bodyReader.onComplete(t);
+ }
+ }));
+ CompletableFuture<State> bodyReaderCF = bodyReader.completion();
+ asyncReceiver.subscribe(bodyReader);
+ assert bodyReaderCF != null : "parsing not started";
+ // Make sure to keep a reference to asyncReceiver from
+ // within this
+ CompletableFuture<?> trailingOp = bodyReaderCF.whenComplete((s,t) -> {
+ t = Utils.getCompletionCause(t);
+ try {
+ if (t != null) {
+ debug.log(Level.DEBUG, () ->
+ "Finished reading body: " + s);
+ assert s == State.READING_BODY;
+ }
+ if (t != null && !cf.isDone()) {
+ pusher.onError(t);
+ cf.completeExceptionally(t);
+ }
+ } catch (Throwable x) {
+ // not supposed to happen
+ asyncReceiver.onReadError(x);
+ }
+ });
+ connection.addTrailingOperation(trailingOp);
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG, () -> "Failed reading body: " + t);
+ try {
+ if (!cf.isDone()) {
+ pusher.onError(t);
+ cf.completeExceptionally(t);
+ }
+ } finally {
+ asyncReceiver.onReadError(t);
+ }
+ }
+ });
+ 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<T>
+ implements Http1AsyncReceiver.Http1AsyncDelegate {
+ abstract void start(T parser);
+ abstract CompletableFuture<State> completion();
+ // accepts a buffer from upstream.
+ // this should be implemented as a simple call to
+ // accept(ref, parser, cf)
+ public abstract boolean tryAsyncReceive(ByteBuffer buffer);
+ public abstract void onReadError(Throwable t);
+ // handle a byte buffer received from upstream.
+ // this method should set the value of Http1Response.buffer
+ // to ref.get() before beginning parsing.
+ abstract void handle(ByteBuffer buf, T parser,
+ CompletableFuture<State> cf);
+ // resets this objects state so that it can be reused later on
+ // typically puts the reference to parser and completion to null
+ abstract void reset();
+
+ // accepts a byte buffer received from upstream
+ // returns true if the buffer is fully parsed and more data can
+ // be accepted, false otherwise.
+ final boolean accept(ByteBuffer buf, T parser,
+ CompletableFuture<State> cf) {
+ if (cf == null || parser == null || cf.isDone()) return false;
+ handle(buf, parser, cf);
+ return !cf.isDone();
+ }
+ public abstract void onSubscribe(AbstractSubscription s);
+ public abstract AbstractSubscription subscription();
+
+ }
+
+ // Invoked with each new ByteBuffer when reading headers...
+ final class HeadersReader extends Receiver<Http1HeaderParser> {
+ final Consumer<State> onComplete;
+ volatile Http1HeaderParser parser;
+ volatile CompletableFuture<State> cf;
+ volatile long count; // bytes parsed (for debug)
+ volatile AbstractSubscription subscription;
+
+ HeadersReader(Consumer<State> onComplete) {
+ this.onComplete = onComplete;
+ }
+
+ @Override
+ public AbstractSubscription subscription() {
+ return subscription;
+ }
+
+ @Override
+ public void onSubscribe(AbstractSubscription s) {
+ this.subscription = s;
+ s.request(1);
+ }
+
+ @Override
+ void reset() {
+ cf = null;
+ parser = null;
+ count = 0;
+ subscription = null;
+ }
+
+ // Revisit: do we need to support restarting?
+ @Override
+ final void start(Http1HeaderParser hp) {
+ count = 0;
+ cf = new MinimalFuture<>();
+ parser = hp;
+ }
+
+ @Override
+ CompletableFuture<State> completion() {
+ return cf;
+ }
+
+ @Override
+ public final boolean tryAsyncReceive(ByteBuffer ref) {
+ boolean hasDemand = subscription.demand().tryDecrement();
+ assert hasDemand;
+ boolean needsMore = accept(ref, parser, cf);
+ if (needsMore) subscription.request(1);
+ return needsMore;
+ }
+
+ @Override
+ public final void onReadError(Throwable t) {
+ Http1Response.this.onReadError(t);
+ }
+
+ @Override
+ final void handle(ByteBuffer b,
+ Http1HeaderParser parser,
+ CompletableFuture<State> cf) {
+ assert cf != null : "parsing not started";
+ assert parser != null : "no parser";
+ try {
+ count += b.remaining();
+ debug.log(Level.DEBUG, () -> "Sending " + b.remaining()
+ + "/" + b.capacity() + " bytes to header parser");
+ if (parser.parse(b)) {
+ count -= b.remaining();
+ debug.log(Level.DEBUG, () ->
+ "Parsing headers completed. bytes=" + count);
+ onComplete.accept(State.READING_HEADERS);
+ cf.complete(State.READING_HEADERS);
+ }
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG,
+ () -> "Header parser failed to handle buffer: " + t);
+ cf.completeExceptionally(t);
+ }
+ }
+ }
+
+ // Invoked with each new ByteBuffer when reading bodies...
+ final class BodyReader extends Receiver<BodyParser> {
+ final Consumer<State> onComplete;
+ volatile BodyParser parser;
+ volatile CompletableFuture<State> cf;
+ volatile AbstractSubscription subscription;
+ BodyReader(Consumer<State> onComplete) {
+ this.onComplete = onComplete;
+ }
+
+ @Override
+ void reset() {
+ parser = null;
+ cf = null;
+ subscription = null;
+ }
+
+ // Revisit: do we need to support restarting?
+ @Override
+ final void start(BodyParser parser) {
+ cf = new MinimalFuture<>();
+ this.parser = parser;
+ }
+
+ @Override
+ CompletableFuture<State> completion() {
+ return cf;
+ }
+
+ @Override
+ public final boolean tryAsyncReceive(ByteBuffer b) {
+ return accept(b, parser, cf);
+ }
+
+ @Override
+ public final void onReadError(Throwable t) {
+ Http1Response.this.onReadError(t);
+ }
+
+ @Override
+ public AbstractSubscription subscription() {
+ return subscription;
+ }
+
+ @Override
+ public void onSubscribe(AbstractSubscription s) {
+ this.subscription = s;
+ parser.onSubscribe(s);
+ }
+
+ @Override
+ final void handle(ByteBuffer b,
+ BodyParser parser,
+ CompletableFuture<State> cf) {
+ assert cf != null : "parsing not started";
+ assert parser != null : "no parser";
+ try {
+ debug.log(Level.DEBUG, () -> "Sending " + b.remaining()
+ + "/" + b.capacity() + " bytes to body parser");
+ parser.accept(b);
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG,
+ () -> "Body parser failed to handle buffer: " + t);
+ if (!cf.isDone()) {
+ cf.completeExceptionally(t);
+ }
+ }
+ }
+
+ final void onComplete(Throwable closedExceptionally) {
+ if (cf.isDone()) return;
+ if (closedExceptionally != null) {
+ cf.completeExceptionally(closedExceptionally);
+ } else {
+ onComplete.accept(State.READING_BODY);
+ cf.complete(State.READING_BODY);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + "/parser=" + String.valueOf(parser);
+ }
+
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<String,Http2Connection> connections = new ConcurrentHashMap<>();
+
+ private final Set<String> failures = Collections.synchronizedSet(new HashSet<>());
+
+ /**
+ * When HTTP/2 requested only. The following describes the aggregate behavior including the
+ * calling code. In all cases, the HTTP2 connection cache
+ * is checked first for a suitable connection and that is returned if available.
+ * If not, a new connection is opened, except in https case when a previous negotiate failed.
+ * In that case, we want to continue using http/1.1. When a connection is to be opened and
+ * if multiple requests are sent in parallel then each will open a new connection.
+ *
+ * If negotiation/upgrade succeeds then
+ * one connection will be put in the cache and the others will be closed
+ * after the initial request completes (not strictly necessary for h2, only for h2c)
+ *
+ * If negotiate/upgrade fails, then any opened connections remain open (as http/1.1)
+ * and will be used and cached in the http/1 cache. Note, this method handles the
+ * https failure case only (by completing the CF with an ALPN exception, handled externally)
+ * The h2c upgrade is handled externally also.
+ *
+ * Specific CF behavior of this method.
+ * 1. completes with ALPN exception: h2 negotiate failed for first time. failure recorded.
+ * 2. completes with other exception: failure not recorded. Caller must handle
+ * 3. completes normally with null: no connection in cache for h2c or h2 failed previously
+ * 4. completes normally with connection: h2 or h2c connection in cache. Use it.
+ */
+ CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req) {
+ URI uri = req.uri();
+ InetSocketAddress proxy = req.proxy();
+ String key = Http2Connection.keyFor(uri, proxy);
+
+ synchronized (this) {
+ Http2Connection connection = connections.get(key);
+ if (connection != null) { // fast path if connection already exists
+ return MinimalFuture.completedFuture(connection);
+ }
+
+ if (!req.secure() || failures.contains(key)) {
+ // secure: negotiate failed before. Use http/1.1
+ // !secure: no connection available in cache. Attempt upgrade
+ return MinimalFuture.completedFuture(null);
+ }
+ }
+ return Http2Connection
+ .createAsync(req, this)
+ .whenComplete((conn, t) -> {
+ synchronized (Http2ClientImpl.this) {
+ if (conn != null) {
+ offerConnection(conn);
+ } else {
+ Throwable cause = Utils.getCompletionCause(t);
+ if (cause instanceof Http2Connection.ALPNException)
+ failures.add(key);
+ }
+ }
+ });
+ }
+
+ /*
+ * Cache the given connection, if no connection to the same
+ * destination exists. If one exists, then we let the initial stream
+ * complete but allow it to close itself upon completion.
+ * This situation should not arise with https because the request
+ * has not been sent as part of the initial alpn negotiation
+ */
+ boolean offerConnection(Http2Connection c) {
+ String key = c.key();
+ Http2Connection c1 = connections.putIfAbsent(key, c);
+ if (c1 != null) {
+ c.setSingleStream(true);
+ return false;
+ }
+ return true;
+ }
+
+ void deleteConnection(Http2Connection c) {
+ connections.remove(c.key());
+ }
+
+ void stop() {
+ debug.log(Level.DEBUG, "stopping");
+ connections.values().forEach(this::close);
+ connections.clear();
+ }
+
+ private void close(Http2Connection h2c) {
+ try { h2c.close(); } catch (Throwable t) {}
+ }
+
+ HttpClientImpl client() {
+ return client;
+ }
+
+ /** Returns the client settings as a base64 (url) encoded string */
+ String getSettingsString() {
+ SettingsFrame sf = getClientSettings();
+ byte[] settings = sf.toByteArray(); // without the header
+ Base64.Encoder encoder = Base64.getUrlEncoder()
+ .withoutPadding();
+ return encoder.encodeToString(settings);
+ }
+
+ private static final int K = 1024;
+
+ private static int getParameter(String property, int min, int max, int defaultValue) {
+ int value = Utils.getIntegerNetProperty(property, defaultValue);
+ // use default value if misconfigured
+ if (value < min || value > max) {
+ Log.logError("Property value for {0}={1} not in [{2}..{3}]: " +
+ "using default={4}", property, value, min, max, defaultValue);
+ value = defaultValue;
+ }
+ return value;
+ }
+
+ // used for the connection window, to have a connection window size
+ // bigger than the initial stream window size.
+ int getConnectionWindowSize(SettingsFrame clientSettings) {
+ // Maximum size is 2^31-1. Don't allow window size to be less
+ // than the stream window size. HTTP/2 specify a default of 64 * K -1,
+ // but we use 2^26 by default for better performance.
+ int streamWindow = clientSettings.getParameter(INITIAL_WINDOW_SIZE);
+
+ // The default is the max between the stream window size
+ // and the connection window size.
+ int defaultValue = Math.min(Integer.MAX_VALUE,
+ Math.max(streamWindow, K*K*32));
+
+ return getParameter(
+ "jdk.httpclient.connectionWindowSize",
+ streamWindow, Integer.MAX_VALUE, defaultValue);
+ }
+
+ SettingsFrame getClientSettings() {
+ SettingsFrame frame = new SettingsFrame();
+ // default defined for HTTP/2 is 4 K, we use 16 K.
+ frame.setParameter(HEADER_TABLE_SIZE, getParameter(
+ "jdk.httpclient.hpack.maxheadertablesize",
+ 0, Integer.MAX_VALUE, 16 * K));
+ // O: does not accept push streams. 1: accepts push streams.
+ frame.setParameter(ENABLE_PUSH, getParameter(
+ "jdk.httpclient.enablepush",
+ 0, 1, 1));
+ // HTTP/2 recommends to set the number of concurrent streams
+ // no lower than 100. We use 100. 0 means no stream would be
+ // accepted. That would render the client to be non functional,
+ // so we won't let 0 be configured for our Http2ClientImpl.
+ frame.setParameter(MAX_CONCURRENT_STREAMS, getParameter(
+ "jdk.httpclient.maxstreams",
+ 1, Integer.MAX_VALUE, 100));
+ // Maximum size is 2^31-1. Don't allow window size to be less
+ // than the minimum frame size as this is likely to be a
+ // configuration error. HTTP/2 specify a default of 64 * K -1,
+ // but we use 16 M for better performance.
+ frame.setParameter(INITIAL_WINDOW_SIZE, getParameter(
+ "jdk.httpclient.windowsize",
+ 16 * K, Integer.MAX_VALUE, 16*K*K));
+ // HTTP/2 specify a minimum size of 16 K, a maximum size of 2^24-1,
+ // and a default of 16 K. We use 16 K as default.
+ frame.setParameter(MAX_FRAME_SIZE, getParameter(
+ "jdk.httpclient.maxframesize",
+ 16 * K, 16 * K * K -1, 16 * K));
+ return frame;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<ByteBuffer> pending;
+
+ boolean processReceivedData(FramesDecoder decoder, ByteBuffer buf)
+ throws IOException
+ {
+ // if preface is not sent, buffers data in the pending list
+ if (!prefaceSent) {
+ debug.log(Level.DEBUG, "Preface is not sent: buffering %d",
+ buf.remaining());
+ synchronized (this) {
+ if (!prefaceSent) {
+ if (pending == null) pending = new ArrayList<>();
+ pending.add(buf);
+ debug.log(Level.DEBUG, () -> "there are now "
+ + Utils.remaining(pending)
+ + " bytes buffered waiting for preface to be sent");
+ return false;
+ }
+ }
+ }
+
+ // Preface is sent. Checks for pending data and flush it.
+ // We rely on this method being called from within the Http2TubeSubscriber
+ // scheduler, so we know that no other thread could execute this method
+ // concurrently while we're here.
+ // This ensures that later incoming buffers will not
+ // be processed before we have flushed the pending queue.
+ // No additional synchronization is therefore necessary here.
+ List<ByteBuffer> pending = this.pending;
+ this.pending = null;
+ if (pending != null) {
+ // flush pending data
+ debug.log(Level.DEBUG, () -> "Processing buffered data: "
+ + Utils.remaining(pending));
+ for (ByteBuffer b : pending) {
+ decoder.decode(b);
+ }
+ }
+ // push the received buffer to the frames decoder.
+ if (buf != EMPTY_TRIGGER) {
+ debug.log(Level.DEBUG, "Processing %d", buf.remaining());
+ decoder.decode(buf);
+ }
+ return true;
+ }
+
+ // Mark that the connection preface is sent
+ void markPrefaceSent() {
+ assert !prefaceSent;
+ synchronized (this) {
+ prefaceSent = true;
+ }
+ }
+ }
+
+ volatile boolean closed;
+
+ //-------------------------------------
+ final HttpConnection connection;
+ private final Http2ClientImpl client2;
+ private final Map<Integer,Stream<?>> streams = new ConcurrentHashMap<>();
+ private int nextstreamid;
+ private int nextPushStream = 2;
+ private final Encoder hpackOut;
+ private final Decoder hpackIn;
+ final SettingsFrame clientSettings;
+ private volatile SettingsFrame serverSettings;
+ private final String key; // for HttpClientImpl.connections map
+ private final FramesDecoder framesDecoder;
+ private final FramesEncoder framesEncoder = new FramesEncoder();
+
+ /**
+ * Send Window controller for both connection and stream windows.
+ * Each of this connection's Streams MUST use this controller.
+ */
+ private final WindowController windowController = new WindowController();
+ private final FramesController framesController = new FramesController();
+ private final Http2TubeSubscriber subscriber = new Http2TubeSubscriber();
+ final ConnectionWindowUpdateSender windowUpdater;
+ private volatile Throwable cause;
+ private volatile Supplier<ByteBuffer> initial;
+
+ static final int DEFAULT_FRAME_SIZE = 16 * 1024;
+
+
+ // TODO: need list of control frames from other threads
+ // that need to be sent
+
+ private Http2Connection(HttpConnection connection,
+ Http2ClientImpl client2,
+ int nextstreamid,
+ String key) {
+ this.connection = connection;
+ this.client2 = client2;
+ this.nextstreamid = nextstreamid;
+ this.key = key;
+ this.clientSettings = this.client2.getClientSettings();
+ this.framesDecoder = new FramesDecoder(this::processFrame,
+ clientSettings.getParameter(SettingsFrame.MAX_FRAME_SIZE));
+ // serverSettings will be updated by server
+ this.serverSettings = SettingsFrame.getDefaultSettings();
+ this.hpackOut = new Encoder(serverSettings.getParameter(HEADER_TABLE_SIZE));
+ this.hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE));
+ debugHpack.log(Level.DEBUG, () -> "For the record:" + super.toString());
+ debugHpack.log(Level.DEBUG, "Decoder created: %s", hpackIn);
+ debugHpack.log(Level.DEBUG, "Encoder created: %s", hpackOut);
+ this.windowUpdater = new ConnectionWindowUpdateSender(this,
+ client2.getConnectionWindowSize(clientSettings));
+ }
+
+ /**
+ * Case 1) Create from upgraded HTTP/1.1 connection.
+ * Is ready to use. Can be SSL. exchange is the Exchange
+ * that initiated the connection, whose response will be delivered
+ * on a Stream.
+ */
+ private Http2Connection(HttpConnection connection,
+ Http2ClientImpl client2,
+ Exchange<?> exchange,
+ Supplier<ByteBuffer> initial)
+ throws IOException, InterruptedException
+ {
+ this(connection,
+ client2,
+ 3, // stream 1 is registered during the upgrade
+ keyFor(connection));
+ Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
+
+ Stream<?> initialStream = createStream(exchange);
+ initialStream.registerStream(1);
+ windowController.registerStream(1, getInitialSendWindowSize());
+ initialStream.requestSent();
+ // Upgrading:
+ // set callbacks before sending preface - makes sure anything that
+ // might be sent by the server will come our way.
+ this.initial = initial;
+ connectFlows(connection);
+ sendConnectionPreface();
+ }
+
+ // Used when upgrading an HTTP/1.1 connection to HTTP/2 after receiving
+ // agreement from the server. Async style but completes immediately, because
+ // the connection is already connected.
+ static CompletableFuture<Http2Connection> createAsync(HttpConnection connection,
+ Http2ClientImpl client2,
+ Exchange<?> exchange,
+ Supplier<ByteBuffer> initial)
+ {
+ return MinimalFuture.supply(() -> new Http2Connection(connection, client2, exchange, initial));
+ }
+
+ // Requires TLS handshake. So, is really async
+ static CompletableFuture<Http2Connection> createAsync(HttpRequestImpl request,
+ Http2ClientImpl h2client) {
+ assert request.secure();
+ AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection)
+ HttpConnection.getConnection(request.getAddress(),
+ h2client.client(),
+ request,
+ HttpClient.Version.HTTP_2);
+
+ return connection.connectAsync()
+ .thenCompose(unused -> checkSSLConfig(connection))
+ .thenCompose(notused-> {
+ CompletableFuture<Http2Connection> cf = new MinimalFuture<>();
+ try {
+ Http2Connection hc = new Http2Connection(request, h2client, connection);
+ cf.complete(hc);
+ } catch (IOException e) {
+ cf.completeExceptionally(e);
+ }
+ return cf; } );
+ }
+
+ /**
+ * Cases 2) 3)
+ *
+ * request is request to be sent.
+ */
+ private Http2Connection(HttpRequestImpl request,
+ Http2ClientImpl h2client,
+ HttpConnection connection)
+ throws IOException
+ {
+ this(connection,
+ h2client,
+ 1,
+ keyFor(request.uri(), request.proxy()));
+
+ Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
+
+ // safe to resume async reading now.
+ connectFlows(connection);
+ sendConnectionPreface();
+ }
+
+ private void connectFlows(HttpConnection connection) {
+ FlowTube tube = connection.getConnectionFlow();
+ // Connect the flow to our Http2TubeSubscriber:
+ tube.connectFlows(connection.publisher(), subscriber);
+ }
+
+ final HttpClientImpl client() {
+ return client2.client();
+ }
+
+ /**
+ * Throws an IOException if h2 was not negotiated
+ */
+ private static CompletableFuture<?> checkSSLConfig(AbstractAsyncSSLConnection aconn) {
+ assert aconn.isSecure();
+
+ Function<String, CompletableFuture<Void>> checkAlpnCF = (alpn) -> {
+ CompletableFuture<Void> cf = new MinimalFuture<>();
+ SSLEngine engine = aconn.getEngine();
+ assert Objects.equals(alpn, engine.getApplicationProtocol());
+
+ DEBUG_LOGGER.log(Level.DEBUG, "checkSSLConfig: alpn: %s", alpn );
+
+ if (alpn == null || !alpn.equals("h2")) {
+ String msg;
+ if (alpn == null) {
+ Log.logSSL("ALPN not supported");
+ msg = "ALPN not supported";
+ } else {
+ switch (alpn) {
+ case "":
+ Log.logSSL(msg = "No ALPN negotiated");
+ break;
+ case "http/1.1":
+ Log.logSSL( msg = "HTTP/1.1 ALPN returned");
+ break;
+ default:
+ Log.logSSL(msg = "Unexpected ALPN: " + alpn);
+ cf.completeExceptionally(new IOException(msg));
+ }
+ }
+ cf.completeExceptionally(new ALPNException(msg, aconn));
+ return cf;
+ }
+ cf.complete(null);
+ return cf;
+ };
+
+ return aconn.getALPN()
+ .whenComplete((r,t) -> {
+ if (t != null && t instanceof SSLException) {
+ // something went wrong during the initial handshake
+ // close the connection
+ aconn.close();
+ }
+ })
+ .thenCompose(checkAlpnCF);
+ }
+
+ synchronized boolean singleStream() {
+ return singleStream;
+ }
+
+ synchronized void setSingleStream(boolean use) {
+ singleStream = use;
+ }
+
+ static String keyFor(HttpConnection connection) {
+ boolean isProxy = connection.isProxied();
+ boolean isSecure = connection.isSecure();
+ InetSocketAddress addr = connection.address();
+
+ return keyString(isSecure, isProxy, addr.getHostString(), addr.getPort());
+ }
+
+ static String keyFor(URI uri, InetSocketAddress proxy) {
+ boolean isSecure = uri.getScheme().equalsIgnoreCase("https");
+ boolean isProxy = proxy != null;
+
+ String host;
+ int port;
+
+ if (proxy != null) {
+ host = proxy.getHostString();
+ port = proxy.getPort();
+ } else {
+ host = uri.getHost();
+ port = uri.getPort();
+ }
+ return keyString(isSecure, isProxy, host, port);
+ }
+
+ // {C,S}:{H:P}:host:port
+ // C indicates clear text connection "http"
+ // S indicates secure "https"
+ // H indicates host (direct) connection
+ // P indicates proxy
+ // Eg: "S:H:foo.com:80"
+ static String keyString(boolean secure, boolean proxy, String host, int port) {
+ if (secure && port == -1)
+ port = 443;
+ else if (!secure && port == -1)
+ port = 80;
+ return (secure ? "S:" : "C:") + (proxy ? "P:" : "H:") + host + ":" + port;
+ }
+
+ String key() {
+ return this.key;
+ }
+
+ boolean offerConnection() {
+ return client2.offerConnection(this);
+ }
+
+ private HttpPublisher publisher() {
+ return connection.publisher();
+ }
+
+ private void decodeHeaders(HeaderFrame frame, DecodingCallback decoder)
+ throws IOException
+ {
+ debugHpack.log(Level.DEBUG, "decodeHeaders(%s)", decoder);
+
+ boolean endOfHeaders = frame.getFlag(HeaderFrame.END_HEADERS);
+
+ List<ByteBuffer> buffers = frame.getHeaderBlock();
+ int len = buffers.size();
+ for (int i = 0; i < len; i++) {
+ ByteBuffer b = buffers.get(i);
+ hpackIn.decode(b, endOfHeaders && (i == len - 1), decoder);
+ }
+ }
+
+ final int getInitialSendWindowSize() {
+ return serverSettings.getParameter(INITIAL_WINDOW_SIZE);
+ }
+
+ void close() {
+ Log.logTrace("Closing HTTP/2 connection: to {0}", connection.address());
+ GoAwayFrame f = new GoAwayFrame(0,
+ ErrorFrame.NO_ERROR,
+ "Requested by user".getBytes(UTF_8));
+ // TODO: set last stream. For now zero ok.
+ sendFrame(f);
+ }
+
+ long count;
+ final void asyncReceive(ByteBuffer buffer) {
+ // We don't need to read anything and
+ // we don't want to send anything back to the server
+ // until the connection preface has been sent.
+ // Therefore we're going to wait if needed before reading
+ // (and thus replying) to anything.
+ // Starting to reply to something (e.g send an ACK to a
+ // SettingsFrame sent by the server) before the connection
+ // preface is fully sent might result in the server
+ // sending a GOAWAY frame with 'invalid_preface'.
+ //
+ // Note: asyncReceive is only called from the Http2TubeSubscriber
+ // sequential scheduler.
+ try {
+ Supplier<ByteBuffer> bs = initial;
+ // ensure that we always handle the initial buffer first,
+ // if any.
+ if (bs != null) {
+ initial = null;
+ ByteBuffer b = bs.get();
+ if (b.hasRemaining()) {
+ long c = ++count;
+ debug.log(Level.DEBUG, () -> "H2 Receiving Initial("
+ + c +"): " + b.remaining());
+ framesController.processReceivedData(framesDecoder, b);
+ }
+ }
+ ByteBuffer b = buffer;
+ // the Http2TubeSubscriber scheduler ensures that the order of incoming
+ // buffers is preserved.
+ if (b == EMPTY_TRIGGER) {
+ debug.log(Level.DEBUG, "H2 Received EMPTY_TRIGGER");
+ boolean prefaceSent = framesController.prefaceSent;
+ assert prefaceSent;
+ // call framesController.processReceivedData to potentially
+ // trigger the processing of all the data buffered there.
+ framesController.processReceivedData(framesDecoder, buffer);
+ debug.log(Level.DEBUG, "H2 processed buffered data");
+ } else {
+ long c = ++count;
+ debug.log(Level.DEBUG, "H2 Receiving(%d): %d", c, b.remaining());
+ framesController.processReceivedData(framesDecoder, buffer);
+ debug.log(Level.DEBUG, "H2 processed(%d)", c);
+ }
+ } catch (Throwable e) {
+ String msg = Utils.stackTrace(e);
+ Log.logTrace(msg);
+ shutdown(e);
+ }
+ }
+
+ Throwable getRecordedCause() {
+ return cause;
+ }
+
+ void shutdown(Throwable t) {
+ debug.log(Level.DEBUG, () -> "Shutting down h2c (closed="+closed+"): " + t);
+ if (closed == true) return;
+ synchronized (this) {
+ if (closed == true) return;
+ closed = true;
+ }
+ Log.logError(t);
+ Throwable initialCause = this.cause;
+ if (initialCause == null) this.cause = t;
+ client2.deleteConnection(this);
+ List<Stream<?>> c = new LinkedList<>(streams.values());
+ for (Stream<?> s : c) {
+ s.cancelImpl(t);
+ }
+ connection.close();
+ }
+
+ /**
+ * Streams initiated by a client MUST use odd-numbered stream
+ * identifiers; those initiated by the server MUST use even-numbered
+ * stream identifiers.
+ */
+ private static final boolean isSeverInitiatedStream(int streamid) {
+ return (streamid & 0x1) == 0;
+ }
+
+ /**
+ * Handles stream 0 (common) frames that apply to whole connection and passes
+ * other stream specific frames to that Stream object.
+ *
+ * Invokes Stream.incoming() which is expected to process frame without
+ * blocking.
+ */
+ void processFrame(Http2Frame frame) throws IOException {
+ Log.logFrames(frame, "IN");
+ int streamid = frame.streamid();
+ if (frame instanceof MalformedFrame) {
+ Log.logError(((MalformedFrame) frame).getMessage());
+ if (streamid == 0) {
+ framesDecoder.close("Malformed frame on stream 0");
+ protocolError(((MalformedFrame) frame).getErrorCode(),
+ ((MalformedFrame) frame).getMessage());
+ } else {
+ debug.log(Level.DEBUG, () -> "Reset stream: "
+ + ((MalformedFrame) frame).getMessage());
+ resetStream(streamid, ((MalformedFrame) frame).getErrorCode());
+ }
+ return;
+ }
+ if (streamid == 0) {
+ handleConnectionFrame(frame);
+ } else {
+ if (frame instanceof SettingsFrame) {
+ // The stream identifier for a SETTINGS frame MUST be zero
+ framesDecoder.close(
+ "The stream identifier for a SETTINGS frame MUST be zero");
+ protocolError(GoAwayFrame.PROTOCOL_ERROR);
+ return;
+ }
+
+ Stream<?> stream = getStream(streamid);
+ if (stream == null) {
+ // Should never receive a frame with unknown stream id
+
+ if (frame instanceof HeaderFrame) {
+ // always decode the headers as they may affect
+ // connection-level HPACK decoding state
+ HeaderDecoder decoder = new LoggingHeaderDecoder(new HeaderDecoder());
+ decodeHeaders((HeaderFrame) frame, decoder);
+ }
+
+ if (!(frame instanceof ResetFrame)) {
+ if (isSeverInitiatedStream(streamid)) {
+ if (streamid < nextPushStream) {
+ // trailing data on a cancelled push promise stream,
+ // reset will already have been sent, ignore
+ Log.logTrace("Ignoring cancelled push promise frame " + frame);
+ } else {
+ resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
+ }
+ } else if (streamid >= nextstreamid) {
+ // otherwise the stream has already been reset/closed
+ resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
+ }
+ }
+ return;
+ }
+ if (frame instanceof PushPromiseFrame) {
+ PushPromiseFrame pp = (PushPromiseFrame)frame;
+ handlePushPromise(stream, pp);
+ } else if (frame instanceof HeaderFrame) {
+ // decode headers (or continuation)
+ decodeHeaders((HeaderFrame) frame, stream.rspHeadersConsumer());
+ stream.incoming(frame);
+ } else {
+ stream.incoming(frame);
+ }
+ }
+ }
+
+ private <T> void handlePushPromise(Stream<T> parent, PushPromiseFrame pp)
+ throws IOException
+ {
+ // always decode the headers as they may affect connection-level HPACK
+ // decoding state
+ HeaderDecoder decoder = new LoggingHeaderDecoder(new HeaderDecoder());
+ decodeHeaders(pp, decoder);
+
+ HttpRequestImpl parentReq = parent.request;
+ int promisedStreamid = pp.getPromisedStream();
+ if (promisedStreamid != nextPushStream) {
+ resetStream(promisedStreamid, ResetFrame.PROTOCOL_ERROR);
+ return;
+ } else {
+ nextPushStream += 2;
+ }
+
+ HttpHeadersImpl headers = decoder.headers();
+ HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest(parentReq, headers);
+ Exchange<T> pushExch = new Exchange<>(pushReq, parent.exchange.multi);
+ Stream.PushedStream<T> pushStream = createPushStream(parent, pushExch);
+ pushExch.exchImpl = pushStream;
+ pushStream.registerStream(promisedStreamid);
+ parent.incoming_pushPromise(pushReq, pushStream);
+ }
+
+ private void handleConnectionFrame(Http2Frame frame)
+ throws IOException
+ {
+ switch (frame.type()) {
+ case SettingsFrame.TYPE:
+ handleSettings((SettingsFrame)frame);
+ break;
+ case PingFrame.TYPE:
+ handlePing((PingFrame)frame);
+ break;
+ case GoAwayFrame.TYPE:
+ handleGoAway((GoAwayFrame)frame);
+ break;
+ case WindowUpdateFrame.TYPE:
+ handleWindowUpdate((WindowUpdateFrame)frame);
+ break;
+ default:
+ protocolError(ErrorFrame.PROTOCOL_ERROR);
+ }
+ }
+
+ void resetStream(int streamid, int code) throws IOException {
+ Log.logError(
+ "Resetting stream {0,number,integer} with error code {1,number,integer}",
+ streamid, code);
+ ResetFrame frame = new ResetFrame(streamid, code);
+ sendFrame(frame);
+ closeStream(streamid);
+ }
+
+ void closeStream(int streamid) {
+ debug.log(Level.DEBUG, "Closed stream %d", streamid);
+ Stream<?> s = streams.remove(streamid);
+ if (s != null) {
+ // decrement the reference count on the HttpClientImpl
+ // to allow the SelectorManager thread to exit if no
+ // other operation is pending and the facade is no
+ // longer referenced.
+ client().unreference();
+ }
+ // ## Remove s != null. It is a hack for delayed cancellation,reset
+ if (s != null && !(s instanceof Stream.PushedStream)) {
+ // Since PushStreams have no request body, then they have no
+ // corresponding entry in the window controller.
+ windowController.removeStream(streamid);
+ }
+ if (singleStream() && streams.isEmpty()) {
+ // should be only 1 stream, but there might be more if server push
+ close();
+ }
+ }
+
+ /**
+ * Increments this connection's send Window by the amount in the given frame.
+ */
+ private void handleWindowUpdate(WindowUpdateFrame f)
+ throws IOException
+ {
+ int amount = f.getUpdate();
+ if (amount <= 0) {
+ // ## temporarily disable to workaround a bug in Jetty where it
+ // ## sends Window updates with a 0 update value.
+ //protocolError(ErrorFrame.PROTOCOL_ERROR);
+ } else {
+ boolean success = windowController.increaseConnectionWindow(amount);
+ if (!success) {
+ protocolError(ErrorFrame.FLOW_CONTROL_ERROR); // overflow
+ }
+ }
+ }
+
+ private void protocolError(int errorCode)
+ throws IOException
+ {
+ protocolError(errorCode, null);
+ }
+
+ private void protocolError(int errorCode, String msg)
+ throws IOException
+ {
+ GoAwayFrame frame = new GoAwayFrame(0, errorCode);
+ sendFrame(frame);
+ shutdown(new IOException("protocol error" + (msg == null?"":(": " + msg))));
+ }
+
+ private void handleSettings(SettingsFrame frame)
+ throws IOException
+ {
+ assert frame.streamid() == 0;
+ if (!frame.getFlag(SettingsFrame.ACK)) {
+ int oldWindowSize = serverSettings.getParameter(INITIAL_WINDOW_SIZE);
+ int newWindowSize = frame.getParameter(INITIAL_WINDOW_SIZE);
+ int diff = newWindowSize - oldWindowSize;
+ if (diff != 0) {
+ windowController.adjustActiveStreams(diff);
+ }
+ serverSettings = frame;
+ sendFrame(new SettingsFrame(SettingsFrame.ACK));
+ }
+ }
+
+ private void handlePing(PingFrame frame)
+ throws IOException
+ {
+ frame.setFlag(PingFrame.ACK);
+ sendUnorderedFrame(frame);
+ }
+
+ private void handleGoAway(GoAwayFrame frame)
+ throws IOException
+ {
+ shutdown(new IOException(
+ String.valueOf(connection.channel().getLocalAddress())
+ +": GOAWAY received"));
+ }
+
+ /**
+ * Max frame size we are allowed to send
+ */
+ public int getMaxSendFrameSize() {
+ int param = serverSettings.getParameter(MAX_FRAME_SIZE);
+ if (param == -1) {
+ param = DEFAULT_FRAME_SIZE;
+ }
+ return param;
+ }
+
+ /**
+ * Max frame size we will receive
+ */
+ public int getMaxReceiveFrameSize() {
+ return clientSettings.getParameter(MAX_FRAME_SIZE);
+ }
+
+ private static final String CLIENT_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
+
+ private static final byte[] PREFACE_BYTES =
+ CLIENT_PREFACE.getBytes(StandardCharsets.ISO_8859_1);
+
+ /**
+ * Sends Connection preface and Settings frame with current preferred
+ * values
+ */
+ private void sendConnectionPreface() throws IOException {
+ Log.logTrace("{0}: start sending connection preface to {1}",
+ connection.channel().getLocalAddress(),
+ connection.address());
+ SettingsFrame sf = new SettingsFrame(clientSettings);
+ int initialWindowSize = sf.getParameter(INITIAL_WINDOW_SIZE);
+ ByteBuffer buf = framesEncoder.encodeConnectionPreface(PREFACE_BYTES, sf);
+ Log.logFrames(sf, "OUT");
+ // send preface bytes and SettingsFrame together
+ HttpPublisher publisher = publisher();
+ publisher.enqueue(List.of(buf));
+ publisher.signalEnqueued();
+ // mark preface sent.
+ framesController.markPrefaceSent();
+ Log.logTrace("PREFACE_BYTES sent");
+ Log.logTrace("Settings Frame sent");
+
+ // send a Window update for the receive buffer we are using
+ // minus the initial 64 K specified in protocol
+ final int len = windowUpdater.initialWindowSize - initialWindowSize;
+ if (len > 0) {
+ windowUpdater.sendWindowUpdate(len);
+ }
+ // there will be an ACK to the windows update - which should
+ // cause any pending data stored before the preface was sent to be
+ // flushed (see PrefaceController).
+ Log.logTrace("finished sending connection preface");
+ debug.log(Level.DEBUG, "Triggering processing of buffered data"
+ + " after sending connection preface");
+ subscriber.onNext(List.of(EMPTY_TRIGGER));
+ }
+
+ /**
+ * Returns an existing Stream with given id, or null if doesn't exist
+ */
+ @SuppressWarnings("unchecked")
+ <T> Stream<T> getStream(int streamid) {
+ return (Stream<T>)streams.get(streamid);
+ }
+
+ /**
+ * Creates Stream with given id.
+ */
+ final <T> Stream<T> createStream(Exchange<T> exchange) {
+ Stream<T> stream = new Stream<>(this, exchange, windowController);
+ return stream;
+ }
+
+ <T> Stream.PushedStream<T> createPushStream(Stream<T> parent, Exchange<T> pushEx) {
+ PushGroup<T> pg = parent.exchange.getPushGroup();
+ return new Stream.PushedStream<>(pg, this, pushEx);
+ }
+
+ <T> void putStream(Stream<T> stream, int streamid) {
+ // increment the reference count on the HttpClientImpl
+ // to prevent the SelectorManager thread from exiting until
+ // the stream is closed.
+ client().reference();
+ streams.put(streamid, stream);
+ }
+
+ /**
+ * Encode the headers into a List<ByteBuffer> and then create HEADERS
+ * and CONTINUATION frames from the list and return the List<Http2Frame>.
+ */
+ private List<HeaderFrame> encodeHeaders(OutgoingHeaders<Stream<?>> frame) {
+ List<ByteBuffer> buffers = encodeHeadersImpl(
+ getMaxSendFrameSize(),
+ frame.getAttachment().getRequestPseudoHeaders(),
+ frame.getUserHeaders(),
+ frame.getSystemHeaders());
+
+ List<HeaderFrame> frames = new ArrayList<>(buffers.size());
+ Iterator<ByteBuffer> bufIterator = buffers.iterator();
+ HeaderFrame oframe = new HeadersFrame(frame.streamid(), frame.getFlags(), bufIterator.next());
+ frames.add(oframe);
+ while(bufIterator.hasNext()) {
+ oframe = new ContinuationFrame(frame.streamid(), bufIterator.next());
+ frames.add(oframe);
+ }
+ oframe.setFlag(HeaderFrame.END_HEADERS);
+ return frames;
+ }
+
+ // Dedicated cache for headers encoding ByteBuffer.
+ // There can be no concurrent access to this buffer as all access to this buffer
+ // and its content happen within a single critical code block section protected
+ // by the sendLock. / (see sendFrame())
+ // private final ByteBufferPool headerEncodingPool = new ByteBufferPool();
+
+ private ByteBuffer getHeaderBuffer(int maxFrameSize) {
+ ByteBuffer buf = ByteBuffer.allocate(maxFrameSize);
+ buf.limit(maxFrameSize);
+ return buf;
+ }
+
+ /*
+ * Encodes all the headers from the given HttpHeaders into the given List
+ * of buffers.
+ *
+ * From https://tools.ietf.org/html/rfc7540#section-8.1.2 :
+ *
+ * ...Just as in HTTP/1.x, header field names are strings of ASCII
+ * characters that are compared in a case-insensitive fashion. However,
+ * header field names MUST be converted to lowercase prior to their
+ * encoding in HTTP/2...
+ */
+ private List<ByteBuffer> encodeHeadersImpl(int maxFrameSize, HttpHeaders... headers) {
+ ByteBuffer buffer = getHeaderBuffer(maxFrameSize);
+ List<ByteBuffer> buffers = new ArrayList<>();
+ for(HttpHeaders header : headers) {
+ for (Map.Entry<String, List<String>> e : header.map().entrySet()) {
+ String lKey = e.getKey().toLowerCase();
+ List<String> values = e.getValue();
+ for (String value : values) {
+ hpackOut.header(lKey, value);
+ while (!hpackOut.encode(buffer)) {
+ buffer.flip();
+ buffers.add(buffer);
+ buffer = getHeaderBuffer(maxFrameSize);
+ }
+ }
+ }
+ }
+ buffer.flip();
+ buffers.add(buffer);
+ return buffers;
+ }
+
+ private List<ByteBuffer> encodeHeaders(OutgoingHeaders<Stream<?>> oh, Stream<?> stream) {
+ oh.streamid(stream.streamid);
+ if (Log.headers()) {
+ StringBuilder sb = new StringBuilder("HEADERS FRAME (stream=");
+ sb.append(stream.streamid).append(")\n");
+ Log.dumpHeaders(sb, " ", oh.getAttachment().getRequestPseudoHeaders());
+ Log.dumpHeaders(sb, " ", oh.getSystemHeaders());
+ Log.dumpHeaders(sb, " ", oh.getUserHeaders());
+ Log.logHeaders(sb.toString());
+ }
+ List<HeaderFrame> frames = encodeHeaders(oh);
+ return encodeFrames(frames);
+ }
+
+ private List<ByteBuffer> encodeFrames(List<HeaderFrame> frames) {
+ if (Log.frames()) {
+ frames.forEach(f -> Log.logFrames(f, "OUT"));
+ }
+ return framesEncoder.encodeFrames(frames);
+ }
+
+ private Stream<?> registerNewStream(OutgoingHeaders<Stream<?>> oh) {
+ Stream<?> stream = oh.getAttachment();
+ int streamid = nextstreamid;
+ nextstreamid += 2;
+ stream.registerStream(streamid);
+ // set outgoing window here. This allows thread sending
+ // body to proceed.
+ windowController.registerStream(streamid, getInitialSendWindowSize());
+ return stream;
+ }
+
+ private final Object sendlock = new Object();
+
+ void sendFrame(Http2Frame frame) {
+ try {
+ HttpPublisher publisher = publisher();
+ synchronized (sendlock) {
+ if (frame instanceof OutgoingHeaders) {
+ @SuppressWarnings("unchecked")
+ OutgoingHeaders<Stream<?>> oh = (OutgoingHeaders<Stream<?>>) frame;
+ Stream<?> stream = registerNewStream(oh);
+ // provide protection from inserting unordered frames between Headers and Continuation
+ publisher.enqueue(encodeHeaders(oh, stream));
+ } else {
+ publisher.enqueue(encodeFrame(frame));
+ }
+ }
+ publisher.signalEnqueued();
+ } catch (IOException e) {
+ if (!closed) {
+ Log.logError(e);
+ shutdown(e);
+ }
+ }
+ }
+
+ private List<ByteBuffer> encodeFrame(Http2Frame frame) {
+ Log.logFrames(frame, "OUT");
+ return framesEncoder.encodeFrame(frame);
+ }
+
+ void sendDataFrame(DataFrame frame) {
+ try {
+ HttpPublisher publisher = publisher();
+ publisher.enqueue(encodeFrame(frame));
+ publisher.signalEnqueued();
+ } catch (IOException e) {
+ if (!closed) {
+ Log.logError(e);
+ shutdown(e);
+ }
+ }
+ }
+
+ /*
+ * Direct call of the method bypasses synchronization on "sendlock" and
+ * allowed only of control frames: WindowUpdateFrame, PingFrame and etc.
+ * prohibited for such frames as DataFrame, HeadersFrame, ContinuationFrame.
+ */
+ void sendUnorderedFrame(Http2Frame frame) {
+ try {
+ HttpPublisher publisher = publisher();
+ publisher.enqueueUnordered(encodeFrame(frame));
+ publisher.signalEnqueued();
+ } catch (IOException e) {
+ if (!closed) {
+ Log.logError(e);
+ shutdown(e);
+ }
+ }
+ }
+
+ /**
+ * A simple tube subscriber for reading from the connection flow.
+ */
+ final class Http2TubeSubscriber implements TubeSubscriber {
+ volatile Flow.Subscription subscription;
+ volatile boolean completed;
+ volatile boolean dropped;
+ volatile Throwable error;
+ final ConcurrentLinkedQueue<ByteBuffer> queue
+ = new ConcurrentLinkedQueue<>();
+ final SequentialScheduler scheduler =
+ SequentialScheduler.synchronizedScheduler(this::processQueue);
+
+ final void processQueue() {
+ try {
+ while (!queue.isEmpty() && !scheduler.isStopped()) {
+ ByteBuffer buffer = queue.poll();
+ debug.log(Level.DEBUG,
+ "sending %d to Http2Connection.asyncReceive",
+ buffer.remaining());
+ asyncReceive(buffer);
+ }
+ } catch (Throwable t) {
+ Throwable x = error;
+ if (x == null) error = t;
+ } finally {
+ Throwable x = error;
+ if (x != null) {
+ debug.log(Level.DEBUG, "Stopping scheduler", x);
+ scheduler.stop();
+ Http2Connection.this.shutdown(x);
+ }
+ }
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ // supports being called multiple time.
+ // doesn't cancel the previous subscription, since that is
+ // most probably the same as the new subscription.
+ assert this.subscription == null || dropped == false;
+ this.subscription = subscription;
+ dropped = false;
+ // TODO FIXME: request(1) should be done by the delegate.
+ if (!completed) {
+ debug.log(Level.DEBUG, "onSubscribe: requesting Long.MAX_VALUE for reading");
+ subscription.request(Long.MAX_VALUE);
+ } else {
+ debug.log(Level.DEBUG, "onSubscribe: already completed");
+ }
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ debug.log(Level.DEBUG, () -> "onNext: got " + Utils.remaining(item)
+ + " bytes in " + item.size() + " buffers");
+ queue.addAll(item);
+ scheduler.runOrSchedule(client().theExecutor());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ debug.log(Level.DEBUG, () -> "onError: " + throwable);
+ error = throwable;
+ completed = true;
+ scheduler.runOrSchedule(client().theExecutor());
+ }
+
+ @Override
+ public void onComplete() {
+ debug.log(Level.DEBUG, "EOF");
+ error = new EOFException("EOF reached while reading");
+ completed = true;
+ scheduler.runOrSchedule(client().theExecutor());
+ }
+
+ @Override
+ public void dropSubscription() {
+ debug.log(Level.DEBUG, "dropSubscription");
+ // we could probably set subscription to null here...
+ // then we might not need the 'dropped' boolean?
+ dropped = true;
+ }
+ }
+
+ @Override
+ public final String toString() {
+ return dbgString();
+ }
+
+ final String dbgString() {
+ return "Http2Connection("
+ + connection.getConnectionFlow() + ")";
+ }
+
+ final class LoggingHeaderDecoder extends HeaderDecoder {
+
+ private final HeaderDecoder delegate;
+ private final System.Logger debugHpack =
+ Utils.getHpackLogger(this::dbgString, DEBUG_HPACK);
+
+ LoggingHeaderDecoder(HeaderDecoder delegate) {
+ this.delegate = delegate;
+ }
+
+ String dbgString() {
+ return Http2Connection.this.dbgString() + "/LoggingHeaderDecoder";
+ }
+
+ @Override
+ public void onDecoded(CharSequence name, CharSequence value) {
+ delegate.onDecoded(name, value);
+ }
+
+ @Override
+ public void onIndexed(int index,
+ CharSequence name,
+ CharSequence value) {
+ debugHpack.log(Level.DEBUG, "onIndexed(%s, %s, %s)%n",
+ index, name, value);
+ delegate.onIndexed(index, name, value);
+ }
+
+ @Override
+ public void onLiteral(int index,
+ CharSequence name,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteral(%s, %s, %s, %s)%n",
+ index, name, value, valueHuffman);
+ delegate.onLiteral(index, name, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteral(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteral(%s, %s, %s, %s)%n",
+ name, nameHuffman, value, valueHuffman);
+ delegate.onLiteral(name, nameHuffman, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteralNeverIndexed(int index,
+ CharSequence name,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteralNeverIndexed(%s, %s, %s, %s)%n",
+ index, name, value, valueHuffman);
+ delegate.onLiteralNeverIndexed(index, name, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteralNeverIndexed(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteralNeverIndexed(%s, %s, %s, %s)%n",
+ name, nameHuffman, value, valueHuffman);
+ delegate.onLiteralNeverIndexed(name, nameHuffman, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteralWithIndexing(int index,
+ CharSequence name,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteralWithIndexing(%s, %s, %s, %s)%n",
+ index, name, value, valueHuffman);
+ delegate.onLiteralWithIndexing(index, name, value, valueHuffman);
+ }
+
+ @Override
+ public void onLiteralWithIndexing(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
+ debugHpack.log(Level.DEBUG, "onLiteralWithIndexing(%s, %s, %s, %s)%n",
+ name, nameHuffman, value, valueHuffman);
+ delegate.onLiteralWithIndexing(name, nameHuffman, value, valueHuffman);
+ }
+
+ @Override
+ public void onSizeUpdate(int capacity) {
+ debugHpack.log(Level.DEBUG, "onSizeUpdate(%s)%n", capacity);
+ delegate.onSizeUpdate(capacity);
+ }
+
+ @Override
+ HttpHeadersImpl headers() {
+ return delegate.headers();
+ }
+ }
+
+ static class HeaderDecoder implements DecodingCallback {
+ HttpHeadersImpl headers;
+
+ HeaderDecoder() {
+ this.headers = new HttpHeadersImpl();
+ }
+
+ @Override
+ public void onDecoded(CharSequence name, CharSequence value) {
+ headers.addHeader(name.toString(), value.toString());
+ }
+
+ HttpHeadersImpl headers() {
+ return headers;
+ }
+ }
+
+ static final class ConnectionWindowUpdateSender extends WindowUpdateSender {
+
+ final int initialWindowSize;
+ public ConnectionWindowUpdateSender(Http2Connection connection,
+ int initialWindowSize) {
+ super(connection, initialWindowSize);
+ this.initialWindowSize = initialWindowSize;
+ }
+
+ @Override
+ int getStreamId() {
+ return 0;
+ }
+ }
+
+ /**
+ * Thrown when https handshake negotiates http/1.1 alpn instead of h2
+ */
+ static final class ALPNException extends IOException {
+ private static final long serialVersionUID = 0L;
+ final transient AbstractAsyncSSLConnection connection;
+
+ ALPNException(String msg, AbstractAsyncSSLConnection connection) {
+ super(msg);
+ this.connection = connection;
+ }
+
+ AbstractAsyncSSLConnection getConnection() {
+ return connection;
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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);
+ }
+}
--- /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> cookieHandler() {
+ return impl.cookieHandler();
+ }
+
+ @Override
+ public Redirect followRedirects() {
+ return impl.followRedirects();
+ }
+
+ @Override
+ public Optional<ProxySelector> proxy() {
+ return impl.proxy();
+ }
+
+ @Override
+ public SSLContext sslContext() {
+ return impl.sslContext();
+ }
+
+ @Override
+ public SSLParameters sslParameters() {
+ return impl.sslParameters();
+ }
+
+ @Override
+ public Optional<Authenticator> authenticator() {
+ return impl.authenticator();
+ }
+
+ @Override
+ public HttpClient.Version version() {
+ return impl.version();
+ }
+
+ @Override
+ public Optional<Executor> executor() {
+ return impl.executor();
+ }
+
+ @Override
+ public <T> HttpResponse<T>
+ send(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)
+ throws IOException, InterruptedException
+ {
+ try {
+ return impl.send(req, responseBodyHandler);
+ } finally {
+ Reference.reachabilityFence(this);
+ }
+ }
+
+ @Override
+ public <T> CompletableFuture<HttpResponse<T>>
+ sendAsync(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler) {
+ try {
+ return impl.sendAsync(req, responseBodyHandler);
+ } finally {
+ Reference.reachabilityFence(this);
+ }
+ }
+
+ @Override
+ public <T> CompletableFuture<HttpResponse<T>>
+ sendAsync(HttpRequest req,
+ BodyHandler<T> responseBodyHandler,
+ PushPromiseHandler<T> pushPromiseHandler){
+ try {
+ return impl.sendAsync(req, responseBodyHandler, pushPromiseHandler);
+ } finally {
+ Reference.reachabilityFence(this);
+ }
+ }
+
+ @Override
+ public WebSocket.Builder newWebSocketBuilder() {
+ try {
+ return impl.newWebSocketBuilder();
+ } finally {
+ Reference.reachabilityFence(this);
+ }
+ }
+
+ @Override
+ public String toString() {
+ // Used by tests to get the client's id.
+ return impl.toString();
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<ProxySelector> userProxySelector;
+ private final ProxySelector proxySelector;
+ private final Authenticator authenticator;
+ private final Version version;
+ private final ConnectionPool connections;
+ private final Executor executor;
+ private final boolean isDefaultExecutor;
+ // Security parameters
+ private final SSLContext sslContext;
+ private final SSLParameters sslParams;
+ private final SelectorManager selmgr;
+ private final FilterFactory filters;
+ private final Http2ClientImpl client2;
+ private final long id;
+ private final String dbgTag;
+
+ // This reference is used to keep track of the facade HttpClient
+ // that was returned to the application code.
+ // It makes it possible to know when the application no longer
+ // holds any reference to the HttpClient.
+ // Unfortunately, this information is not enough to know when
+ // to exit the SelectorManager thread. Because of the asynchronous
+ // nature of the API, we also need to wait until all pending operations
+ // have completed.
+ private final WeakReference<HttpClientFacade> facadeRef;
+
+ // This counter keeps track of the number of operations pending
+ // on the HttpClient. The SelectorManager thread will wait
+ // until there are no longer any pending operations and the
+ // facadeRef is cleared before exiting.
+ //
+ // The pendingOperationCount is incremented every time a send/sendAsync
+ // operation is invoked on the HttpClient, and is decremented when
+ // the HttpResponse<T> object is returned to the user.
+ // However, at this point, the body may not have been fully read yet.
+ // This is the case when the response T is implemented as a streaming
+ // subscriber (such as an InputStream).
+ //
+ // To take care of this issue the pendingOperationCount will additionally
+ // be incremented/decremented in the following cases:
+ //
+ // 1. For HTTP/2 it is incremented when a stream is added to the
+ // Http2Connection streams map, and decreased when the stream is removed
+ // from the map. This should also take care of push promises.
+ // 2. For WebSocket the count is increased when creating a
+ // DetachedConnectionChannel for the socket, and decreased
+ // when the the channel is closed.
+ // In addition, the HttpClient facade is passed to the WebSocket builder,
+ // (instead of the client implementation delegate).
+ // 3. For HTTP/1.1 the count is incremented before starting to parse the body
+ // response, and decremented when the parser has reached the end of the
+ // response body flow.
+ //
+ // This should ensure that the selector manager thread remains alive until
+ // the response has been fully received or the web socket is closed.
+ private final AtomicLong pendingOperationCount = new AtomicLong();
+ private final AtomicLong pendingWebSocketCount = new AtomicLong();
+ private final AtomicLong pendingHttpRequestCount = new AtomicLong();
+
+ /** A Set of, deadline first, ordered timeout events. */
+ private final TreeSet<TimeoutEvent> timeouts;
+
+ /**
+ * This is a bit tricky:
+ * 1. an HttpClientFacade has a final HttpClientImpl field.
+ * 2. an HttpClientImpl has a final WeakReference<HttpClientFacade> field,
+ * where the referent is the facade created for that instance.
+ * 3. We cannot just create the HttpClientFacade in the HttpClientImpl
+ * constructor, because it would be only weakly referenced and could
+ * be GC'ed before we can return it.
+ * The solution is to use an instance of SingleFacadeFactory which will
+ * allow the caller of new HttpClientImpl(...) to retrieve the facade
+ * after the HttpClientImpl has been created.
+ */
+ private static final class SingleFacadeFactory {
+ HttpClientFacade facade;
+ HttpClientFacade createFacade(HttpClientImpl impl) {
+ assert facade == null;
+ return (facade = new HttpClientFacade(impl));
+ }
+ }
+
+ static HttpClientFacade create(HttpClientBuilderImpl builder) {
+ SingleFacadeFactory facadeFactory = new SingleFacadeFactory();
+ HttpClientImpl impl = new HttpClientImpl(builder, facadeFactory);
+ impl.start();
+ assert facadeFactory.facade != null;
+ assert impl.facadeRef.get() == facadeFactory.facade;
+ return facadeFactory.facade;
+ }
+
+ private HttpClientImpl(HttpClientBuilderImpl builder,
+ SingleFacadeFactory facadeFactory) {
+ id = CLIENT_IDS.incrementAndGet();
+ dbgTag = "HttpClientImpl(" + id +")";
+ if (builder.sslContext == null) {
+ try {
+ sslContext = SSLContext.getDefault();
+ } catch (NoSuchAlgorithmException ex) {
+ throw new InternalError(ex);
+ }
+ } else {
+ sslContext = builder.sslContext;
+ }
+ Executor ex = builder.executor;
+ if (ex == null) {
+ ex = Executors.newCachedThreadPool(new DefaultThreadFactory(id));
+ isDefaultExecutor = true;
+ } else {
+ ex = builder.executor;
+ isDefaultExecutor = false;
+ }
+ facadeRef = new WeakReference<>(facadeFactory.createFacade(this));
+ client2 = new Http2ClientImpl(this);
+ executor = ex;
+ cookieHandler = builder.cookieHandler;
+ followRedirects = builder.followRedirects == null ?
+ Redirect.NEVER : builder.followRedirects;
+ this.userProxySelector = Optional.ofNullable(builder.proxy);
+ this.proxySelector = userProxySelector
+ .orElseGet(HttpClientImpl::getDefaultProxySelector);
+ debug.log(Level.DEBUG, "proxySelector is %s (user-supplied=%s)",
+ this.proxySelector, userProxySelector.isPresent());
+ authenticator = builder.authenticator;
+ if (builder.version == null) {
+ version = HttpClient.Version.HTTP_2;
+ } else {
+ version = builder.version;
+ }
+ if (builder.sslParams == null) {
+ sslParams = getDefaultParams(sslContext);
+ } else {
+ sslParams = builder.sslParams;
+ }
+ connections = new ConnectionPool(id);
+ connections.start();
+ timeouts = new TreeSet<>();
+ try {
+ selmgr = new SelectorManager(this);
+ } catch (IOException e) {
+ // unlikely
+ throw new InternalError(e);
+ }
+ selmgr.setDaemon(true);
+ filters = new FilterFactory();
+ initFilters();
+ assert facadeRef.get() != null;
+ }
+
+ private void start() {
+ selmgr.start();
+ }
+
+ // Called from the SelectorManager thread, just before exiting.
+ // Clears the HTTP/1.1 and HTTP/2 cache, ensuring that the connections
+ // that may be still lingering there are properly closed (and their
+ // possibly still opened SocketChannel released).
+ private void stop() {
+ // Clears HTTP/1.1 cache and close its connections
+ connections.stop();
+ // Clears HTTP/2 cache and close its connections.
+ client2.stop();
+ }
+
+ private static SSLParameters getDefaultParams(SSLContext ctx) {
+ SSLParameters params = ctx.getSupportedSSLParameters();
+ params.setProtocols(new String[]{"TLSv1.2"});
+ return params;
+ }
+
+ private static ProxySelector getDefaultProxySelector() {
+ PrivilegedAction<ProxySelector> action = ProxySelector::getDefault;
+ return AccessController.doPrivileged(action);
+ }
+
+ // Returns the facade that was returned to the application code.
+ // May be null if that facade is no longer referenced.
+ final HttpClientFacade facade() {
+ return facadeRef.get();
+ }
+
+ // Increments the pendingOperationCount.
+ final long reference() {
+ pendingHttpRequestCount.incrementAndGet();
+ return pendingOperationCount.incrementAndGet();
+ }
+
+ // Decrements the pendingOperationCount.
+ final long unreference() {
+ final long count = pendingOperationCount.decrementAndGet();
+ final long httpCount = pendingHttpRequestCount.decrementAndGet();
+ final long webSocketCount = pendingWebSocketCount.get();
+ if (count == 0 && facade() == null) {
+ selmgr.wakeupSelector();
+ }
+ assert httpCount >= 0 : "count of HTTP operations < 0";
+ assert webSocketCount >= 0 : "count of WS operations < 0";
+ assert count >= 0 : "count of pending operations < 0";
+ return count;
+ }
+
+ // Increments the pendingOperationCount.
+ final long webSocketOpen() {
+ pendingWebSocketCount.incrementAndGet();
+ return pendingOperationCount.incrementAndGet();
+ }
+
+ // Decrements the pendingOperationCount.
+ final long webSocketClose() {
+ final long count = pendingOperationCount.decrementAndGet();
+ final long webSocketCount = pendingWebSocketCount.decrementAndGet();
+ final long httpCount = pendingHttpRequestCount.get();
+ if (count == 0 && facade() == null) {
+ selmgr.wakeupSelector();
+ }
+ assert httpCount >= 0 : "count of HTTP operations < 0";
+ assert webSocketCount >= 0 : "count of WS operations < 0";
+ assert count >= 0 : "count of pending operations < 0";
+ return count;
+ }
+
+ // Returns the pendingOperationCount.
+ final long referenceCount() {
+ return pendingOperationCount.get();
+ }
+
+ // Called by the SelectorManager thread to figure out whether it's time
+ // to terminate.
+ final boolean isReferenced() {
+ HttpClient facade = facade();
+ return facade != null || referenceCount() > 0;
+ }
+
+ /**
+ * Wait for activity on given exchange.
+ * The following occurs in the SelectorManager thread.
+ *
+ * 1) add to selector
+ * 2) If selector fires for this exchange then
+ * call AsyncEvent.handle()
+ *
+ * If exchange needs to change interest ops, then call registerEvent() again.
+ */
+ void registerEvent(AsyncEvent exchange) throws IOException {
+ selmgr.register(exchange);
+ }
+
+ /**
+ * Only used from RawChannel to disconnect the channel from
+ * the selector
+ */
+ void cancelRegistration(SocketChannel s) {
+ selmgr.cancel(s);
+ }
+
+ /**
+ * Allows an AsyncEvent to modify its interestOps.
+ * @param event The modified event.
+ */
+ void eventUpdated(AsyncEvent event) throws ClosedChannelException {
+ assert !(event instanceof AsyncTriggerEvent);
+ selmgr.eventUpdated(event);
+ }
+
+ boolean isSelectorThread() {
+ return Thread.currentThread() == selmgr;
+ }
+
+ Http2ClientImpl client2() {
+ return client2;
+ }
+
+ private void debugCompleted(String tag, long startNanos, HttpRequest req) {
+ if (debugelapsed.isLoggable(Level.DEBUG)) {
+ debugelapsed.log(Level.DEBUG, () -> tag + " elapsed "
+ + (System.nanoTime() - startNanos)/1000_000L
+ + " millis for " + req.method()
+ + " to " + req.uri());
+ }
+ }
+
+ @Override
+ public <T> HttpResponse<T>
+ send(HttpRequest req, BodyHandler<T> responseHandler)
+ throws IOException, InterruptedException
+ {
+ try {
+ return sendAsync(req, responseHandler).get();
+ } catch (ExecutionException e) {
+ Throwable t = e.getCause();
+ if (t instanceof Error)
+ throw (Error)t;
+ if (t instanceof RuntimeException)
+ throw (RuntimeException)t;
+ else if (t instanceof IOException)
+ throw Utils.getIOException(t);
+ else
+ throw new InternalError("Unexpected exception", t);
+ }
+ }
+
+ @Override
+ public <T> CompletableFuture<HttpResponse<T>>
+ sendAsync(HttpRequest userRequest, BodyHandler<T> responseHandler)
+ {
+ return sendAsync(userRequest, responseHandler, null);
+ }
+
+
+ @Override
+ public <T> CompletableFuture<HttpResponse<T>>
+ sendAsync(HttpRequest userRequest,
+ BodyHandler<T> responseHandler,
+ PushPromiseHandler<T> pushPromiseHandler)
+ {
+ AccessControlContext acc = null;
+ if (System.getSecurityManager() != null)
+ acc = AccessController.getContext();
+
+ // Clone the, possibly untrusted, HttpRequest
+ HttpRequestImpl requestImpl = new HttpRequestImpl(userRequest, proxySelector, acc);
+ if (requestImpl.method().equals("CONNECT"))
+ throw new IllegalArgumentException("Unsupported method CONNECT");
+
+ long start = DEBUGELAPSED ? System.nanoTime() : 0;
+ reference();
+ try {
+ debugelapsed.log(Level.DEBUG, "ClientImpl (async) send %s", userRequest);
+
+ MultiExchange<T> mex = new MultiExchange<>(userRequest,
+ requestImpl,
+ this,
+ responseHandler,
+ pushPromiseHandler,
+ acc);
+ CompletableFuture<HttpResponse<T>> res =
+ mex.responseAsync().whenComplete((b,t) -> unreference());
+ if (DEBUGELAPSED) {
+ res = res.whenComplete(
+ (b,t) -> debugCompleted("ClientImpl (async)", start, userRequest));
+ }
+ // makes sure that any dependent actions happen in the executor
+ if (acc != null) {
+ res.whenCompleteAsync((r, t) -> { /* do nothing */},
+ new PrivilegedExecutor(executor, acc));
+ }
+
+ return res;
+ } catch(Throwable t) {
+ unreference();
+ debugCompleted("ClientImpl (async)", start, userRequest);
+ throw t;
+ }
+ }
+
+ // Main loop for this client's selector
+ private final static class SelectorManager extends Thread {
+
+ // For testing purposes we have an internal System property that
+ // can control the frequency at which the selector manager will wake
+ // up when there are no pending operations.
+ // Increasing the frequency (shorter delays) might allow the selector
+ // to observe that the facade is no longer referenced and might allow
+ // the selector thread to terminate more timely - for when nothing is
+ // ongoing it will only check for that condition every NODEADLINE ms.
+ // To avoid misuse of the property, the delay that can be specified
+ // is comprised between [MIN_NODEADLINE, MAX_NODEADLINE], and its default
+ // value if unspecified (or <= 0) is DEF_NODEADLINE = 3000ms
+ // The property is -Djdk.httpclient.internal.selector.timeout=<millis>
+ private static final int MIN_NODEADLINE = 1000; // ms
+ private static final int MAX_NODEADLINE = 1000 * 1200; // ms
+ private static final int DEF_NODEADLINE = 3000; // ms
+ private static final long NODEADLINE; // default is DEF_NODEADLINE ms
+ static {
+ // ensure NODEADLINE is initialized with some valid value.
+ long deadline = Utils.getIntegerNetProperty(
+ "jdk.httpclient.internal.selector.timeout",
+ DEF_NODEADLINE); // millis
+ if (deadline <= 0) deadline = DEF_NODEADLINE;
+ deadline = Math.max(deadline, MIN_NODEADLINE);
+ NODEADLINE = Math.min(deadline, MAX_NODEADLINE);
+ }
+
+ private final Selector selector;
+ private volatile boolean closed;
+ private final List<AsyncEvent> registrations;
+ private final System.Logger debug;
+ private final System.Logger debugtimeout;
+ HttpClientImpl owner;
+ ConnectionPool pool;
+
+ SelectorManager(HttpClientImpl ref) throws IOException {
+ super(null, null, "HttpClient-" + ref.id + "-SelectorManager", 0, false);
+ owner = ref;
+ debug = ref.debug;
+ debugtimeout = ref.debugtimeout;
+ pool = ref.connectionPool();
+ registrations = new ArrayList<>();
+ selector = Selector.open();
+ }
+
+ void eventUpdated(AsyncEvent e) throws ClosedChannelException {
+ if (Thread.currentThread() == this) {
+ SelectionKey key = e.channel().keyFor(selector);
+ 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<Pair<AsyncEvent,IOException>> errorList = new ArrayList<>();
+ List<AsyncEvent> readyList = new ArrayList<>();
+ try {
+ while (!Thread.currentThread().isInterrupted()) {
+ synchronized (this) {
+ assert errorList.isEmpty();
+ assert readyList.isEmpty();
+ for (AsyncEvent event : registrations) {
+ if (event instanceof AsyncTriggerEvent) {
+ readyList.add(event);
+ continue;
+ }
+ SelectableChannel chan = event.channel();
+ SelectionKey key = null;
+ try {
+ key = chan.keyFor(selector);
+ SelectorAttachment sa;
+ if (key == null || !key.isValid()) {
+ if (key != null) {
+ // key is canceled.
+ // invoke selectNow() to purge it
+ // before registering the new event.
+ selector.selectNow();
+ }
+ sa = new SelectorAttachment(chan, selector);
+ } else {
+ sa = (SelectorAttachment) key.attachment();
+ }
+ // may throw IOE if channel closed: that's OK
+ sa.register(event);
+ if (!chan.isOpen()) {
+ throw new IOException("Channel closed");
+ }
+ } catch (IOException e) {
+ Log.logTrace("HttpClientImpl: " + e);
+ debug.log(Level.DEBUG, () ->
+ "Got " + e.getClass().getName()
+ + " while handling"
+ + " registration events");
+ chan.close();
+ // let the event abort deal with it
+ errorList.add(new Pair<>(event, e));
+ if (key != null) {
+ key.cancel();
+ selector.selectNow();
+ }
+ }
+ }
+ registrations.clear();
+ selector.selectedKeys().clear();
+ }
+
+ for (AsyncEvent event : readyList) {
+ assert event instanceof AsyncTriggerEvent;
+ event.handle();
+ }
+ readyList.clear();
+
+ for (Pair<AsyncEvent,IOException> error : errorList) {
+ // an IOException was raised and the channel closed.
+ handleEvent(error.first, error.second);
+ }
+ errorList.clear();
+
+ // Check whether client is still alive, and if not,
+ // gracefully stop this thread
+ if (!owner.isReferenced()) {
+ Log.logTrace("HttpClient no longer referenced. Exiting...");
+ return;
+ }
+
+ // Timeouts will have milliseconds granularity. It is important
+ // to handle them in a timely fashion.
+ long nextTimeout = owner.purgeTimeoutsAndReturnNextDeadline();
+ debugtimeout.log(Level.DEBUG, "next timeout: %d", nextTimeout);
+
+ // Keep-alive have seconds granularity. It's not really an
+ // issue if we keep connections linger a bit more in the keep
+ // alive cache.
+ long nextExpiry = pool.purgeExpiredConnectionsAndReturnNextDeadline();
+ debugtimeout.log(Level.DEBUG, "next expired: %d", nextExpiry);
+
+ assert nextTimeout >= 0;
+ assert nextExpiry >= 0;
+
+ // Don't wait for ever as it might prevent the thread to
+ // stop gracefully. millis will be 0 if no deadline was found.
+ if (nextTimeout <= 0) nextTimeout = NODEADLINE;
+
+ // Clip nextExpiry at NODEADLINE limit. The default
+ // keep alive is 1200 seconds (half an hour) - we don't
+ // want to wait that long.
+ if (nextExpiry <= 0) nextExpiry = NODEADLINE;
+ else nextExpiry = Math.min(NODEADLINE, nextExpiry);
+
+ // takes the least of the two.
+ long millis = Math.min(nextExpiry, nextTimeout);
+
+ debugtimeout.log(Level.DEBUG, "Next deadline is %d",
+ (millis == 0 ? NODEADLINE : millis));
+ //debugPrint(selector);
+ int n = selector.select(millis == 0 ? NODEADLINE : millis);
+ if (n == 0) {
+ // Check whether client is still alive, and if not,
+ // gracefully stop this thread
+ if (!owner.isReferenced()) {
+ Log.logTrace("HttpClient no longer referenced. Exiting...");
+ return;
+ }
+ owner.purgeTimeoutsAndReturnNextDeadline();
+ continue;
+ }
+ Set<SelectionKey> keys = selector.selectedKeys();
+
+ assert errorList.isEmpty();
+ for (SelectionKey key : keys) {
+ SelectorAttachment sa = (SelectorAttachment) key.attachment();
+ if (!key.isValid()) {
+ IOException ex = sa.chan.isOpen()
+ ? new IOException("Invalid key")
+ : new ClosedChannelException();
+ sa.pending.forEach(e -> errorList.add(new Pair<>(e,ex)));
+ sa.pending.clear();
+ continue;
+ }
+
+ int eventsOccurred;
+ try {
+ eventsOccurred = key.readyOps();
+ } catch (CancelledKeyException ex) {
+ IOException io = Utils.getIOException(ex);
+ sa.pending.forEach(e -> errorList.add(new Pair<>(e,io)));
+ sa.pending.clear();
+ continue;
+ }
+ sa.events(eventsOccurred).forEach(readyList::add);
+ sa.resetInterestOps(eventsOccurred);
+ }
+ selector.selectNow(); // complete cancellation
+ selector.selectedKeys().clear();
+
+ for (AsyncEvent event : readyList) {
+ handleEvent(event, null); // will be delegated to executor
+ }
+ readyList.clear();
+ errorList.forEach((p) -> handleEvent(p.first, p.second));
+ errorList.clear();
+ }
+ } catch (Throwable e) {
+ //e.printStackTrace();
+ if (!closed) {
+ // This terminates thread. So, better just print stack trace
+ String err = Utils.stackTrace(e);
+ Log.logError("HttpClientImpl: fatal error: " + err);
+ }
+ debug.log(Level.DEBUG, "shutting down", e);
+ if (Utils.ASSERTIONSENABLED && !debug.isLoggable(Level.DEBUG)) {
+ e.printStackTrace(System.err); // always print the stack
+ }
+ } finally {
+ shutdown();
+ }
+ }
+
+// void debugPrint(Selector selector) {
+// System.err.println("Selector: debugprint start");
+// Set<SelectionKey> keys = selector.keys();
+// for (SelectionKey key : keys) {
+// SelectableChannel c = key.channel();
+// int ops = key.interestOps();
+// System.err.printf("selector chan:%s ops:%d\n", c, ops);
+// }
+// System.err.println("Selector: debugprint end");
+// }
+
+ /** Handles the given event. The given ioe may be null. */
+ void handleEvent(AsyncEvent event, IOException ioe) {
+ if (closed || ioe != null) {
+ event.abort(ioe);
+ } else {
+ event.handle();
+ }
+ }
+ }
+
+ /**
+ * Tracks multiple user level registrations associated with one NIO
+ * registration (SelectionKey). In this implementation, registrations
+ * are one-off and when an event is posted the registration is cancelled
+ * until explicitly registered again.
+ *
+ * <p> No external synchronization required as this class is only used
+ * by the SelectorManager thread. One of these objects required per
+ * connection.
+ */
+ private static class SelectorAttachment {
+ private final SelectableChannel chan;
+ private final Selector selector;
+ private final Set<AsyncEvent> pending;
+ private final static System.Logger debug =
+ Utils.getDebugLogger("SelectorAttachment"::toString, DEBUG);
+ private int interestOps;
+
+ SelectorAttachment(SelectableChannel chan, Selector selector) {
+ this.pending = new HashSet<>();
+ this.chan = chan;
+ this.selector = selector;
+ }
+
+ void register(AsyncEvent e) throws ClosedChannelException {
+ int newOps = e.interestOps();
+ boolean reRegister = (interestOps & newOps) != newOps;
+ interestOps |= newOps;
+ pending.add(e);
+ if (reRegister) {
+ // first time registration happens here also
+ try {
+ chan.register(selector, interestOps, this);
+ } catch (CancelledKeyException x) {
+ abortPending(x);
+ }
+ }
+ }
+
+ /**
+ * Returns a Stream<AsyncEvents> containing only events that are
+ * registered with the given {@code interestOps}.
+ */
+ Stream<AsyncEvent> events(int interestOps) {
+ return pending.stream()
+ .filter(ev -> (ev.interestOps() & interestOps) != 0);
+ }
+
+ /**
+ * Removes any events with the given {@code interestOps}, and if no
+ * events remaining, cancels the associated SelectionKey.
+ */
+ void resetInterestOps(int interestOps) {
+ int newOps = 0;
+
+ Iterator<AsyncEvent> itr = pending.iterator();
+ while (itr.hasNext()) {
+ AsyncEvent event = itr.next();
+ int evops = event.interestOps();
+ if (event.repeating()) {
+ newOps |= evops;
+ continue;
+ }
+ if ((evops & interestOps) != 0) {
+ itr.remove();
+ } else {
+ newOps |= evops;
+ }
+ }
+
+ this.interestOps = newOps;
+ SelectionKey key = chan.keyFor(selector);
+ if (newOps == 0 && pending.isEmpty()) {
+ key.cancel();
+ } else {
+ try {
+ key.interestOps(newOps);
+ } catch (CancelledKeyException x) {
+ // channel may have been closed
+ debug.log(Level.DEBUG, "key cancelled for " + chan);
+ abortPending(x);
+ }
+ }
+ }
+
+ void abortPending(Throwable x) {
+ if (!pending.isEmpty()) {
+ AsyncEvent[] evts = pending.toArray(new AsyncEvent[0]);
+ pending.clear();
+ IOException io = Utils.getIOException(x);
+ for (AsyncEvent event : evts) {
+ event.abort(io);
+ }
+ }
+ }
+ }
+
+ /*package-private*/ SSLContext theSSLContext() {
+ return sslContext;
+ }
+
+ @Override
+ public SSLContext sslContext() {
+ return sslContext;
+ }
+
+ @Override
+ public SSLParameters sslParameters() {
+ return Utils.copySSLParameters(sslParams);
+ }
+
+ @Override
+ public Optional<Authenticator> authenticator() {
+ return Optional.ofNullable(authenticator);
+ }
+
+ /*package-private*/ final Executor theExecutor() {
+ return executor;
+ }
+
+ @Override
+ public final Optional<Executor> executor() {
+ return isDefaultExecutor ? Optional.empty() : Optional.of(executor);
+ }
+
+ ConnectionPool connectionPool() {
+ return connections;
+ }
+
+ @Override
+ public Redirect followRedirects() {
+ return followRedirects;
+ }
+
+
+ @Override
+ public Optional<CookieHandler> cookieHandler() {
+ return Optional.ofNullable(cookieHandler);
+ }
+
+ @Override
+ public Optional<ProxySelector> proxy() {
+ return this.userProxySelector;
+ }
+
+ // Return the effective proxy that this client uses.
+ ProxySelector proxySelector() {
+ return proxySelector;
+ }
+
+ @Override
+ public WebSocket.Builder newWebSocketBuilder() {
+ // Make sure to pass the HttpClientFacade to the WebSocket builder.
+ // This will ensure that the facade is not released before the
+ // WebSocket has been created, at which point the pendingOperationCount
+ // will have been incremented by the DetachedConnectionChannel
+ // (see PlainHttpConnection.detachChannel())
+ return new BuilderImpl(this.facade(), proxySelector);
+ }
+
+ @Override
+ public Version version() {
+ return version;
+ }
+
+ String dbgString() {
+ return dbgTag;
+ }
+
+ @Override
+ public String toString() {
+ // Used by tests to get the client's id and compute the
+ // name of the SelectorManager thread.
+ return super.toString() + ("(" + id + ")");
+ }
+
+ private void initFilters() {
+ addFilter(AuthenticationFilter.class);
+ addFilter(RedirectFilter.class);
+ if (this.cookieHandler != null) {
+ addFilter(CookieFilter.class);
+ }
+ }
+
+ private void addFilter(Class<? extends HeaderFilter> f) {
+ filters.addFilter(f);
+ }
+
+ final List<HeaderFilter> filterChain() {
+ return filters.getFilterChain();
+ }
+
+ // Timer controls.
+ // Timers are implemented through timed Selector.select() calls.
+
+ synchronized void registerTimer(TimeoutEvent event) {
+ Log.logTrace("Registering timer {0}", event);
+ timeouts.add(event);
+ selmgr.wakeupSelector();
+ }
+
+ synchronized void cancelTimer(TimeoutEvent event) {
+ Log.logTrace("Canceling timer {0}", event);
+ timeouts.remove(event);
+ }
+
+ /**
+ * Purges ( handles ) timer events that have passed their deadline, and
+ * returns the amount of time, in milliseconds, until the next earliest
+ * event. A return value of 0 means that there are no events.
+ */
+ private long purgeTimeoutsAndReturnNextDeadline() {
+ long diff = 0L;
+ List<TimeoutEvent> toHandle = null;
+ int remaining = 0;
+ // enter critical section to retrieve the timeout event to handle
+ synchronized(this) {
+ if (timeouts.isEmpty()) return 0L;
+
+ Instant now = Instant.now();
+ Iterator<TimeoutEvent> itr = timeouts.iterator();
+ while (itr.hasNext()) {
+ TimeoutEvent event = itr.next();
+ diff = now.until(event.deadline(), ChronoUnit.MILLIS);
+ if (diff <= 0) {
+ itr.remove();
+ toHandle = (toHandle == null) ? new ArrayList<>() : toHandle;
+ toHandle.add(event);
+ } else {
+ break;
+ }
+ }
+ remaining = timeouts.size();
+ }
+
+ // can be useful for debugging
+ if (toHandle != null && Log.trace()) {
+ Log.logTrace("purgeTimeoutsAndReturnNextDeadline: handling "
+ + toHandle.size() + " events, "
+ + "remaining " + remaining
+ + ", next deadline: " + (diff < 0 ? 0L : diff));
+ }
+
+ // handle timeout events out of critical section
+ if (toHandle != null) {
+ Throwable failed = null;
+ for (TimeoutEvent event : toHandle) {
+ try {
+ Log.logTrace("Firing timer {0}", event);
+ event.handle();
+ } catch (Error | RuntimeException e) {
+ // Not expected. Handle remaining events then throw...
+ // If e is an OOME or SOE it might simply trigger a new
+ // error from here - but in this case there's not much we
+ // could do anyway. Just let it flow...
+ if (failed == null) failed = e;
+ else failed.addSuppressed(e);
+ Log.logTrace("Failed to handle event {0}: {1}", event, e);
+ }
+ }
+ if (failed instanceof Error) throw (Error) failed;
+ if (failed instanceof RuntimeException) throw (RuntimeException) failed;
+ }
+
+ // return time to wait until next event. 0L if there's no more events.
+ return diff < 0 ? 0L : diff;
+ }
+
+ // used for the connection window
+ int getReceiveBufferSize() {
+ return Utils.getIntegerNetProperty(
+ "jdk.httpclient.receiveBufferSize", 2 * 1024 * 1024
+ );
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<CompletionStage<?>, Boolean> operations =
+ new IdentityHashMap<>();
+ void add(CompletionStage<?> cf) {
+ synchronized(operations) {
+ cf.whenComplete((r,t)-> remove(cf));
+ operations.put(cf, Boolean.TRUE);
+ }
+ }
+ boolean remove(CompletionStage<?> cf) {
+ synchronized(operations) {
+ return operations.remove(cf);
+ }
+ }
+ }
+
+ final void addTrailingOperation(CompletionStage<?> cf) {
+ trailingOperations.add(cf);
+ }
+
+// final void removeTrailingOperation(CompletableFuture<?> cf) {
+// trailingOperations.remove(cf);
+// }
+
+ final HttpClientImpl client() {
+ return client;
+ }
+
+ //public abstract void connect() throws IOException, InterruptedException;
+
+ public abstract CompletableFuture<Void> connectAsync();
+
+ /** Tells whether, or not, this connection is connected to its destination. */
+ abstract boolean connected();
+
+ /** Tells whether, or not, this connection is secure ( over SSL ) */
+ abstract boolean isSecure();
+
+ /** Tells whether, or not, this connection is proxied. */
+ abstract boolean isProxied();
+
+ /** Tells whether, or not, this connection is open. */
+ final boolean isOpen() {
+ return channel().isOpen() &&
+ (connected() ? !getConnectionFlow().isFinished() : true);
+ }
+
+ interface HttpPublisher extends FlowTube.TubePublisher {
+ void enqueue(List<ByteBuffer> buffers) throws IOException;
+ void enqueueUnordered(List<ByteBuffer> buffers) throws IOException;
+ void signalEnqueued() throws IOException;
+ }
+
+ /**
+ * Returns the HTTP publisher associated with this connection. May be null
+ * if invoked before connecting.
+ */
+ abstract HttpPublisher publisher();
+
+ // HTTP/2 MUST use TLS version 1.2 or higher for HTTP/2 over TLS
+ private static final Predicate<String> testRequiredHTTP2TLSVersion = proto ->
+ proto.equals("TLSv1.2") || proto.equals("TLSv1.3");
+
+ /**
+ * Returns true if the given client's SSL parameter protocols contains at
+ * least one TLS version that HTTP/2 requires.
+ */
+ private static final boolean hasRequiredHTTP2TLSVersion(HttpClient client) {
+ String[] protos = client.sslParameters().getProtocols();
+ if (protos != null) {
+ return Arrays.stream(protos).filter(testRequiredHTTP2TLSVersion).findAny().isPresent();
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Factory for retrieving HttpConnections. A connection can be retrieved
+ * from the connection pool, or a new one created if none available.
+ *
+ * The given {@code addr} is the ultimate destination. Any proxies,
+ * etc, are determined from the request. Returns a concrete instance which
+ * is one of the following:
+ * {@link PlainHttpConnection}
+ * {@link PlainTunnelingConnection}
+ *
+ * The returned connection, if not from the connection pool, must have its,
+ * connect() or connectAsync() method invoked, which ( when it completes
+ * successfully ) renders the connection usable for requests.
+ */
+ public static HttpConnection getConnection(InetSocketAddress addr,
+ HttpClientImpl client,
+ HttpRequestImpl request,
+ Version version) {
+ HttpConnection c = null;
+ InetSocketAddress proxy = request.proxy();
+ if (proxy != null && proxy.isUnresolved()) {
+ // The default proxy selector may select a proxy whose address is
+ // unresolved. We must resolve the address before connecting to it.
+ proxy = new InetSocketAddress(proxy.getHostString(), proxy.getPort());
+ }
+ boolean secure = request.secure();
+ ConnectionPool pool = client.connectionPool();
+
+ if (!secure) {
+ c = pool.getConnection(false, addr, proxy);
+ if (c != null && c.isOpen() /* may have been eof/closed when in the pool */) {
+ final HttpConnection conn = c;
+ DEBUG_LOGGER.log(Level.DEBUG, () -> conn.getConnectionFlow()
+ + ": plain connection retrieved from HTTP/1.1 pool");
+ return c;
+ } else {
+ return getPlainConnection(addr, proxy, request, client);
+ }
+ } else { // secure
+ if (version != HTTP_2) { // only HTTP/1.1 connections are in the pool
+ c = pool.getConnection(true, addr, proxy);
+ }
+ if (c != null && c.isOpen()) {
+ final HttpConnection conn = c;
+ DEBUG_LOGGER.log(Level.DEBUG, () -> conn.getConnectionFlow()
+ + ": SSL connection retrieved from HTTP/1.1 pool");
+ return c;
+ } else {
+ String[] alpn = null;
+ if (version == HTTP_2 && hasRequiredHTTP2TLSVersion(client)) {
+ alpn = new String[] { "h2", "http/1.1" };
+ }
+ return getSSLConnection(addr, proxy, alpn, request, client);
+ }
+ }
+ }
+
+ private static HttpConnection getSSLConnection(InetSocketAddress addr,
+ InetSocketAddress proxy,
+ String[] alpn,
+ HttpRequestImpl request,
+ HttpClientImpl client) {
+ if (proxy != null)
+ return new AsyncSSLTunnelConnection(addr, client, alpn, proxy,
+ proxyTunnelHeaders(request));
+ else
+ return new AsyncSSLConnection(addr, client, alpn);
+ }
+
+ /**
+ * This method is used to build a filter that will accept or
+ * veto (header-name, value) tuple for transmission on the
+ * wire.
+ * The filter is applied to the headers when sending the headers
+ * to the remote party.
+ * Which tuple is accepted/vetoed depends on:
+ * <pre>
+ * - whether the connection is a tunnel connection
+ * [talking to a server through a proxy tunnel]
+ * - whether the method is CONNECT
+ * [establishing a CONNECT tunnel through a proxy]
+ * - whether the request is using a proxy
+ * (and the connection is not a tunnel)
+ * [talking to a server through a proxy]
+ * - whether the request is a direct connection to
+ * a server (no tunnel, no proxy).
+ * </pre>
+ * @param request
+ * @return
+ */
+ BiPredicate<String,List<String>> headerFilter(HttpRequestImpl request) {
+ if (isTunnel()) {
+ // talking to a server through a proxy tunnel
+ // don't send proxy-* headers to a plain server
+ assert !request.isConnect();
+ return Utils.NO_PROXY_HEADERS_FILTER;
+ } else if (request.isConnect()) {
+ // establishing a proxy tunnel
+ // check for proxy tunnel disabled schemes
+ // assert !this.isTunnel();
+ assert request.proxy() == null;
+ return Utils.PROXY_TUNNEL_FILTER;
+ } else if (request.proxy() != null) {
+ // talking to a server through a proxy (no tunnel)
+ // check for proxy disabled schemes
+ // assert !isTunnel() && !request.isConnect();
+ return Utils.PROXY_FILTER;
+ } else {
+ // talking to a server directly (no tunnel, no proxy)
+ // don't send proxy-* headers to a plain server
+ // assert request.proxy() == null && !request.isConnect();
+ return Utils.NO_PROXY_HEADERS_FILTER;
+ }
+ }
+
+ // Composes a new immutable HttpHeaders that combines the
+ // user and system header but only keeps those headers that
+ // start with "proxy-"
+ private static HttpHeaders proxyTunnelHeaders(HttpRequestImpl request) {
+ Map<String, List<String>> combined = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ combined.putAll(request.getSystemHeaders().map());
+ combined.putAll(request.headers().map()); // let user override system
+
+ // keep only proxy-* - and also strip authorization headers
+ // for disabled schemes
+ return ImmutableHeaders.of(combined, Utils.PROXY_TUNNEL_FILTER);
+ }
+
+ /* Returns either a plain HTTP connection or a plain tunnelling connection
+ * for proxied WebSocket */
+ private static HttpConnection getPlainConnection(InetSocketAddress addr,
+ InetSocketAddress proxy,
+ HttpRequestImpl request,
+ HttpClientImpl client) {
+ if (request.isWebSocket() && proxy != null)
+ return new PlainTunnelingConnection(addr, proxy, client,
+ proxyTunnelHeaders(request));
+
+ if (proxy == null)
+ return new PlainHttpConnection(addr, client);
+ else
+ return new PlainProxyConnection(proxy, client);
+ }
+
+ void closeOrReturnToCache(HttpHeaders hdrs) {
+ if (hdrs == null) {
+ // the connection was closed by server, eof
+ close();
+ return;
+ }
+ if (!isOpen()) {
+ return;
+ }
+ HttpClientImpl client = client();
+ if (client == null) {
+ close();
+ return;
+ }
+ ConnectionPool pool = client.connectionPool();
+ boolean keepAlive = hdrs.firstValue("Connection")
+ .map((s) -> !s.equalsIgnoreCase("close"))
+ .orElse(true);
+
+ if (keepAlive) {
+ Log.logTrace("Returning connection to the pool: {0}", this);
+ pool.returnToPool(this);
+ } else {
+ close();
+ }
+ }
+
+ /* Tells whether or not this connection is a tunnel through a proxy */
+ boolean isTunnel() { return false; }
+
+ abstract SocketChannel channel();
+
+ final InetSocketAddress address() {
+ return address;
+ }
+
+ abstract ConnectionPool.CacheKey cacheKey();
+
+ /**
+ * Closes this connection, by returning the socket to its connection pool.
+ */
+ @Override
+ public abstract void close();
+
+ abstract void shutdownInput() throws IOException;
+
+ abstract void shutdownOutput() throws IOException;
+
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ abstract static class DetachedConnectionChannel implements Closeable {
+ DetachedConnectionChannel() {}
+ abstract SocketChannel channel();
+ abstract long write(ByteBuffer[] buffers, int start, int number)
+ throws IOException;
+ abstract void shutdownInput() throws IOException;
+ abstract void shutdownOutput() throws IOException;
+ abstract ByteBuffer read() throws IOException;
+ @Override
+ public abstract void close();
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName() + ": " + channel().toString();
+ }
+ }
+
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ abstract DetachedConnectionChannel detachChannel();
+
+ abstract FlowTube getConnectionFlow();
+
+ /**
+ * A publisher that makes it possible to publish (write)
+ * ordered (normal priority) and unordered (high priority)
+ * buffers downstream.
+ */
+ final class PlainHttpPublisher implements HttpPublisher {
+ final Object reading;
+ PlainHttpPublisher() {
+ this(new Object());
+ }
+ PlainHttpPublisher(Object readingLock) {
+ this.reading = readingLock;
+ }
+ final ConcurrentLinkedDeque<List<ByteBuffer>> queue = new ConcurrentLinkedDeque<>();
+ volatile Flow.Subscriber<? super List<ByteBuffer>> subscriber;
+ volatile HttpWriteSubscription subscription;
+ final SequentialScheduler writeScheduler =
+ new SequentialScheduler(this::flushTask);
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+ synchronized (reading) {
+ //assert this.subscription == null;
+ //assert this.subscriber == null;
+ if (subscription == null) {
+ subscription = new HttpWriteSubscription();
+ }
+ this.subscriber = subscriber;
+ }
+ // TODO: should we do this in the flow?
+ subscriber.onSubscribe(subscription);
+ signal();
+ }
+
+ void flushTask(DeferredCompleter completer) {
+ try {
+ HttpWriteSubscription sub = subscription;
+ if (sub != null) sub.flush();
+ } finally {
+ completer.complete();
+ }
+ }
+
+ void signal() {
+ writeScheduler.runOrSchedule();
+ }
+
+ final class HttpWriteSubscription implements Flow.Subscription {
+ final Demand demand = new Demand();
+
+ @Override
+ public void request(long n) {
+ if (n <= 0) throw new IllegalArgumentException("non-positive request");
+ demand.increase(n);
+ debug.log(Level.DEBUG, () -> "HttpPublisher: got request of "
+ + n + " from "
+ + getConnectionFlow());
+ writeScheduler.runOrSchedule();
+ }
+
+ @Override
+ public void cancel() {
+ debug.log(Level.DEBUG, () -> "HttpPublisher: cancelled by "
+ + getConnectionFlow());
+ }
+
+ void flush() {
+ while (!queue.isEmpty() && demand.tryDecrement()) {
+ List<ByteBuffer> elem = queue.poll();
+ debug.log(Level.DEBUG, () -> "HttpPublisher: sending "
+ + Utils.remaining(elem) + " bytes ("
+ + elem.size() + " buffers) to "
+ + getConnectionFlow());
+ subscriber.onNext(elem);
+ }
+ }
+ }
+
+ @Override
+ public void enqueue(List<ByteBuffer> buffers) throws IOException {
+ queue.add(buffers);
+ int bytes = buffers.stream().mapToInt(ByteBuffer::remaining).sum();
+ debug.log(Level.DEBUG, "added %d bytes to the write queue", bytes);
+ }
+
+ @Override
+ public void enqueueUnordered(List<ByteBuffer> buffers) throws IOException {
+ // Unordered frames are sent before existing frames.
+ int bytes = buffers.stream().mapToInt(ByteBuffer::remaining).sum();
+ queue.addFirst(buffers);
+ debug.log(Level.DEBUG, "inserted %d bytes in the write queue", bytes);
+ }
+
+ @Override
+ public void signalEnqueued() throws IOException {
+ debug.log(Level.DEBUG, "signalling the publisher of the write queue");
+ signal();
+ }
+ }
+
+ String dbgTag = null;
+ final String dbgString() {
+ FlowTube flow = getConnectionFlow();
+ String tag = dbgTag;
+ if (tag == null && flow != null) {
+ dbgTag = tag = this.getClass().getSimpleName() + "(" + flow + ")";
+ } else if (tag == null) {
+ tag = this.getClass().getSimpleName() + "(?)";
+ }
+ return tag;
+ }
+
+ @Override
+ public String toString() {
+ return "HttpConnection: " + channel().toString();
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<HttpClient.Version> version;
+ private Duration duration;
+
+ public HttpRequestBuilderImpl(URI uri) {
+ requireNonNull(uri, "uri must be non-null");
+ checkURI(uri);
+ this.uri = uri;
+ this.userHeaders = new HttpHeadersImpl();
+ this.method = "GET"; // default, as per spec
+ this.version = Optional.empty();
+ }
+
+ public HttpRequestBuilderImpl() {
+ this.userHeaders = new HttpHeadersImpl();
+ this.method = "GET"; // default, as per spec
+ this.version = Optional.empty();
+ }
+
+ @Override
+ public HttpRequestBuilderImpl uri(URI uri) {
+ requireNonNull(uri, "uri must be non-null");
+ checkURI(uri);
+ this.uri = uri;
+ return this;
+ }
+
+ private static IllegalArgumentException newIAE(String message, Object... args) {
+ return new IllegalArgumentException(format(message, args));
+ }
+
+ private static void checkURI(URI uri) {
+ String scheme = uri.getScheme();
+ if (scheme == null)
+ throw newIAE("URI with undefined scheme");
+ scheme = scheme.toLowerCase();
+ if (!(scheme.equals("https") || scheme.equals("http"))) {
+ throw newIAE("invalid URI scheme %s", scheme);
+ }
+ if (uri.getHost() == null) {
+ throw newIAE("unsupported URI %s", uri);
+ }
+ }
+
+ @Override
+ public HttpRequestBuilderImpl copy() {
+ HttpRequestBuilderImpl b = new HttpRequestBuilderImpl(this.uri);
+ b.userHeaders = this.userHeaders.deepCopy();
+ b.method = this.method;
+ b.expectContinue = this.expectContinue;
+ b.bodyPublisher = bodyPublisher;
+ b.uri = uri;
+ b.duration = duration;
+ b.version = version;
+ return b;
+ }
+
+ private void checkNameAndValue(String name, String value) {
+ requireNonNull(name, "name");
+ requireNonNull(value, "value");
+ if (!isValidName(name)) {
+ throw newIAE("invalid header name: \"%s\"", name);
+ }
+ if (!Utils.ALLOWED_HEADERS.test(name)) {
+ throw newIAE("restricted header name: \"%s\"", name);
+ }
+ if (!isValidValue(value)) {
+ throw newIAE("invalid header value: \"%s\"", value);
+ }
+ }
+
+ @Override
+ public HttpRequestBuilderImpl setHeader(String name, String value) {
+ checkNameAndValue(name, value);
+ userHeaders.setHeader(name, value);
+ return this;
+ }
+
+ @Override
+ public HttpRequestBuilderImpl header(String name, String value) {
+ checkNameAndValue(name, value);
+ userHeaders.addHeader(name, value);
+ return this;
+ }
+
+ @Override
+ public HttpRequestBuilderImpl headers(String... params) {
+ requireNonNull(params);
+ if (params.length == 0 || params.length % 2 != 0) {
+ throw newIAE("wrong number, %d, of parameters", params.length);
+ }
+ for (int i = 0; i < params.length; i += 2) {
+ String name = params[i];
+ String value = params[i + 1];
+ header(name, value);
+ }
+ return this;
+ }
+
+ @Override
+ public HttpRequestBuilderImpl expectContinue(boolean enable) {
+ expectContinue = enable;
+ return this;
+ }
+
+ @Override
+ public HttpRequestBuilderImpl version(HttpClient.Version version) {
+ requireNonNull(version);
+ this.version = Optional.of(version);
+ return this;
+ }
+
+ HttpHeadersImpl headers() { return userHeaders; }
+
+ URI uri() { return uri; }
+
+ String method() { return method; }
+
+ boolean expectContinue() { return expectContinue; }
+
+ BodyPublisher bodyPublisher() { return bodyPublisher; }
+
+ Optional<HttpClient.Version> version() { return version; }
+
+ @Override
+ public HttpRequest.Builder GET() {
+ return method0("GET", null);
+ }
+
+ @Override
+ public HttpRequest.Builder POST(BodyPublisher body) {
+ return method0("POST", requireNonNull(body));
+ }
+
+ @Override
+ public HttpRequest.Builder DELETE(BodyPublisher body) {
+ return method0("DELETE", requireNonNull(body));
+ }
+
+ @Override
+ public HttpRequest.Builder PUT(BodyPublisher body) {
+ return method0("PUT", requireNonNull(body));
+ }
+
+ @Override
+ public HttpRequest.Builder method(String method, BodyPublisher body) {
+ requireNonNull(method);
+ if (method.equals(""))
+ throw newIAE("illegal method <empty string>");
+ if (method.equals("CONNECT"))
+ throw newIAE("method CONNECT is not supported");
+ return method0(method, requireNonNull(body));
+ }
+
+ private HttpRequest.Builder method0(String method, BodyPublisher body) {
+ assert method != null;
+ assert !method.equals("GET") ? body != null : true;
+ assert !method.equals("");
+ this.method = method;
+ this.bodyPublisher = body;
+ return this;
+ }
+
+ @Override
+ public HttpRequest build() {
+ if (uri == null)
+ throw new IllegalStateException("uri is null");
+ assert method != null;
+ return new HttpRequestImpl(this);
+ }
+
+ @Override
+ public HttpRequest.Builder timeout(Duration duration) {
+ requireNonNull(duration);
+ if (duration.isNegative() || Duration.ZERO.equals(duration))
+ throw new IllegalArgumentException("Invalid duration: " + duration);
+ this.duration = duration;
+ return this;
+ }
+
+ Duration timeout() { return duration; }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<HttpClient.Version> version;
+
+ private static String userAgent() {
+ PrivilegedAction<String> pa = () -> System.getProperty("java.version");
+ String version = AccessController.doPrivileged(pa);
+ return "Java-http-client/" + version;
+ }
+
+ /** The value of the User-Agent header for all requests sent by the client. */
+ public static final String USER_AGENT = userAgent();
+
+ /**
+ * Creates an HttpRequestImpl from the given builder.
+ */
+ public HttpRequestImpl(HttpRequestBuilderImpl builder) {
+ String method = builder.method();
+ this.method = method == null ? "GET" : method;
+ this.userHeaders = ImmutableHeaders.of(builder.headers().map(), ALLOWED_HEADERS);
+ this.systemHeaders = new HttpHeadersImpl();
+ this.uri = builder.uri();
+ assert uri != null;
+ this.proxy = null;
+ this.expectContinue = builder.expectContinue();
+ this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
+ this.requestPublisher = builder.bodyPublisher(); // may be null
+ this.timeout = builder.timeout();
+ this.version = builder.version();
+ this.authority = null;
+ }
+
+ /**
+ * Creates an HttpRequestImpl from the given request.
+ */
+ public HttpRequestImpl(HttpRequest request, ProxySelector ps, AccessControlContext acc) {
+ String method = request.method();
+ this.method = method == null ? "GET" : method;
+ this.userHeaders = request.headers();
+ if (request instanceof HttpRequestImpl) {
+ this.systemHeaders = ((HttpRequestImpl) request).systemHeaders;
+ this.isWebSocket = ((HttpRequestImpl) request).isWebSocket;
+ } else {
+ this.systemHeaders = new HttpHeadersImpl();
+ }
+ this.systemHeaders.setHeader("User-Agent", USER_AGENT);
+ this.uri = request.uri();
+ if (isWebSocket) {
+ // WebSocket determines and sets the proxy itself
+ this.proxy = ((HttpRequestImpl) request).proxy;
+ } else {
+ if (ps != null)
+ this.proxy = retrieveProxy(ps, uri);
+ else
+ this.proxy = null;
+ }
+ this.expectContinue = request.expectContinue();
+ this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
+ this.requestPublisher = request.bodyPublisher().orElse(null);
+ if (acc != null && requestPublisher instanceof RequestPublishers.FilePublisher) {
+ // Restricts the file publisher with the senders ACC, if any
+ ((RequestPublishers.FilePublisher)requestPublisher).setAccessControlContext(acc);
+ }
+ this.timeout = request.timeout().orElse(null);
+ this.version = request.version();
+ this.authority = null;
+ }
+
+ /** Creates a HttpRequestImpl using fields of an existing request impl. */
+ public HttpRequestImpl(URI uri,
+ String method,
+ HttpRequestImpl other) {
+ this.method = method == null? "GET" : method;
+ this.userHeaders = other.userHeaders;
+ this.isWebSocket = other.isWebSocket;
+ this.systemHeaders = other.systemHeaders;
+ this.uri = uri;
+ this.proxy = other.proxy;
+ this.expectContinue = other.expectContinue;
+ this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
+ this.requestPublisher = other.requestPublisher; // may be null
+ this.acc = other.acc;
+ this.timeout = other.timeout;
+ this.version = other.version();
+ this.authority = null;
+ }
+
+ /* used for creating CONNECT requests */
+ HttpRequestImpl(String method, InetSocketAddress authority, HttpHeaders headers) {
+ // TODO: isWebSocket flag is not specified, but the assumption is that
+ // such a request will never be made on a connection that will be returned
+ // to the connection pool (we might need to revisit this constructor later)
+ assert "CONNECT".equalsIgnoreCase(method);
+ this.method = method;
+ this.systemHeaders = new HttpHeadersImpl();
+ this.userHeaders = ImmutableHeaders.of(headers);
+ this.uri = URI.create("socket://" + authority.getHostString() + ":"
+ + Integer.toString(authority.getPort()) + "/");
+ this.proxy = null;
+ this.requestPublisher = null;
+ this.authority = authority;
+ this.secure = false;
+ this.expectContinue = false;
+ this.timeout = null;
+ // The CONNECT request sent for tunneling is only used in two cases:
+ // 1. websocket, which only supports HTTP/1.1
+ // 2. SSL tunneling through a HTTP/1.1 proxy
+ // In either case we do not want to upgrade the connection to the proxy.
+ // What we want to possibly upgrade is the tunneled connection to the
+ // target server (so not the CONNECT request itself)
+ this.version = Optional.of(HttpClient.Version.HTTP_1_1);
+ }
+
+ final boolean isConnect() {
+ return "CONNECT".equalsIgnoreCase(method);
+ }
+
+ /**
+ * Creates a HttpRequestImpl from the given set of Headers and the associated
+ * "parent" request. Fields not taken from the headers are taken from the
+ * parent.
+ */
+ static HttpRequestImpl createPushRequest(HttpRequestImpl parent,
+ HttpHeadersImpl headers)
+ throws IOException
+ {
+ return new HttpRequestImpl(parent, headers);
+ }
+
+ // only used for push requests
+ private HttpRequestImpl(HttpRequestImpl parent, HttpHeadersImpl headers)
+ throws IOException
+ {
+ this.method = headers.firstValue(":method")
+ .orElseThrow(() -> new IOException("No method in Push Promise"));
+ String path = headers.firstValue(":path")
+ .orElseThrow(() -> new IOException("No path in Push Promise"));
+ String scheme = headers.firstValue(":scheme")
+ .orElseThrow(() -> new IOException("No scheme in Push Promise"));
+ String authority = headers.firstValue(":authority")
+ .orElseThrow(() -> new IOException("No authority in Push Promise"));
+ StringBuilder sb = new StringBuilder();
+ sb.append(scheme).append("://").append(authority).append(path);
+ this.uri = URI.create(sb.toString());
+ this.proxy = null;
+ this.userHeaders = ImmutableHeaders.of(headers.map(), ALLOWED_HEADERS);
+ this.systemHeaders = parent.systemHeaders;
+ this.expectContinue = parent.expectContinue;
+ this.secure = parent.secure;
+ this.requestPublisher = parent.requestPublisher;
+ this.acc = parent.acc;
+ this.timeout = parent.timeout;
+ this.version = parent.version;
+ this.authority = null;
+ }
+
+ @Override
+ public String toString() {
+ return (uri == null ? "" : uri.toString()) + " " + method;
+ }
+
+ @Override
+ public HttpHeaders headers() {
+ return userHeaders;
+ }
+
+ InetSocketAddress authority() { return authority; }
+
+ void setH2Upgrade(Http2ClientImpl h2client) {
+ systemHeaders.setHeader("Connection", "Upgrade, HTTP2-Settings");
+ systemHeaders.setHeader("Upgrade", "h2c");
+ systemHeaders.setHeader("HTTP2-Settings", h2client.getSettingsString());
+ }
+
+ @Override
+ public boolean expectContinue() { return expectContinue; }
+
+ /** Retrieves the proxy, from the given ProxySelector, if there is one. */
+ private static Proxy retrieveProxy(ProxySelector ps, URI uri) {
+ Proxy proxy = null;
+ List<Proxy> pl = ps.select(uri);
+ if (!pl.isEmpty()) {
+ Proxy p = pl.get(0);
+ if (p.type() == Proxy.Type.HTTP)
+ proxy = p;
+ }
+ return proxy;
+ }
+
+ InetSocketAddress proxy() {
+ if (proxy == null || proxy.type() != Proxy.Type.HTTP
+ || method.equalsIgnoreCase("CONNECT")) {
+ return null;
+ }
+ return (InetSocketAddress)proxy.address();
+ }
+
+ boolean secure() { return secure; }
+
+ @Override
+ public void setProxy(Proxy proxy) {
+ assert isWebSocket;
+ this.proxy = proxy;
+ }
+
+ @Override
+ public void isWebSocket(boolean is) {
+ isWebSocket = is;
+ }
+
+ boolean isWebSocket() {
+ return isWebSocket;
+ }
+
+ @Override
+ public Optional<BodyPublisher> bodyPublisher() {
+ return requestPublisher == null ? Optional.empty()
+ : Optional.of(requestPublisher);
+ }
+
+ /**
+ * Returns the request method for this request. If not set explicitly,
+ * the default method for any request is "GET".
+ */
+ @Override
+ public String method() { return method; }
+
+ @Override
+ public URI uri() { return uri; }
+
+ @Override
+ public Optional<Duration> timeout() {
+ return timeout == null ? Optional.empty() : Optional.of(timeout);
+ }
+
+ HttpHeaders getUserHeaders() { return userHeaders; }
+
+ HttpHeadersImpl getSystemHeaders() { return systemHeaders; }
+
+ @Override
+ public Optional<HttpClient.Version> version() { return version; }
+
+ void addSystemHeader(String name, String value) {
+ systemHeaders.addHeader(name, value);
+ }
+
+ @Override
+ public void setSystemHeader(String name, String value) {
+ systemHeaders.setHeader(name, value);
+ }
+
+ InetSocketAddress getAddress() {
+ URI uri = uri();
+ if (uri == null) {
+ return authority();
+ }
+ int p = uri.getPort();
+ if (p == -1) {
+ if (uri.getScheme().equalsIgnoreCase("https")) {
+ p = 443;
+ } else {
+ p = 80;
+ }
+ }
+ final String host = uri.getHost();
+ final int port = p;
+ if (proxy() == null) {
+ PrivilegedAction<InetSocketAddress> pa = () -> new InetSocketAddress(host, port);
+ return AccessController.doPrivileged(pa);
+ } else {
+ return InetSocketAddress.createUnresolved(host, port);
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<T> extends HttpResponse<T> implements RawChannel.Provider {
+
+ final int responseCode;
+ final Exchange<T> exchange;
+ final HttpRequest initialRequest;
+ final Optional<HttpResponse<T>> previousResponse;
+ final HttpHeaders headers;
+ final SSLParameters sslParameters;
+ final URI uri;
+ final HttpClient.Version version;
+ RawChannel rawchan;
+ final HttpConnection connection;
+ final Stream<T> stream;
+ final T body;
+
+ public HttpResponseImpl(HttpRequest initialRequest,
+ Response response,
+ HttpResponse<T> previousResponse,
+ T body,
+ Exchange<T> exch) {
+ this.responseCode = response.statusCode();
+ this.exchange = exch;
+ this.initialRequest = initialRequest;
+ this.previousResponse = Optional.ofNullable(previousResponse);
+ this.headers = response.headers();
+ //this.trailers = trailers;
+ this.sslParameters = exch.client().sslParameters();
+ this.uri = response.request().uri();
+ this.version = response.version();
+ this.connection = connection(exch);
+ this.stream = null;
+ this.body = body;
+ }
+
+ private HttpConnection connection(Exchange<?> exch) {
+ if (exch == null || exch.exchImpl == null) {
+ assert responseCode == 407;
+ return null; // case of Proxy 407
+ }
+ return exch.exchImpl.connection();
+ }
+
+ private ExchangeImpl<?> exchangeImpl() {
+ return exchange != null ? exchange.exchImpl : stream;
+ }
+
+ @Override
+ public int statusCode() {
+ return responseCode;
+ }
+
+ @Override
+ public HttpRequest request() {
+ return initialRequest;
+ }
+
+ @Override
+ public Optional<HttpResponse<T>> previousResponse() {
+ return previousResponse;
+ }
+
+ @Override
+ public HttpHeaders headers() {
+ return headers;
+ }
+
+ @Override
+ public T body() {
+ return body;
+ }
+
+ @Override
+ public SSLParameters sslParameters() {
+ return sslParameters;
+ }
+
+ @Override
+ public URI uri() {
+ return uri;
+ }
+
+ @Override
+ public HttpClient.Version version() {
+ return version;
+ }
+ // keepalive flag determines whether connection is closed or kept alive
+ // by reading/skipping data
+
+ /**
+ * Returns a RawChannel that may be used for WebSocket protocol.
+ * @implNote This implementation does not support RawChannel over
+ * HTTP/2 connections.
+ * @return a RawChannel that may be used for WebSocket protocol.
+ * @throws UnsupportedOperationException if getting a RawChannel over
+ * this connection is not supported.
+ * @throws IOException if an I/O exception occurs while retrieving
+ * the channel.
+ */
+ @Override
+ public synchronized RawChannel rawChannel() throws IOException {
+ if (rawchan == null) {
+ ExchangeImpl<?> exchImpl = exchangeImpl();
+ if (!(exchImpl instanceof Http1Exchange)) {
+ // RawChannel is only used for WebSocket - and WebSocket
+ // is not supported over HTTP/2 yet, so we should not come
+ // here. Getting a RawChannel over HTTP/2 might be supported
+ // in the future, but it would entail retrieving any left over
+ // bytes that might have been read but not consumed by the
+ // HTTP/2 connection.
+ throw new UnsupportedOperationException("RawChannel is not supported over HTTP/2");
+ }
+ // Http1Exchange may have some remaining bytes in its
+ // internal buffer.
+ Supplier<ByteBuffer> initial = ((Http1Exchange<?>)exchImpl)::drainLeftOverBytes;
+ rawchan = new RawChannelImpl(exchange.client(), connection, initial);
+ }
+ return rawchan;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ String method = request().method();
+ URI uri = request().uri();
+ String uristring = uri == null ? "" : uri.toString();
+ sb.append('(')
+ .append(method)
+ .append(" ")
+ .append(uristring)
+ .append(") ")
+ .append(statusCode());
+ return sb.toString();
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<String, List<String>> map;
+
+ public static ImmutableHeaders empty() {
+ return of(emptyMap());
+ }
+
+ public static ImmutableHeaders of(Map<String, List<String>> src) {
+ return of(src, x -> true);
+ }
+
+ public static ImmutableHeaders of(HttpHeaders headers) {
+ return (headers instanceof ImmutableHeaders)
+ ? (ImmutableHeaders)headers
+ : of(headers.map());
+ }
+
+ public static ImmutableHeaders of(Map<String, List<String>> src,
+ Predicate<? super String> keyAllowed) {
+ requireNonNull(src, "src");
+ requireNonNull(keyAllowed, "keyAllowed");
+ return new ImmutableHeaders(src, headerAllowed(keyAllowed));
+ }
+
+ public static ImmutableHeaders of(Map<String, List<String>> src,
+ BiPredicate<? super String, ? super List<String>> headerAllowed) {
+ requireNonNull(src, "src");
+ requireNonNull(headerAllowed, "headerAllowed");
+ return new ImmutableHeaders(src, headerAllowed);
+ }
+
+ private ImmutableHeaders(Map<String, List<String>> src,
+ BiPredicate<? super String, ? super List<String>> headerAllowed) {
+ Map<String, List<String>> m = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ src.entrySet().stream()
+ .filter(e -> headerAllowed.test(e.getKey(), e.getValue()))
+ .forEach(e ->
+ {
+ List<String> values = new ArrayList<>(e.getValue());
+ m.put(e.getKey(), unmodifiableList(values));
+ }
+ );
+ this.map = unmodifiableMap(m);
+ }
+
+ private static BiPredicate<String, List<String>> headerAllowed(Predicate<? super String> keyAllowed) {
+ return (n,v) -> keyAllowed.test(n);
+ }
+
+ @Override
+ public Map<String, List<String>> map() {
+ return map;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<String>}. */
+public final class LineSubscriberAdapter<S extends Subscriber<? super String>,R>
+ implements BodySubscriber<R> {
+ private final CompletableFuture<R> cf = new MinimalFuture<>();
+ private final S subscriber;
+ private final Function<S, R> finisher;
+ private final Charset charset;
+ private final String eol;
+ private volatile LineSubscription downstream;
+
+ private LineSubscriberAdapter(S subscriber,
+ Function<S, R> finisher,
+ Charset charset,
+ String eol) {
+ if (eol != null && eol.isEmpty())
+ throw new IllegalArgumentException("empty line separator");
+ this.subscriber = Objects.requireNonNull(subscriber);
+ this.finisher = Objects.requireNonNull(finisher);
+ this.charset = Objects.requireNonNull(charset);
+ this.eol = eol;
+ }
+
+ @Override
+ public void onSubscribe(Subscription subscription) {
+ downstream = LineSubscription.create(subscription,
+ charset,
+ eol,
+ subscriber,
+ cf);
+ subscriber.onSubscribe(downstream);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ try {
+ downstream.submit(item);
+ } catch (Throwable t) {
+ onError(t);
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ try {
+ downstream.signalError(throwable);
+ } finally {
+ cf.completeExceptionally(throwable);
+ }
+ }
+
+ @Override
+ public void onComplete() {
+ try {
+ downstream.signalComplete();
+ } finally {
+ cf.complete(finisher.apply(subscriber));
+ }
+ }
+
+ @Override
+ public CompletionStage<R> getBody() {
+ return cf;
+ }
+
+ public static <S extends Subscriber<? super String>, R> LineSubscriberAdapter<S, R>
+ create(S subscriber, Function<S, R> finisher, Charset charset, String eol)
+ {
+ if (eol != null && eol.isEmpty())
+ throw new IllegalArgumentException("empty line separator");
+ return new LineSubscriberAdapter<>(Objects.requireNonNull(subscriber),
+ Objects.requireNonNull(finisher),
+ Objects.requireNonNull(charset),
+ eol);
+ }
+
+ static final class LineSubscription implements Flow.Subscription {
+ final Flow.Subscription upstreamSubscription;
+ final CharsetDecoder decoder;
+ final String newline;
+ final Demand downstreamDemand;
+ final ConcurrentLinkedDeque<ByteBuffer> queue;
+ final SequentialScheduler scheduler;
+ final Flow.Subscriber<? super String> upstream;
+ final CompletableFuture<?> cf;
+ private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+ private final AtomicLong demanded = new AtomicLong();
+ private volatile boolean completed;
+ private volatile boolean cancelled;
+
+ private final char[] chars = new char[1024];
+ private final ByteBuffer leftover = ByteBuffer.wrap(new byte[64]);
+ private final CharBuffer buffer = CharBuffer.wrap(chars);
+ private final StringBuilder builder = new StringBuilder();
+ private int lineCount;
+ private String nextLine;
+
+ private LineSubscription(Flow.Subscription s,
+ CharsetDecoder dec,
+ String separator,
+ Flow.Subscriber<? super String> subscriber,
+ CompletableFuture<?> completion) {
+ downstreamDemand = new Demand();
+ queue = new ConcurrentLinkedDeque<>();
+ upstreamSubscription = Objects.requireNonNull(s);
+ decoder = Objects.requireNonNull(dec);
+ newline = separator;
+ upstream = Objects.requireNonNull(subscriber);
+ cf = Objects.requireNonNull(completion);
+ scheduler = SequentialScheduler.synchronizedScheduler(this::loop);
+ }
+
+ @Override
+ public void request(long n) {
+ if (cancelled) return;
+ if (downstreamDemand.increase(n)) {
+ scheduler.runOrSchedule();
+ }
+ }
+
+ @Override
+ public void cancel() {
+ cancelled = true;
+ upstreamSubscription.cancel();
+ }
+
+ public void submit(List<ByteBuffer> list) {
+ queue.addAll(list);
+ demanded.decrementAndGet();
+ scheduler.runOrSchedule();
+ }
+
+ public void signalComplete() {
+ completed = true;
+ scheduler.runOrSchedule();
+ }
+
+ public void signalError(Throwable error) {
+ if (errorRef.compareAndSet(null,
+ Objects.requireNonNull(error))) {
+ scheduler.runOrSchedule();
+ }
+ }
+
+ // This method looks at whether some bytes where left over (in leftover)
+ // from decoding the previous buffer when the previous buffer was in
+ // underflow. If so, it takes bytes one by one from the new buffer 'in'
+ // and combines them with the leftover bytes until 'in' is exhausted or a
+ // character was produced in 'out', resolving the previous underflow.
+ // Returns true if the buffer is still in underflow, false otherwise.
+ // However, in both situation some chars might have been produced in 'out'.
+ private boolean isUnderFlow(ByteBuffer in, CharBuffer out, boolean endOfInput)
+ throws CharacterCodingException {
+ int limit = leftover.position();
+ if (limit == 0) {
+ // no leftover
+ return false;
+ } else {
+ CoderResult res = null;
+ while (in.hasRemaining()) {
+ leftover.position(limit);
+ leftover.limit(++limit);
+ leftover.put(in.get());
+ leftover.position(0);
+ res = decoder.decode(leftover, out,
+ endOfInput && !in.hasRemaining());
+ int remaining = leftover.remaining();
+ if (remaining > 0) {
+ assert leftover.position() == 0;
+ leftover.position(remaining);
+ } else {
+ leftover.position(0);
+ }
+ leftover.limit(leftover.capacity());
+ if (res.isUnderflow() && remaining > 0 && in.hasRemaining()) {
+ continue;
+ }
+ if (res.isError()) {
+ res.throwException();
+ }
+ assert !res.isOverflow();
+ return false;
+ }
+ return !endOfInput;
+ }
+ }
+
+ // extract characters from start to end and remove them from
+ // the StringBuilder
+ private static String take(StringBuilder b, int start, int end) {
+ assert start == 0;
+ String line;
+ if (end == start) return "";
+ line = b.substring(start, end);
+ b.delete(start, end);
+ return line;
+ }
+
+ // finds end of line, returns -1 if not found, or the position after
+ // the line delimiter if found, removing the delimiter in the process.
+ private static int endOfLine(StringBuilder b, String eol, boolean endOfInput) {
+ int len = b.length();
+ if (eol != null) { // delimiter explicitly specified
+ int i = b.indexOf(eol);
+ if (i >= 0) {
+ // remove the delimiter and returns the position
+ // of the char after it.
+ b.delete(i, i + eol.length());
+ return i;
+ }
+ } else { // no delimiter specified, behaves as BufferedReader::readLine
+ boolean crfound = false;
+ for (int i = 0; i < len; i++) {
+ char c = b.charAt(i);
+ if (c == '\n') {
+ // '\n' or '\r\n' found.
+ // remove the delimiter and returns the position
+ // of the char after it.
+ b.delete(crfound ? i - 1 : i, i + 1);
+ return crfound ? i - 1 : i;
+ } else if (crfound) {
+ // previous char was '\r', c != '\n'
+ assert i != 0;
+ // remove the delimiter and returns the position
+ // of the char after it.
+ b.delete(i - 1, i);
+ return i - 1;
+ }
+ crfound = c == '\r';
+ }
+ if (crfound && endOfInput) {
+ // remove the delimiter and returns the position
+ // of the char after it.
+ b.delete(len - 1, len);
+ return len - 1;
+ }
+ }
+ return endOfInput && len > 0 ? len : -1;
+ }
+
+ // Looks at whether the StringBuilder contains a line.
+ // Returns null if more character are needed.
+ private static String nextLine(StringBuilder b, String eol, boolean endOfInput) {
+ int next = endOfLine(b, eol, endOfInput);
+ return (next > -1) ? take(b, 0, next) : null;
+ }
+
+ // Attempts to read the next line. Returns the next line if
+ // the delimiter was found, null otherwise. The delimiters are
+ // consumed.
+ private String nextLine()
+ throws CharacterCodingException {
+ assert nextLine == null;
+ LINES:
+ while (nextLine == null) {
+ boolean endOfInput = completed && queue.isEmpty();
+ nextLine = nextLine(builder, newline,
+ endOfInput && leftover.position() == 0);
+ if (nextLine != null) return nextLine;
+ ByteBuffer b;
+ BUFFERS:
+ while ((b = queue.peek()) != null) {
+ if (!b.hasRemaining()) {
+ queue.poll();
+ continue BUFFERS;
+ }
+ BYTES:
+ while (b.hasRemaining()) {
+ buffer.position(0);
+ buffer.limit(buffer.capacity());
+ boolean endofInput = completed && queue.size() <= 1;
+ if (isUnderFlow(b, buffer, endofInput)) {
+ assert !b.hasRemaining();
+ if (buffer.position() > 0) {
+ buffer.flip();
+ builder.append(buffer);
+ }
+ continue BUFFERS;
+ }
+ CoderResult res = decoder.decode(b, buffer, endofInput);
+ if (res.isError()) res.throwException();
+ if (buffer.position() > 0) {
+ buffer.flip();
+ builder.append(buffer);
+ continue LINES;
+ }
+ if (res.isUnderflow() && b.hasRemaining()) {
+ //System.out.println("underflow: adding " + b.remaining() + " bytes");
+ leftover.put(b);
+ assert !b.hasRemaining();
+ continue BUFFERS;
+ }
+ }
+ }
+
+ assert queue.isEmpty();
+ if (endOfInput) {
+ // Time to cleanup: there may be some undecoded leftover bytes
+ // We need to flush them out.
+ // The decoder has been configured to replace malformed/unmappable
+ // chars with some replacement, in order to behave like
+ // InputStreamReader.
+ leftover.flip();
+ buffer.position(0);
+ buffer.limit(buffer.capacity());
+
+ // decode() must be called just before flush, even if there
+ // is nothing to decode. We must do this even if leftover
+ // has no remaining bytes.
+ CoderResult res = decoder.decode(leftover, buffer, endOfInput);
+ if (buffer.position() > 0) {
+ buffer.flip();
+ builder.append(buffer);
+ }
+ if (res.isError()) res.throwException();
+
+ // Now call decoder.flush()
+ buffer.position(0);
+ buffer.limit(buffer.capacity());
+ res = decoder.flush(buffer);
+ if (buffer.position() > 0) {
+ buffer.flip();
+ builder.append(buffer);
+ }
+ if (res.isError()) res.throwException();
+
+ // It's possible that we reach here twice - just for the
+ // purpose of checking that no bytes were left over, so
+ // we reset leftover/decoder to make the function reentrant.
+ leftover.position(0);
+ leftover.limit(leftover.capacity());
+ decoder.reset();
+
+ // if some chars were produced then this call will
+ // return them.
+ return nextLine = nextLine(builder, newline, endOfInput);
+ }
+ return null;
+ }
+ return null;
+ }
+
+ // The main sequential scheduler loop.
+ private void loop() {
+ try {
+ while (!cancelled) {
+ Throwable error = errorRef.get();
+ if (error != null) {
+ cancelled = true;
+ scheduler.stop();
+ upstream.onError(error);
+ cf.completeExceptionally(error);
+ return;
+ }
+ if (nextLine == null) nextLine = nextLine();
+ if (nextLine == null) {
+ if (completed) {
+ scheduler.stop();
+ if (leftover.position() != 0) {
+ // Underflow: not all bytes could be
+ // decoded, but no more bytes will be coming.
+ // This should not happen as we should already
+ // have got a MalformedInputException, or
+ // replaced the unmappable chars.
+ errorRef.compareAndSet(null,
+ new IllegalStateException(
+ "premature end of input ("
+ + leftover.position()
+ + " undecoded bytes)"));
+ continue;
+ } else {
+ upstream.onComplete();
+ }
+ return;
+ } else if (demanded.get() == 0
+ && !downstreamDemand.isFulfilled()) {
+ long incr = Math.max(1, downstreamDemand.get());
+ demanded.addAndGet(incr);
+ upstreamSubscription.request(incr);
+ continue;
+ } else return;
+ }
+ assert nextLine != null;
+ assert newline != null && !nextLine.endsWith(newline)
+ || !nextLine.endsWith("\n") || !nextLine.endsWith("\r");
+ if (downstreamDemand.tryDecrement()) {
+ String forward = nextLine;
+ nextLine = null;
+ upstream.onNext(forward);
+ } else return; // no demand: come back later
+ }
+ } catch (Throwable t) {
+ try {
+ upstreamSubscription.cancel();
+ } finally {
+ signalError(t);
+ }
+ }
+ }
+
+ static LineSubscription create(Flow.Subscription s,
+ Charset charset,
+ String lineSeparator,
+ Flow.Subscriber<? super String> upstream,
+ CompletableFuture<?> cf) {
+ return new LineSubscription(Objects.requireNonNull(s),
+ Objects.requireNonNull(charset).newDecoder()
+ // use the same decoder configuration than
+ // java.io.InputStreamReader
+ .onMalformedInput(CodingErrorAction.REPLACE)
+ .onUnmappableCharacter(CodingErrorAction.REPLACE),
+ lineSeparator,
+ Objects.requireNonNull(upstream),
+ Objects.requireNonNull(cf));
+ }
+ }
+}
+
--- /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<T> {
+
+ static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag.
+ static final System.Logger DEBUG_LOGGER =
+ Utils.getDebugLogger("MultiExchange"::toString, DEBUG);
+
+ private final HttpRequest userRequest; // the user request
+ private final HttpRequestImpl request; // a copy of the user request
+ final AccessControlContext acc;
+ final HttpClientImpl client;
+ final HttpResponse.BodyHandler<T> responseHandler;
+ final Executor executor;
+ final AtomicInteger attempts = new AtomicInteger();
+ HttpRequestImpl currentreq; // used for async only
+ Exchange<T> exchange; // the current exchange
+ Exchange<T> previous;
+ volatile Throwable retryCause;
+ volatile boolean expiredOnce;
+ volatile HttpResponse<T> response = null;
+
+ // Maximum number of times a request will be retried/redirected
+ // for any reason
+
+ static final int DEFAULT_MAX_ATTEMPTS = 5;
+ static final int max_attempts = Utils.getIntegerNetProperty(
+ "jdk.httpclient.redirects.retrylimit", DEFAULT_MAX_ATTEMPTS
+ );
+
+ private final List<HeaderFilter> filters;
+ TimedEvent timedEvent;
+ volatile boolean cancelled;
+ final PushGroup<T> pushGroup;
+
+ /**
+ * Filter fields. These are attached as required by filters
+ * and only used by the filter implementations. This could be
+ * generalised into Objects that are passed explicitly to the filters
+ * (one per MultiExchange object, and one per Exchange object possibly)
+ */
+ volatile AuthenticationFilter.AuthInfo serverauth, proxyauth;
+ // RedirectHandler
+ volatile int numberOfRedirects = 0;
+
+ /**
+ * MultiExchange with one final response.
+ */
+ MultiExchange(HttpRequest userRequest,
+ HttpRequestImpl requestImpl,
+ HttpClientImpl client,
+ HttpResponse.BodyHandler<T> responseHandler,
+ PushPromiseHandler<T> pushPromiseHandler,
+ AccessControlContext acc) {
+ this.previous = null;
+ this.userRequest = userRequest;
+ this.request = requestImpl;
+ this.currentreq = request;
+ this.client = client;
+ this.filters = client.filterChain();
+ this.acc = acc;
+ this.executor = client.theExecutor();
+ this.responseHandler = responseHandler;
+ if (acc != null) {
+ // Restricts the file publisher with the senders ACC, if any
+ if (responseHandler instanceof UntrustedBodyHandler)
+ ((UntrustedBodyHandler)this.responseHandler).setAccessControlContext(acc);
+ }
+
+ if (pushPromiseHandler != null) {
+ this.pushGroup = new PushGroup<>(pushPromiseHandler, request, acc);
+ } else {
+ pushGroup = null;
+ }
+
+ this.exchange = new Exchange<>(request, this);
+ }
+
+ private synchronized Exchange<T> getExchange() {
+ return exchange;
+ }
+
+ HttpClientImpl client() {
+ return client;
+ }
+
+ HttpClient.Version version() {
+ HttpClient.Version vers = request.version().orElse(client.version());
+ if (vers == HttpClient.Version.HTTP_2 && !request.secure() && request.proxy() != null)
+ vers = HttpClient.Version.HTTP_1_1;
+ return vers;
+ }
+
+ private synchronized void setExchange(Exchange<T> exchange) {
+ if (this.exchange != null && exchange != this.exchange) {
+ this.exchange.released();
+ }
+ this.exchange = exchange;
+ }
+
+ private void cancelTimer() {
+ if (timedEvent != null) {
+ client.cancelTimer(timedEvent);
+ }
+ }
+
+ private void requestFilters(HttpRequestImpl r) throws IOException {
+ Log.logTrace("Applying request filters");
+ for (HeaderFilter filter : filters) {
+ Log.logTrace("Applying {0}", filter);
+ filter.request(r, this);
+ }
+ Log.logTrace("All filters applied");
+ }
+
+ private HttpRequestImpl responseFilters(Response response) throws IOException
+ {
+ Log.logTrace("Applying response filters");
+ for (HeaderFilter filter : filters) {
+ Log.logTrace("Applying {0}", filter);
+ HttpRequestImpl newreq = filter.response(response);
+ if (newreq != null) {
+ Log.logTrace("New request: stopping filters");
+ return newreq;
+ }
+ }
+ Log.logTrace("All filters applied");
+ return null;
+ }
+
+// public void cancel() {
+// cancelled = true;
+// getExchange().cancel();
+// }
+
+ public void cancel(IOException cause) {
+ cancelled = true;
+ getExchange().cancel(cause);
+ }
+
+ public CompletableFuture<HttpResponse<T>> responseAsync() {
+ CompletableFuture<Void> start = new MinimalFuture<>();
+ CompletableFuture<HttpResponse<T>> cf = responseAsync0(start);
+ start.completeAsync( () -> null, executor); // trigger execution
+ return cf;
+ }
+
+ private CompletableFuture<HttpResponse<T>>
+ responseAsync0(CompletableFuture<Void> start) {
+ return start.thenCompose( v -> responseAsyncImpl())
+ .thenCompose((Response r) -> {
+ Exchange<T> exch = getExchange();
+ return exch.readBodyAsync(responseHandler)
+ .thenApply((T body) -> {
+ this.response =
+ new HttpResponseImpl<>(userRequest, r, this.response, body, exch);
+ return this.response;
+ });
+ });
+ }
+
+ private CompletableFuture<Response> responseAsyncImpl() {
+ CompletableFuture<Response> cf;
+ if (attempts.incrementAndGet() > max_attempts) {
+ cf = failedFuture(new IOException("Too many retries", retryCause));
+ } else {
+ if (currentreq.timeout().isPresent()) {
+ timedEvent = new TimedEvent(currentreq.timeout().get());
+ client.registerTimer(timedEvent);
+ }
+ try {
+ // 1. apply request filters
+ requestFilters(currentreq);
+ } catch (IOException e) {
+ return failedFuture(e);
+ }
+ Exchange<T> exch = getExchange();
+ // 2. get response
+ cf = exch.responseAsync()
+ .thenCompose((Response response) -> {
+ HttpRequestImpl newrequest;
+ try {
+ // 3. apply response filters
+ newrequest = responseFilters(response);
+ } catch (IOException e) {
+ return failedFuture(e);
+ }
+ // 4. check filter result and repeat or continue
+ if (newrequest == null) {
+ if (attempts.get() > 1) {
+ Log.logError("Succeeded on attempt: " + attempts);
+ }
+ return completedFuture(response);
+ } else {
+ this.response =
+ new HttpResponseImpl<>(currentreq, response, this.response, null, exch);
+ Exchange<T> oldExch = exch;
+ return exch.ignoreBody().handle((r,t) -> {
+ currentreq = newrequest;
+ expiredOnce = false;
+ setExchange(new Exchange<>(currentreq, this, acc));
+ return responseAsyncImpl();
+ }).thenCompose(Function.identity());
+ } })
+ .handle((response, ex) -> {
+ // 5. handle errors and cancel any timer set
+ cancelTimer();
+ if (ex == null) {
+ assert response != null;
+ return completedFuture(response);
+ }
+ // all exceptions thrown are handled here
+ CompletableFuture<Response> errorCF = getExceptionalCF(ex);
+ if (errorCF == null) {
+ return responseAsyncImpl();
+ } else {
+ return errorCF;
+ } })
+ .thenCompose(Function.identity());
+ }
+ return cf;
+ }
+
+ /**
+ * Takes a Throwable and returns a suitable CompletableFuture that is
+ * completed exceptionally, or null.
+ */
+ private CompletableFuture<Response> getExceptionalCF(Throwable t) {
+ if ((t instanceof CompletionException) || (t instanceof ExecutionException)) {
+ if (t.getCause() != null) {
+ t = t.getCause();
+ }
+ }
+ if (cancelled && t instanceof IOException) {
+ t = new HttpTimeoutException("request timed out");
+ } else if (t instanceof ConnectionExpiredException) {
+ // allow the retry mechanism to do its work
+ // ####: method (GET,HEAD, not POST?), no bytes written or read ( differentiate? )
+ if (t.getCause() != null) retryCause = t.getCause();
+ if (!expiredOnce) {
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "MultiExchange: ConnectionExpiredException (async): retrying...",
+ t);
+ expiredOnce = true;
+ return null;
+ } else {
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "MultiExchange: ConnectionExpiredException (async): already retried once.",
+ t);
+ if (t.getCause() != null) t = t.getCause();
+ }
+ }
+ return failedFuture(t);
+ }
+
+ class TimedEvent extends TimeoutEvent {
+ TimedEvent(Duration duration) {
+ super(duration);
+ }
+ @Override
+ public void handle() {
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Cancelling MultiExchange due to timeout for request %s",
+ request);
+ cancel(new HttpTimeoutException("request timed out"));
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<Void> cf;
+
+ ConnectEvent(CompletableFuture<Void> cf) {
+ this.cf = cf;
+ }
+
+ @Override
+ public SelectableChannel channel() {
+ return chan;
+ }
+
+ @Override
+ public int interestOps() {
+ return SelectionKey.OP_CONNECT;
+ }
+
+ @Override
+ public void handle() {
+ try {
+ assert !connected : "Already connected";
+ assert !chan.isBlocking() : "Unexpected blocking channel";
+ debug.log(Level.DEBUG, "ConnectEvent: finishing connect");
+ boolean finished = chan.finishConnect();
+ assert finished : "Expected channel to be connected";
+ debug.log(Level.DEBUG,
+ "ConnectEvent: connect finished: %s Local addr: %s", finished, chan.getLocalAddress());
+ connected = true;
+ // complete async since the event runs on the SelectorManager thread
+ cf.completeAsync(() -> null, client().theExecutor());
+ } catch (Throwable e) {
+ client().theExecutor().execute( () -> cf.completeExceptionally(e));
+ }
+ }
+
+ @Override
+ public void abort(IOException ioe) {
+ close();
+ client().theExecutor().execute( () -> cf.completeExceptionally(ioe));
+ }
+ }
+
+ @Override
+ public CompletableFuture<Void> connectAsync() {
+ CompletableFuture<Void> cf = new MinimalFuture<>();
+ try {
+ assert !connected : "Already connected";
+ assert !chan.isBlocking() : "Unexpected blocking channel";
+ boolean finished = false;
+ PrivilegedExceptionAction<Boolean> pa = () -> chan.connect(address);
+ try {
+ finished = AccessController.doPrivileged(pa);
+ } catch (PrivilegedActionException e) {
+ cf.completeExceptionally(e.getCause());
+ }
+ if (finished) {
+ debug.log(Level.DEBUG, "connect finished without blocking");
+ connected = true;
+ cf.complete(null);
+ } else {
+ debug.log(Level.DEBUG, "registering connect event");
+ client().registerEvent(new ConnectEvent(cf));
+ }
+ } catch (Throwable throwable) {
+ cf.completeExceptionally(throwable);
+ }
+ return cf;
+ }
+
+ @Override
+ SocketChannel channel() {
+ return chan;
+ }
+
+ @Override
+ final FlowTube getConnectionFlow() {
+ return tube;
+ }
+
+ PlainHttpConnection(InetSocketAddress addr, HttpClientImpl client) {
+ super(addr, client);
+ try {
+ this.chan = SocketChannel.open();
+ chan.configureBlocking(false);
+ int bufsize = client.getReceiveBufferSize();
+ if (!trySetReceiveBufferSize(bufsize)) {
+ trySetReceiveBufferSize(256*1024);
+ }
+ chan.setOption(StandardSocketOptions.TCP_NODELAY, true);
+ // wrap the connected channel in a Tube for async reading and writing
+ tube = new SocketTube(client(), chan, Utils::getBuffer);
+ } catch (IOException e) {
+ throw new InternalError(e);
+ }
+ }
+
+ private boolean trySetReceiveBufferSize(int bufsize) {
+ try {
+ chan.setOption(StandardSocketOptions.SO_RCVBUF, bufsize);
+ return true;
+ } catch(IOException x) {
+ debug.log(Level.DEBUG,
+ "Failed to set receive buffer size to %d on %s",
+ bufsize, chan);
+ }
+ return false;
+ }
+
+ @Override
+ HttpPublisher publisher() { return writePublisher; }
+
+
+ @Override
+ public String toString() {
+ return "PlainHttpConnection: " + super.toString();
+ }
+
+ /**
+ * Closes this connection
+ */
+ @Override
+ public synchronized void close() {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ try {
+ Log.logTrace("Closing: " + toString());
+ chan.close();
+ } catch (IOException e) {}
+ }
+
+ @Override
+ void shutdownInput() throws IOException {
+ debug.log(Level.DEBUG, "Shutting down input");
+ chan.shutdownInput();
+ }
+
+ @Override
+ void shutdownOutput() throws IOException {
+ debug.log(Level.DEBUG, "Shutting down output");
+ chan.shutdownOutput();
+ }
+
+ @Override
+ ConnectionPool.CacheKey cacheKey() {
+ return new ConnectionPool.CacheKey(address, null);
+ }
+
+ @Override
+ synchronized boolean connected() {
+ return connected;
+ }
+
+
+ @Override
+ boolean isSecure() {
+ return false;
+ }
+
+ @Override
+ boolean isProxied() {
+ return false;
+ }
+
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ private static final class PlainDetachedChannel
+ extends DetachedConnectionChannel {
+ final PlainHttpConnection plainConnection;
+ boolean closed;
+ PlainDetachedChannel(PlainHttpConnection conn) {
+ // We're handing the connection channel over to a web socket.
+ // We need the selector manager's thread to stay alive until
+ // the WebSocket is closed.
+ conn.client().webSocketOpen();
+ this.plainConnection = conn;
+ }
+
+ @Override
+ SocketChannel channel() {
+ return plainConnection.channel();
+ }
+
+ @Override
+ ByteBuffer read() throws IOException {
+ ByteBuffer dst = ByteBuffer.allocate(8192);
+ int n = readImpl(dst);
+ if (n > 0) {
+ return dst;
+ } else if (n == 0) {
+ return Utils.EMPTY_BYTEBUFFER;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void close() {
+ HttpClientImpl client = plainConnection.client();
+ try {
+ plainConnection.close();
+ } finally {
+ // notify the HttpClientImpl that the websocket is no
+ // no longer operating.
+ synchronized(this) {
+ if (closed == true) return;
+ closed = true;
+ }
+ client.webSocketClose();
+ }
+ }
+
+ @Override
+ public long write(ByteBuffer[] buffers, int start, int number)
+ throws IOException
+ {
+ return channel().write(buffers, start, number);
+ }
+
+ @Override
+ public void shutdownInput() throws IOException {
+ plainConnection.shutdownInput();
+ }
+
+ @Override
+ public void shutdownOutput() throws IOException {
+ plainConnection.shutdownOutput();
+ }
+
+ private int readImpl(ByteBuffer buf) throws IOException {
+ int mark = buf.position();
+ int n;
+ n = channel().read(buf);
+ if (n == -1) {
+ return -1;
+ }
+ Utils.flipToMark(buf, mark);
+ return n;
+ }
+ }
+
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ @Override
+ DetachedConnectionChannel detachChannel() {
+ client().cancelRegistration(channel());
+ return new PlainDetachedChannel(this);
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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);
+ }
+}
--- /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<Void> connectAsync() {
+ debug.log(Level.DEBUG, "Connecting plain connection");
+ return delegate.connectAsync()
+ .thenCompose((Void v) -> {
+ debug.log(Level.DEBUG, "sending HTTP/1.1 CONNECT");
+ HttpClientImpl client = client();
+ assert client != null;
+ HttpRequestImpl req = new HttpRequestImpl("CONNECT", address, proxyHeaders);
+ MultiExchange<Void> mulEx = new MultiExchange<>(null, req,
+ client, discard(), null, null);
+ Exchange<Void> connectExchange = new Exchange<>(req, mulEx);
+
+ return connectExchange
+ .responseAsyncImpl(delegate)
+ .thenCompose((Response resp) -> {
+ CompletableFuture<Void> cf = new MinimalFuture<>();
+ debug.log(Level.DEBUG, "got response: %d", resp.statusCode());
+ if (resp.statusCode() == 407) {
+ return connectExchange.ignoreBody().handle((r,t) -> {
+ // close delegate after reading body: we won't
+ // be reusing that connection anyway.
+ delegate.close();
+ ProxyAuthenticationRequired authenticationRequired =
+ new ProxyAuthenticationRequired(resp);
+ cf.completeExceptionally(authenticationRequired);
+ return cf;
+ }).thenCompose(Function.identity());
+ } else if (resp.statusCode() != 200) {
+ delegate.close();
+ cf.completeExceptionally(new IOException(
+ "Tunnel failed, got: "+ resp.statusCode()));
+ } else {
+ // get the initial/remaining bytes
+ ByteBuffer b = ((Http1Exchange<?>)connectExchange.exchImpl).drainLeftOverBytes();
+ int remaining = b.remaining();
+ assert remaining == 0: "Unexpected remaining: " + remaining;
+ connected = true;
+ cf.complete(null);
+ }
+ return cf;
+ });
+ });
+ }
+
+ @Override
+ boolean isTunnel() { return true; }
+
+ @Override
+ HttpPublisher publisher() { return delegate.publisher(); }
+
+ @Override
+ boolean connected() {
+ return connected;
+ }
+
+ @Override
+ SocketChannel channel() {
+ return delegate.channel();
+ }
+
+ @Override
+ FlowTube getConnectionFlow() {
+ return delegate.getConnectionFlow();
+ }
+
+ @Override
+ ConnectionPool.CacheKey cacheKey() {
+ return new ConnectionPool.CacheKey(null, proxyAddr);
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ connected = false;
+ }
+
+ @Override
+ void shutdownInput() throws IOException {
+ delegate.shutdownInput();
+ }
+
+ @Override
+ void shutdownOutput() throws IOException {
+ delegate.shutdownOutput();
+ }
+
+ @Override
+ boolean isSecure() {
+ return false;
+ }
+
+ @Override
+ boolean isProxied() {
+ return true;
+ }
+
+ // Support for WebSocket/RawChannelImpl which unfortunately
+ // still depends on synchronous read/writes.
+ // It should be removed when RawChannelImpl moves to using asynchronous APIs.
+ @Override
+ DetachedConnectionChannel detachChannel() {
+ return delegate.detachChannel();
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<Void> pa = () -> { r.run(); return null; };
+ AccessController.doPrivileged(pa, acc);
+ }
+ }
+
+ @Override
+ public void execute(Runnable r) {
+ executor.execute(new PrivilegedRunnable(r, acc));
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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;
+ }
+}
--- /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<T> implements Flow.Publisher<T> {
+
+ // Only one of `iterable` and `throwable` can be non-null. throwable is
+ // non-null when an error has been encountered, by the creator of
+ // PullPublisher, while subscribing the subscriber, but before subscribe has
+ // completed.
+ private final Iterable<T> iterable;
+ private final Throwable throwable;
+
+ PullPublisher(Iterable<T> iterable, Throwable throwable) {
+ this.iterable = iterable;
+ this.throwable = throwable;
+ }
+
+ PullPublisher(Iterable<T> iterable) {
+ this(iterable, null);
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super T> subscriber) {
+ Subscription sub;
+ if (throwable != null) {
+ assert iterable == null : "non-null iterable: " + iterable;
+ sub = new Subscription(subscriber, null, throwable);
+ } else {
+ assert throwable == null : "non-null exception: " + throwable;
+ sub = new Subscription(subscriber, iterable.iterator(), null);
+ }
+ subscriber.onSubscribe(sub);
+
+ if (throwable != null) {
+ sub.pullScheduler.runOrSchedule();
+ }
+ }
+
+ private class Subscription implements Flow.Subscription {
+
+ private final Flow.Subscriber<? super T> subscriber;
+ private final Iterator<T> iter;
+ private volatile boolean completed;
+ private volatile boolean cancelled;
+ private volatile Throwable error;
+ final SequentialScheduler pullScheduler = new SequentialScheduler(new PullTask());
+ private final Demand demand = new Demand();
+
+ Subscription(Flow.Subscriber<? super T> subscriber,
+ Iterator<T> iter,
+ Throwable throwable) {
+ this.subscriber = subscriber;
+ this.iter = iter;
+ this.error = throwable;
+ }
+
+ final class PullTask extends SequentialScheduler.CompleteRestartableTask {
+ @Override
+ protected void run() {
+ if (completed || cancelled) {
+ return;
+ }
+
+ Throwable t = error;
+ if (t != null) {
+ completed = true;
+ pullScheduler.stop();
+ subscriber.onError(t);
+ return;
+ }
+
+ while (demand.tryDecrement() && !cancelled) {
+ if (!iter.hasNext()) {
+ break;
+ } else {
+ subscriber.onNext(iter.next());
+ }
+ }
+ if (!iter.hasNext() && !cancelled) {
+ completed = true;
+ pullScheduler.stop();
+ subscriber.onComplete();
+ }
+ }
+ }
+
+ @Override
+ public void request(long n) {
+ if (cancelled)
+ return; // no-op
+
+ if (n <= 0) {
+ error = new IllegalArgumentException("illegal non-positive request:" + n);
+ } else {
+ demand.increase(n);
+ }
+ pullScheduler.runOrSchedule();
+ }
+
+ @Override
+ public void cancel() {
+ cancelled = true;
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<T> {
+ private final HttpRequest initiatingRequest;
+
+ final CompletableFuture<Void> noMorePushesCF;
+
+ volatile Throwable error; // any exception that occurred during pushes
+
+ // user's subscriber object
+ final PushPromiseHandler<T> pushPromiseHandler;
+
+ private final AccessControlContext acc;
+
+ int numberOfPushes;
+ int remainingPushes;
+ boolean noMorePushes = false;
+
+ PushGroup(PushPromiseHandler<T> pushPromiseHandler,
+ HttpRequestImpl initiatingRequest,
+ AccessControlContext acc) {
+ this(pushPromiseHandler, initiatingRequest, new MinimalFuture<>(), acc);
+ }
+
+ // Check mainBodyHandler before calling nested constructor.
+ private PushGroup(HttpResponse.PushPromiseHandler<T> pushPromiseHandler,
+ HttpRequestImpl initiatingRequest,
+ CompletableFuture<HttpResponse<T>> mainResponse,
+ AccessControlContext acc) {
+ this.noMorePushesCF = new MinimalFuture<>();
+ this.pushPromiseHandler = pushPromiseHandler;
+ this.initiatingRequest = initiatingRequest;
+ // Restricts the file publisher with the senders ACC, if any
+ if (pushPromiseHandler instanceof UntrustedBodyHandler)
+ ((UntrustedBodyHandler)this.pushPromiseHandler).setAccessControlContext(acc);
+ this.acc = acc;
+ }
+
+ interface Acceptor<T> {
+ BodyHandler<T> bodyHandler();
+ CompletableFuture<HttpResponse<T>> cf();
+ boolean accepted();
+ }
+
+ private static class AcceptorImpl<T> implements Acceptor<T> {
+ private volatile HttpResponse.BodyHandler<T> bodyHandler;
+ private volatile CompletableFuture<HttpResponse<T>> cf;
+
+ CompletableFuture<HttpResponse<T>> accept(BodyHandler<T> bodyHandler) {
+ Objects.requireNonNull(bodyHandler);
+ if (this.bodyHandler != null)
+ throw new IllegalStateException("non-null bodyHandler");
+ this.bodyHandler = bodyHandler;
+ cf = new MinimalFuture<>();
+ return cf;
+ }
+
+ @Override public BodyHandler<T> bodyHandler() { return bodyHandler; }
+
+ @Override public CompletableFuture<HttpResponse<T>> cf() { return cf; }
+
+ @Override public boolean accepted() { return cf != null; }
+ }
+
+ Acceptor<T> acceptPushRequest(HttpRequest pushRequest) {
+ AcceptorImpl<T> acceptor = new AcceptorImpl<>();
+
+ pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, acceptor::accept);
+
+ synchronized (this) {
+ if (acceptor.accepted()) {
+ if (acceptor.bodyHandler instanceof UntrustedBodyHandler) {
+ ((UntrustedBodyHandler) acceptor.bodyHandler).setAccessControlContext(acc);
+ }
+ numberOfPushes++;
+ remainingPushes++;
+ }
+ return acceptor;
+ }
+ }
+
+ // This is called when the main body response completes because it means
+ // no more PUSH_PROMISEs are possible
+
+ synchronized void noMorePushes(boolean noMore) {
+ noMorePushes = noMore;
+ checkIfCompleted();
+ noMorePushesCF.complete(null);
+ }
+
+ synchronized CompletableFuture<Void> pushesCF() {
+ return noMorePushesCF;
+ }
+
+ synchronized boolean noMorePushes() {
+ return noMorePushes;
+ }
+
+ synchronized void pushCompleted() {
+ remainingPushes--;
+ checkIfCompleted();
+ }
+
+ synchronized void checkIfCompleted() {
+ if (Log.trace()) {
+ Log.logTrace("PushGroup remainingPushes={0} error={1} noMorePushes={2}",
+ remainingPushes,
+ (error==null)?error:error.getClass().getSimpleName(),
+ noMorePushes);
+ }
+ if (remainingPushes == 0 && error == null && noMorePushes) {
+ if (Log.trace()) {
+ Log.logTrace("push completed");
+ }
+ }
+ }
+
+ synchronized void pushError(Throwable t) {
+ if (t == null) {
+ return;
+ }
+ this.error = t;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<ByteBuffer> initial;
+
+ RawChannelImpl(HttpClientImpl client,
+ HttpConnection connection,
+ Supplier<ByteBuffer> initial)
+ throws IOException
+ {
+ this.client = client;
+ this.detachedChannel = connection.detachChannel();
+ this.initial = initial;
+
+ SocketChannel chan = connection.channel();
+ client.cancelRegistration(chan);
+ // Constructing a RawChannel is supposed to have a "hand over"
+ // semantics, in other words if construction fails, the channel won't be
+ // needed by anyone, in which case someone still needs to close it
+ try {
+ chan.configureBlocking(false);
+ } catch (IOException e) {
+ try {
+ chan.close();
+ } catch (IOException e1) {
+ e.addSuppressed(e1);
+ } finally {
+ detachedChannel.close();
+ }
+ throw e;
+ }
+ }
+
+ private class NonBlockingRawAsyncEvent extends AsyncEvent {
+
+ private final RawEvent re;
+
+ NonBlockingRawAsyncEvent(RawEvent re) {
+ // !BLOCKING & !REPEATING
+ this.re = re;
+ }
+
+ @Override
+ public SelectableChannel channel() {
+ return detachedChannel.channel();
+ }
+
+ @Override
+ public int interestOps() {
+ return re.interestOps();
+ }
+
+ @Override
+ public void handle() {
+ re.handle();
+ }
+
+ @Override
+ public void abort(IOException ioe) { }
+ }
+
+ @Override
+ public void registerEvent(RawEvent event) throws IOException {
+ client.registerEvent(new NonBlockingRawAsyncEvent(event));
+ }
+
+ @Override
+ public ByteBuffer read() throws IOException {
+ assert !detachedChannel.channel().isBlocking();
+ // connection.read() will no longer be available.
+ return detachedChannel.read();
+ }
+
+ @Override
+ public ByteBuffer initialByteBuffer() {
+ synchronized (initialLock) {
+ if (initial == null) {
+ throw new IllegalStateException();
+ }
+ ByteBuffer ref = initial.get();
+ ref = ref.hasRemaining() ? Utils.copy(ref)
+ : Utils.EMPTY_BYTEBUFFER;
+ initial = null;
+ return ref;
+ }
+ }
+
+ @Override
+ public long write(ByteBuffer[] src, int offset, int len) throws IOException {
+ // this makes the whitebox driver test fail.
+ return detachedChannel.write(src, offset, len);
+ }
+
+ @Override
+ public void shutdownInput() throws IOException {
+ detachedChannel.shutdownInput();
+ }
+
+ @Override
+ public void shutdownOutput() throws IOException {
+ detachedChannel.shutdownOutput();
+ }
+
+ @Override
+ public void close() throws IOException {
+ detachedChannel.close();
+ }
+
+ @Override
+ public String toString() {
+ return super.toString()+"("+ detachedChannel.toString() + ")";
+ }
+
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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();
+ }
+ }
+}
--- /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<ByteBuffer> delegate;
+ private final int length;
+ private final byte[] content;
+ private final int offset;
+ private final int bufSize;
+
+ public ByteArrayPublisher(byte[] content) {
+ this(content, 0, content.length);
+ }
+
+ public ByteArrayPublisher(byte[] content, int offset, int length) {
+ this(content, offset, length, Utils.BUFSIZE);
+ }
+
+ /* bufSize exposed for testing purposes */
+ ByteArrayPublisher(byte[] content, int offset, int length, int bufSize) {
+ this.content = content;
+ this.offset = offset;
+ this.length = length;
+ this.bufSize = bufSize;
+ }
+
+ List<ByteBuffer> copy(byte[] content, int offset, int length) {
+ List<ByteBuffer> bufs = new ArrayList<>();
+ while (length > 0) {
+ ByteBuffer b = ByteBuffer.allocate(Math.min(bufSize, length));
+ int max = b.capacity();
+ int tocopy = Math.min(max, length);
+ b.put(content, offset, tocopy);
+ offset += tocopy;
+ length -= tocopy;
+ b.flip();
+ bufs.add(b);
+ }
+ return bufs;
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+ List<ByteBuffer> copy = copy(content, offset, length);
+ this.delegate = new PullPublisher<>(copy);
+ delegate.subscribe(subscriber);
+ }
+
+ @Override
+ public long contentLength() {
+ return length;
+ }
+ }
+
+ // This implementation has lots of room for improvement.
+ public static class IterablePublisher implements BodyPublisher {
+ private volatile Flow.Publisher<ByteBuffer> delegate;
+ private final Iterable<byte[]> content;
+ private volatile long contentLength;
+
+ public IterablePublisher(Iterable<byte[]> content) {
+ this.content = Objects.requireNonNull(content);
+ }
+
+ // The ByteBufferIterator will iterate over the byte[] arrays in
+ // the content one at the time.
+ //
+ class ByteBufferIterator implements Iterator<ByteBuffer> {
+ final ConcurrentLinkedQueue<ByteBuffer> buffers = new ConcurrentLinkedQueue<>();
+ final Iterator<byte[]> iterator = content.iterator();
+ @Override
+ public boolean hasNext() {
+ return !buffers.isEmpty() || iterator.hasNext();
+ }
+
+ @Override
+ public ByteBuffer next() {
+ ByteBuffer buffer = buffers.poll();
+ while (buffer == null) {
+ copy();
+ buffer = buffers.poll();
+ }
+ return buffer;
+ }
+
+ ByteBuffer getBuffer() {
+ return Utils.getBuffer();
+ }
+
+ void copy() {
+ byte[] bytes = iterator.next();
+ int length = bytes.length;
+ if (length == 0 && iterator.hasNext()) {
+ // avoid inserting empty buffers, except
+ // if that's the last.
+ return;
+ }
+ int offset = 0;
+ do {
+ ByteBuffer b = getBuffer();
+ int max = b.capacity();
+
+ int tocopy = Math.min(max, length);
+ b.put(bytes, offset, tocopy);
+ offset += tocopy;
+ length -= tocopy;
+ b.flip();
+ buffers.add(b);
+ } while (length > 0);
+ }
+ }
+
+ public Iterator<ByteBuffer> iterator() {
+ return new ByteBufferIterator();
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+ Iterable<ByteBuffer> iterable = this::iterator;
+ this.delegate = new PullPublisher<>(iterable);
+ delegate.subscribe(subscriber);
+ }
+
+ static long computeLength(Iterable<byte[]> bytes) {
+ long len = 0;
+ for (byte[] b : bytes) {
+ len = Math.addExact(len, (long)b.length);
+ }
+ return len;
+ }
+
+ @Override
+ public long contentLength() {
+ if (contentLength == 0) {
+ synchronized(this) {
+ if (contentLength == 0) {
+ contentLength = computeLength(content);
+ }
+ }
+ }
+ return contentLength;
+ }
+ }
+
+ public static class StringPublisher extends ByteArrayPublisher {
+ public StringPublisher(String content, Charset charset) {
+ super(content.getBytes(charset));
+ }
+ }
+
+ public static class EmptyPublisher implements BodyPublisher {
+ private final Flow.Publisher<ByteBuffer> delegate =
+ new PullPublisher<ByteBuffer>(Collections.emptyList(), null);
+
+ @Override
+ public long contentLength() {
+ return 0;
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+ delegate.subscribe(subscriber);
+ }
+ }
+
+ public static class FilePublisher implements BodyPublisher {
+ private final File file;
+ private volatile AccessControlContext acc;
+
+ public FilePublisher(Path name) {
+ file = name.toFile();
+ }
+
+ void setAccessControlContext(AccessControlContext acc) {
+ this.acc = acc;
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+ if (System.getSecurityManager() != null && acc == null)
+ throw new InternalError(
+ "Unexpected null acc when security manager has been installed");
+
+ InputStream is;
+ try {
+ PrivilegedExceptionAction<FileInputStream> pa =
+ () -> new FileInputStream(file);
+ is = AccessController.doPrivileged(pa, acc);
+ } catch (PrivilegedActionException pae) {
+ throw new UncheckedIOException((IOException)pae.getCause());
+ }
+ PullPublisher<ByteBuffer> publisher =
+ new PullPublisher<>(() -> new StreamIterator(is));
+ publisher.subscribe(subscriber);
+ }
+
+ @Override
+ public long contentLength() {
+ assert System.getSecurityManager() != null ? acc != null: true;
+ PrivilegedAction<Long> pa = () -> file.length();
+ return AccessController.doPrivileged(pa, acc);
+ }
+ }
+
+ /**
+ * Reads one buffer ahead all the time, blocking in hasNext()
+ */
+ public static class StreamIterator implements Iterator<ByteBuffer> {
+ final InputStream is;
+ final Supplier<? extends ByteBuffer> bufSupplier;
+ volatile ByteBuffer nextBuffer;
+ volatile boolean need2Read = true;
+ volatile boolean haveNext;
+
+ StreamIterator(InputStream is) {
+ this(is, Utils::getBuffer);
+ }
+
+ StreamIterator(InputStream is, Supplier<? extends ByteBuffer> bufSupplier) {
+ this.is = is;
+ this.bufSupplier = bufSupplier;
+ }
+
+// Throwable error() {
+// return error;
+// }
+
+ private int read() {
+ nextBuffer = bufSupplier.get();
+ nextBuffer.clear();
+ byte[] buf = nextBuffer.array();
+ int offset = nextBuffer.arrayOffset();
+ int cap = nextBuffer.capacity();
+ try {
+ int n = is.read(buf, offset, cap);
+ if (n == -1) {
+ is.close();
+ return -1;
+ }
+ //flip
+ nextBuffer.limit(n);
+ nextBuffer.position(0);
+ return n;
+ } catch (IOException ex) {
+ return -1;
+ }
+ }
+
+ @Override
+ public synchronized boolean hasNext() {
+ if (need2Read) {
+ haveNext = read() != -1;
+ if (haveNext) {
+ need2Read = false;
+ }
+ return haveNext;
+ }
+ return haveNext;
+ }
+
+ @Override
+ public synchronized ByteBuffer next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ need2Read = true;
+ return nextBuffer;
+ }
+
+ }
+
+ public static class InputStreamPublisher implements BodyPublisher {
+ private final Supplier<? extends InputStream> streamSupplier;
+
+ public InputStreamPublisher(Supplier<? extends InputStream> streamSupplier) {
+ this.streamSupplier = Objects.requireNonNull(streamSupplier);
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+ PullPublisher<ByteBuffer> publisher;
+ InputStream is = streamSupplier.get();
+ if (is == null) {
+ Throwable t = new IOException("streamSupplier returned null");
+ publisher = new PullPublisher<>(null, t);
+ } else {
+ publisher = new PullPublisher<>(iterableOf(is), null);
+ }
+ publisher.subscribe(subscriber);
+ }
+
+ protected Iterable<ByteBuffer> iterableOf(InputStream is) {
+ return () -> new StreamIterator(is);
+ }
+
+ @Override
+ public long contentLength() {
+ return -1;
+ }
+ }
+
+ public static final class PublisherAdapter implements BodyPublisher {
+
+ private final Publisher<? extends ByteBuffer> publisher;
+ private final long contentLength;
+
+ public PublisherAdapter(Publisher<? extends ByteBuffer> publisher,
+ long contentLength) {
+ this.publisher = Objects.requireNonNull(publisher);
+ this.contentLength = contentLength;
+ }
+
+ @Override
+ public final long contentLength() {
+ return contentLength;
+ }
+
+ @Override
+ public final void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
+ publisher.subscribe(subscriber);
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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();
+ }
+}
--- /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<Path> {
+ private final Path file;
+ private final OpenOption[]openOptions;
+ private volatile AccessControlContext acc;
+
+ public PathBodyHandler(Path file, OpenOption... openOptions) {
+ this.file = file;
+ this.openOptions = openOptions;
+ }
+
+ @Override
+ public void setAccessControlContext(AccessControlContext acc) {
+ this.acc = acc;
+ }
+
+ @Override
+ public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
+ PathSubscriber bs = (PathSubscriber) asFileImpl(file, openOptions);
+ bs.setAccessControlContext(acc);
+ return bs;
+ }
+ }
+
+ /** With push promise Map implementation */
+ public static class PushPromisesHandlerWithMap<T>
+ implements HttpResponse.PushPromiseHandler<T>
+ {
+ private final ConcurrentMap<HttpRequest,CompletableFuture<HttpResponse<T>>> pushPromisesMap;
+ private final Function<HttpRequest,BodyHandler<T>> pushPromiseHandler;
+
+ public PushPromisesHandlerWithMap(Function<HttpRequest,BodyHandler<T>> pushPromiseHandler,
+ ConcurrentMap<HttpRequest,CompletableFuture<HttpResponse<T>>> pushPromisesMap) {
+ this.pushPromiseHandler = pushPromiseHandler;
+ this.pushPromisesMap = pushPromisesMap;
+ }
+
+ @Override
+ public void applyPushPromise(
+ HttpRequest initiatingRequest, HttpRequest pushRequest,
+ Function<BodyHandler<T>,CompletableFuture<HttpResponse<T>>> acceptor)
+ {
+ URI initiatingURI = initiatingRequest.uri();
+ URI pushRequestURI = pushRequest.uri();
+ if (!initiatingURI.getHost().equalsIgnoreCase(pushRequestURI.getHost()))
+ return;
+
+ int initiatingPort = initiatingURI.getPort();
+ if (initiatingPort == -1 ) {
+ if ("https".equalsIgnoreCase(initiatingURI.getScheme()))
+ initiatingPort = 443;
+ else
+ initiatingPort = 80;
+ }
+ int pushPort = pushRequestURI.getPort();
+ if (pushPort == -1 ) {
+ if ("https".equalsIgnoreCase(pushRequestURI.getScheme()))
+ pushPort = 443;
+ else
+ pushPort = 80;
+ }
+ if (initiatingPort != pushPort)
+ return;
+
+ CompletableFuture<HttpResponse<T>> cf =
+ acceptor.apply(pushPromiseHandler.apply(pushRequest));
+ pushPromisesMap.put(pushRequest, cf);
+ }
+ }
+
+ // Similar to Path body handler, but for file download. Supports setting ACC.
+ public static class FileDownloadBodyHandler implements UntrustedBodyHandler<Path> {
+ private final Path directory;
+ private final OpenOption[]openOptions;
+ private volatile AccessControlContext acc;
+
+ public FileDownloadBodyHandler(Path directory, OpenOption... openOptions) {
+ this.directory = directory;
+ this.openOptions = openOptions;
+ }
+
+ @Override
+ public void setAccessControlContext(AccessControlContext acc) {
+ this.acc = acc;
+ }
+
+ @Override
+ public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
+ String dispoHeader = headers.firstValue("Content-Disposition")
+ .orElseThrow(() -> unchecked(new IOException("No Content-Disposition")));
+ if (!dispoHeader.startsWith("attachment;")) {
+ throw unchecked(new IOException("Unknown Content-Disposition type"));
+ }
+ int n = dispoHeader.indexOf("filename=");
+ if (n == -1) {
+ throw unchecked(new IOException("Bad Content-Disposition type"));
+ }
+ int lastsemi = dispoHeader.lastIndexOf(';');
+ String disposition;
+ if (lastsemi < n) {
+ disposition = dispoHeader.substring(n + 9);
+ } else {
+ disposition = dispoHeader.substring(n + 9, lastsemi);
+ }
+ Path file = Paths.get(directory.toString(), disposition);
+
+ PathSubscriber bs = (PathSubscriber)asFileImpl(file, openOptions);
+ bs.setAccessControlContext(acc);
+ return bs;
+ }
+ }
+
+ // no security check
+ private static BodySubscriber<Path> asFileImpl(Path file, OpenOption... openOptions) {
+ return new ResponseSubscribers.PathSubscriber(file, openOptions);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<ByteBuffer> {
+ void onSubscribe(AbstractSubscription sub);
+ }
+
+ // Returns a parser that will take care of parsing the received byte
+ // buffers and forward them to the BodySubscriber.
+ // When the parser is done, it will call onComplete.
+ // If parsing was successful, the throwable parameter will be null.
+ // Otherwise it will be the exception that occurred
+ // Note: revisit: it might be better to use a CompletableFuture than
+ // a completion handler.
+ BodyParser getBodyParser(Consumer<Throwable> onComplete)
+ throws IOException {
+ if (contentChunked()) {
+ return new ChunkedBodyParser(onComplete);
+ } else {
+ return new FixedLengthBodyParser(contentLength, onComplete);
+ }
+ }
+
+
+ static enum ChunkState {READING_LENGTH, READING_DATA, DONE}
+ class ChunkedBodyParser implements BodyParser {
+ final ByteBuffer READMORE = Utils.EMPTY_BYTEBUFFER;
+ final Consumer<Throwable> onComplete;
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ final String dbgTag = ResponseContent.this.dbgTag + "/ChunkedBodyParser";
+
+ volatile Throwable closedExceptionally;
+ volatile int partialChunklen = 0; // partially read chunk len
+ volatile int chunklen = -1; // number of bytes in chunk
+ volatile int bytesremaining; // number of bytes in chunk left to be read incl CRLF
+ volatile boolean cr = false; // tryReadChunkLength has found CR
+ volatile int bytesToConsume; // number of bytes that still need to be consumed before proceeding
+ volatile ChunkState state = ChunkState.READING_LENGTH; // current state
+ volatile AbstractSubscription sub;
+ ChunkedBodyParser(Consumer<Throwable> onComplete) {
+ this.onComplete = onComplete;
+ }
+
+ String dbgString() {
+ return dbgTag;
+ }
+
+ @Override
+ public void onSubscribe(AbstractSubscription sub) {
+ debug.log(Level.DEBUG, () -> "onSubscribe: "
+ + pusher.getClass().getName());
+ pusher.onSubscribe(this.sub = sub);
+ }
+
+ @Override
+ public void accept(ByteBuffer b) {
+ if (closedExceptionally != null) {
+ debug.log(Level.DEBUG, () -> "already closed: "
+ + closedExceptionally);
+ return;
+ }
+ boolean completed = false;
+ try {
+ List<ByteBuffer> out = new ArrayList<>();
+ do {
+ if (tryPushOneHunk(b, out)) {
+ // We're done! (true if the final chunk was parsed).
+ if (!out.isEmpty()) {
+ // push what we have and complete
+ // only reduce demand if we actually push something.
+ // we would not have come here if there was no
+ // demand.
+ boolean hasDemand = sub.demand().tryDecrement();
+ assert hasDemand;
+ pusher.onNext(Collections.unmodifiableList(out));
+ }
+ debug.log(Level.DEBUG, () -> "done!");
+ assert closedExceptionally == null;
+ assert state == ChunkState.DONE;
+ onFinished.run();
+ pusher.onComplete();
+ completed = true;
+ onComplete.accept(closedExceptionally); // should be null
+ break;
+ }
+ // the buffer may contain several hunks, and therefore
+ // we must loop while it's not exhausted.
+ } while (b.hasRemaining());
+
+ if (!completed && !out.isEmpty()) {
+ // push what we have.
+ // only reduce demand if we actually push something.
+ // we would not have come here if there was no
+ // demand.
+ boolean hasDemand = sub.demand().tryDecrement();
+ assert hasDemand;
+ pusher.onNext(Collections.unmodifiableList(out));
+ }
+ assert state == ChunkState.DONE || !b.hasRemaining();
+ } catch(Throwable t) {
+ closedExceptionally = t;
+ if (!completed) onComplete.accept(t);
+ }
+ }
+
+ // reads and returns chunklen. Position of chunkbuf is first byte
+ // of chunk on return. chunklen includes the CR LF at end of chunk
+ // returns -1 if needs more bytes
+ private int tryReadChunkLen(ByteBuffer chunkbuf) throws IOException {
+ assert state == ChunkState.READING_LENGTH;
+ while (chunkbuf.hasRemaining()) {
+ int c = chunkbuf.get();
+ if (cr) {
+ if (c == LF) {
+ return partialChunklen;
+ } else {
+ throw new IOException("invalid chunk header");
+ }
+ }
+ if (c == CR) {
+ cr = true;
+ } else {
+ int digit = toDigit(c);
+ partialChunklen = partialChunklen * 16 + digit;
+ }
+ }
+ return -1;
+ }
+
+
+ // try to consume as many bytes as specified by bytesToConsume.
+ // returns the number of bytes that still need to be consumed.
+ // In practice this method is only called to consume one CRLF pair
+ // with bytesToConsume set to 2, so it will only return 0 (if completed),
+ // 1, or 2 (if chunkbuf doesn't have the 2 chars).
+ private int tryConsumeBytes(ByteBuffer chunkbuf) throws IOException {
+ int n = bytesToConsume;
+ if (n > 0) {
+ int e = Math.min(chunkbuf.remaining(), n);
+
+ // verifies some assertions
+ // this methods is called only to consume CRLF
+ if (Utils.ASSERTIONSENABLED) {
+ assert n <= 2 && e <= 2;
+ ByteBuffer tmp = chunkbuf.slice();
+ // if n == 2 assert that we will first consume CR
+ assert (n == 2 && e > 0) ? tmp.get() == CR : true;
+ // if n == 1 || n == 2 && e == 2 assert that we then consume LF
+ assert (n == 1 || e == 2) ? tmp.get() == LF : true;
+ }
+
+ chunkbuf.position(chunkbuf.position() + e);
+ n -= e;
+ bytesToConsume = n;
+ }
+ assert n >= 0;
+ return n;
+ }
+
+ /**
+ * Returns a ByteBuffer containing chunk of data or a "hunk" of data
+ * (a chunk of a chunk if the chunk size is larger than our ByteBuffers).
+ * If the given chunk does not have enough data this method return
+ * an empty ByteBuffer (READMORE).
+ * If we encounter the final chunk (an empty chunk) this method
+ * returns null.
+ */
+ ByteBuffer tryReadOneHunk(ByteBuffer chunk) throws IOException {
+ int unfulfilled = bytesremaining;
+ int toconsume = bytesToConsume;
+ ChunkState st = state;
+ if (st == ChunkState.READING_LENGTH && chunklen == -1) {
+ debug.log(Level.DEBUG, () -> "Trying to read chunk len"
+ + " (remaining in buffer:"+chunk.remaining()+")");
+ int clen = chunklen = tryReadChunkLen(chunk);
+ if (clen == -1) return READMORE;
+ debug.log(Level.DEBUG, "Got chunk len %d", clen);
+ cr = false; partialChunklen = 0;
+ unfulfilled = bytesremaining = clen;
+ if (clen == 0) toconsume = bytesToConsume = 2; // that was the last chunk
+ else st = state = ChunkState.READING_DATA; // read the data
+ }
+
+ if (toconsume > 0) {
+ debug.log(Level.DEBUG,
+ "Trying to consume bytes: %d (remaining in buffer: %s)",
+ toconsume, chunk.remaining());
+ if (tryConsumeBytes(chunk) > 0) {
+ return READMORE;
+ }
+ }
+
+ toconsume = bytesToConsume;
+ assert toconsume == 0;
+
+
+ if (st == ChunkState.READING_LENGTH) {
+ // we will come here only if chunklen was 0, after having
+ // consumed the trailing CRLF
+ int clen = chunklen;
+ assert clen == 0;
+ debug.log(Level.DEBUG, "No more chunks: %d", clen);
+ // the DONE state is not really needed but it helps with
+ // assertions...
+ state = ChunkState.DONE;
+ return null;
+ }
+
+ int clen = chunklen;
+ assert clen > 0;
+ assert st == ChunkState.READING_DATA;
+
+ ByteBuffer returnBuffer = READMORE; // May be a hunk or a chunk
+ if (unfulfilled > 0) {
+ int bytesread = chunk.remaining();
+ debug.log(Level.DEBUG, "Reading chunk: available %d, needed %d",
+ bytesread, unfulfilled);
+
+ int bytes2return = Math.min(bytesread, unfulfilled);
+ debug.log(Level.DEBUG, "Returning chunk bytes: %d", bytes2return);
+ returnBuffer = Utils.sliceWithLimitedCapacity(chunk, bytes2return).asReadOnlyBuffer();
+ unfulfilled = bytesremaining -= bytes2return;
+ if (unfulfilled == 0) bytesToConsume = 2;
+ }
+
+ assert unfulfilled >= 0;
+
+ if (unfulfilled == 0) {
+ debug.log(Level.DEBUG,
+ "No more bytes to read - %d yet to consume.",
+ unfulfilled);
+ // check whether the trailing CRLF is consumed, try to
+ // consume it if not. If tryConsumeBytes needs more bytes
+ // then we will come back here later - skipping the block
+ // that reads data because remaining==0, and finding
+ // that the two bytes are now consumed.
+ if (tryConsumeBytes(chunk) == 0) {
+ // we're done for this chunk! reset all states and
+ // prepare to read the next chunk.
+ chunklen = -1;
+ partialChunklen = 0;
+ cr = false;
+ state = ChunkState.READING_LENGTH;
+ debug.log(Level.DEBUG, "Ready to read next chunk");
+ }
+ }
+ if (returnBuffer == READMORE) {
+ debug.log(Level.DEBUG, "Need more data");
+ }
+ return returnBuffer;
+ }
+
+
+ // Attempt to parse and push one hunk from the buffer.
+ // Returns true if the final chunk was parsed.
+ // Returns false if we need to push more chunks.
+ private boolean tryPushOneHunk(ByteBuffer b, List<ByteBuffer> out)
+ throws IOException {
+ assert state != ChunkState.DONE;
+ ByteBuffer b1 = tryReadOneHunk(b);
+ if (b1 != null) {
+ //assert b1.hasRemaining() || b1 == READMORE;
+ if (b1.hasRemaining()) {
+ debug.log(Level.DEBUG, "Sending chunk to consumer (%d)",
+ b1.remaining());
+ out.add(b1);
+ debug.log(Level.DEBUG, "Chunk sent.");
+ }
+ return false; // we haven't parsed the final chunk yet.
+ } else {
+ return true; // we're done! the final chunk was parsed.
+ }
+ }
+
+ private int toDigit(int b) throws IOException {
+ if (b >= 0x30 && b <= 0x39) {
+ return b - 0x30;
+ }
+ if (b >= 0x41 && b <= 0x46) {
+ return b - 0x41 + 10;
+ }
+ if (b >= 0x61 && b <= 0x66) {
+ return b - 0x61 + 10;
+ }
+ throw new IOException("Invalid chunk header byte " + b);
+ }
+
+ }
+
+ class FixedLengthBodyParser implements BodyParser {
+ final int contentLength;
+ final Consumer<Throwable> onComplete;
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+ final String dbgTag = ResponseContent.this.dbgTag + "/FixedLengthBodyParser";
+ volatile int remaining;
+ volatile Throwable closedExceptionally;
+ volatile AbstractSubscription sub;
+ FixedLengthBodyParser(int contentLength, Consumer<Throwable> onComplete) {
+ this.contentLength = this.remaining = contentLength;
+ this.onComplete = onComplete;
+ }
+
+ String dbgString() {
+ return dbgTag;
+ }
+
+ @Override
+ public void onSubscribe(AbstractSubscription sub) {
+ debug.log(Level.DEBUG, () -> "length="
+ + contentLength +", onSubscribe: "
+ + pusher.getClass().getName());
+ pusher.onSubscribe(this.sub = sub);
+ try {
+ if (contentLength == 0) {
+ onFinished.run();
+ pusher.onComplete();
+ onComplete.accept(null);
+ }
+ } catch (Throwable t) {
+ closedExceptionally = t;
+ try {
+ pusher.onError(t);
+ } finally {
+ onComplete.accept(t);
+ }
+ }
+ }
+
+ @Override
+ public void accept(ByteBuffer b) {
+ if (closedExceptionally != null) {
+ debug.log(Level.DEBUG, () -> "already closed: "
+ + closedExceptionally);
+ return;
+ }
+ boolean completed = false;
+ try {
+ int unfulfilled = remaining;
+ debug.log(Level.DEBUG, "Parser got %d bytes (%d remaining / %d)",
+ b.remaining(), unfulfilled, contentLength);
+ assert unfulfilled != 0 || contentLength == 0 || b.remaining() == 0;
+
+ if (unfulfilled == 0 && contentLength > 0) return;
+
+ if (b.hasRemaining() && unfulfilled > 0) {
+ // only reduce demand if we actually push something.
+ // we would not have come here if there was no
+ // demand.
+ boolean hasDemand = sub.demand().tryDecrement();
+ assert hasDemand;
+ int amount = Math.min(b.remaining(), unfulfilled);
+ unfulfilled = remaining -= amount;
+ ByteBuffer buffer = Utils.sliceWithLimitedCapacity(b, amount);
+ pusher.onNext(List.of(buffer.asReadOnlyBuffer()));
+ }
+ if (unfulfilled == 0) {
+ // We're done! All data has been received.
+ assert closedExceptionally == null;
+ onFinished.run();
+ pusher.onComplete();
+ completed = true;
+ onComplete.accept(closedExceptionally); // should be null
+ } else {
+ assert b.remaining() == 0;
+ }
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG, "Unexpected exception", t);
+ closedExceptionally = t;
+ if (!completed) {
+ onComplete.accept(t);
+ }
+ }
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<Void> {
+ private final Consumer<Optional<byte[]>> consumer;
+ private Flow.Subscription subscription;
+ private final CompletableFuture<Void> result = new MinimalFuture<>();
+ private final AtomicBoolean subscribed = new AtomicBoolean();
+
+ public ConsumerSubscriber(Consumer<Optional<byte[]>> consumer) {
+ this.consumer = Objects.requireNonNull(consumer);
+ }
+
+ @Override
+ public CompletionStage<Void> getBody() {
+ return result;
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ if (!subscribed.compareAndSet(false, true)) {
+ subscription.cancel();
+ } else {
+ this.subscription = subscription;
+ subscription.request(1);
+ }
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> items) {
+ for (ByteBuffer item : items) {
+ byte[] buf = new byte[item.remaining()];
+ item.get(buf);
+ consumer.accept(Optional.of(buf));
+ }
+ subscription.request(1);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ result.completeExceptionally(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ consumer.accept(Optional.empty());
+ result.complete(null);
+ }
+
+ }
+
+ public static class PathSubscriber implements BodySubscriber<Path> {
+
+ private final Path file;
+ private final CompletableFuture<Path> 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<FileChannel> 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<ByteBuffer> items) {
+ try {
+ out.write(items.toArray(Utils.EMPTY_BB_ARRAY));
+ } catch (IOException ex) {
+ Utils.close(out);
+ subscription.cancel();
+ result.completeExceptionally(ex);
+ }
+ subscription.request(1);
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ result.completeExceptionally(e);
+ Utils.close(out);
+ }
+
+ @Override
+ public void onComplete() {
+ Utils.close(out);
+ result.complete(file);
+ }
+
+ @Override
+ public CompletionStage<Path> getBody() {
+ return result;
+ }
+ }
+
+ public static class ByteArraySubscriber<T> implements BodySubscriber<T> {
+ private final Function<byte[], T> finisher;
+ private final CompletableFuture<T> result = new MinimalFuture<>();
+ private final List<ByteBuffer> received = new ArrayList<>();
+
+ private volatile Flow.Subscription subscription;
+
+ public ByteArraySubscriber(Function<byte[],T> finisher) {
+ this.finisher = finisher;
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ if (this.subscription != null) {
+ subscription.cancel();
+ return;
+ }
+ this.subscription = subscription;
+ // We can handle whatever you've got
+ subscription.request(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> items) {
+ // incoming buffers are allocated by http client internally,
+ // and won't be used anywhere except this place.
+ // So it's free simply to store them for further processing.
+ assert Utils.hasRemaining(items);
+ received.addAll(items);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ received.clear();
+ result.completeExceptionally(throwable);
+ }
+
+ static private byte[] join(List<ByteBuffer> bytes) {
+ int size = Utils.remaining(bytes, Integer.MAX_VALUE);
+ byte[] res = new byte[size];
+ int from = 0;
+ for (ByteBuffer b : bytes) {
+ int l = b.remaining();
+ b.get(res, from, l);
+ from += l;
+ }
+ return res;
+ }
+
+ @Override
+ public void onComplete() {
+ try {
+ result.complete(finisher.apply(join(received)));
+ received.clear();
+ } catch (IllegalArgumentException e) {
+ result.completeExceptionally(e);
+ }
+ }
+
+ @Override
+ public CompletionStage<T> getBody() {
+ return result;
+ }
+ }
+
+ /**
+ * An InputStream built on top of the Flow API.
+ */
+ public static class HttpResponseInputStream extends InputStream
+ implements BodySubscriber<InputStream>
+ {
+ final static 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<ByteBuffer> 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<List<ByteBuffer>> buffers;
+ private volatile Flow.Subscription subscription;
+ private volatile boolean closed;
+ private volatile Throwable failed;
+ private volatile Iterator<ByteBuffer> currentListItr;
+ private volatile ByteBuffer currentBuffer;
+ private final AtomicBoolean subscribed = new AtomicBoolean();
+
+ public HttpResponseInputStream() {
+ this(MAX_BUFFERS_IN_QUEUE);
+ }
+
+ HttpResponseInputStream(int maxBuffers) {
+ int capacity = (maxBuffers <= 0 ? MAX_BUFFERS_IN_QUEUE : maxBuffers);
+ // 1 additional slot needed for LAST_LIST added by onComplete
+ this.buffers = new ArrayBlockingQueue<>(capacity + 1);
+ }
+
+ @Override
+ public CompletionStage<InputStream> getBody() {
+ // Returns the stream immediately, before the
+ // response body is received.
+ // This makes it possible for sendAsync().get().body()
+ // to complete before the response body is received.
+ return CompletableFuture.completedStage(this);
+ }
+
+ // Returns the current byte buffer to read from.
+ // If the current buffer has no remaining data, this method will take the
+ // next buffer from the buffers queue, possibly blocking until
+ // a new buffer is made available through the Flow API, or the
+ // end of the flow has been reached.
+ private ByteBuffer current() throws IOException {
+ while (currentBuffer == null || !currentBuffer.hasRemaining()) {
+ // Check whether the stream is closed or exhausted
+ if (closed || failed != null) {
+ throw new IOException("closed", failed);
+ }
+ if (currentBuffer == LAST_BUFFER) break;
+
+ try {
+ if (currentListItr == null || !currentListItr.hasNext()) {
+ // Take a new list of buffers from the queue, blocking
+ // if none is available yet...
+
+ DEBUG_LOGGER.log(Level.DEBUG, "Taking list of Buffers");
+ List<ByteBuffer> 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<ByteBuffer> 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<Stream<String>> createLineStream() {
+ return createLineStream(UTF_8);
+ }
+
+ public static BodySubscriber<Stream<String>> createLineStream(Charset charset) {
+ Objects.requireNonNull(charset);
+ BodySubscriber<InputStream> s = new HttpResponseInputStream();
+ return new MappedSubscriber<InputStream,Stream<String>>(s,
+ (InputStream stream) -> {
+ return new BufferedReader(new InputStreamReader(stream, charset))
+ .lines().onClose(() -> Utils.close(stream));
+ });
+ }
+
+ /**
+ * Currently this consumes all of the data and ignores it
+ */
+ public static class NullSubscriber<T> implements BodySubscriber<T> {
+
+ private final CompletableFuture<T> cf = new MinimalFuture<>();
+ private final Optional<T> result;
+ private final AtomicBoolean subscribed = new AtomicBoolean();
+
+ public NullSubscriber(Optional<T> result) {
+ this.result = result;
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ if (!subscribed.compareAndSet(false, true)) {
+ subscription.cancel();
+ } else {
+ subscription.request(Long.MAX_VALUE);
+ }
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> items) {
+ Objects.requireNonNull(items);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ cf.completeExceptionally(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ if (result.isPresent()) {
+ cf.complete(result.get());
+ } else {
+ cf.complete(null);
+ }
+ }
+
+ @Override
+ public CompletionStage<T> getBody() {
+ return cf;
+ }
+ }
+
+ /** An adapter between {@code BodySubscriber} and {@code Flow.Subscriber}. */
+ public static final class SubscriberAdapter<S extends Subscriber<? super List<ByteBuffer>>,R>
+ implements BodySubscriber<R>
+ {
+ private final CompletableFuture<R> cf = new MinimalFuture<>();
+ private final S subscriber;
+ private final Function<S,R> finisher;
+ private volatile Subscription subscription;
+
+ public SubscriberAdapter(S subscriber, Function<S,R> finisher) {
+ this.subscriber = Objects.requireNonNull(subscriber);
+ this.finisher = Objects.requireNonNull(finisher);
+ }
+
+ @Override
+ public void onSubscribe(Subscription subscription) {
+ Objects.requireNonNull(subscription);
+ if (this.subscription != null) {
+ subscription.cancel();
+ } else {
+ this.subscription = subscription;
+ subscriber.onSubscribe(subscription);
+ }
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ Objects.requireNonNull(item);
+ try {
+ subscriber.onNext(item);
+ } catch (Throwable throwable) {
+ subscription.cancel();
+ onError(throwable);
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ Objects.requireNonNull(throwable);
+ try {
+ subscriber.onError(throwable);
+ } finally {
+ cf.completeExceptionally(throwable);
+ }
+ }
+
+ @Override
+ public void onComplete() {
+ try {
+ subscriber.onComplete();
+ } finally {
+ try {
+ cf.complete(finisher.apply(subscriber));
+ } catch (Throwable throwable) {
+ cf.completeExceptionally(throwable);
+ }
+ }
+ }
+
+ @Override
+ public CompletionStage<R> getBody() {
+ return cf;
+ }
+ }
+
+ /**
+ * A body subscriber which receives input from an upstream subscriber
+ * and maps that subscriber's body type to a new type. The upstream subscriber
+ * delegates all flow operations directly to this object. The
+ * {@link CompletionStage} returned by {@link #getBody()}} takes the output
+ * of the upstream {@code getBody()} and applies the mapper function to
+ * obtain the new {@code CompletionStage} type.
+ *
+ * Uses an Executor that must be set externally.
+ *
+ * @param <T> the upstream body type
+ * @param <U> this subscriber's body type
+ */
+ public static class MappedSubscriber<T,U> implements BodySubscriber<U> {
+ final BodySubscriber<T> upstream;
+ final Function<T,U> mapper;
+
+ /**
+ *
+ * @param upstream
+ * @param mapper
+ */
+ public MappedSubscriber(BodySubscriber<T> upstream, Function<T,U> mapper) {
+ this.upstream = upstream;
+ this.mapper = mapper;
+ }
+
+ @Override
+ public CompletionStage<U> getBody() {
+ return upstream.getBody()
+ .thenApply(mapper);
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ upstream.onSubscribe(subscription);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ upstream.onNext(item);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ upstream.onError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ upstream.onComplete();
+ }
+ }
+}
--- /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.
+ * <p>
+ * This class is only used to implement the {@link
+ * AbstractAsyncSSLConnection.SSLConnectionChannel} which is handed of
+ * to RawChannelImpl when creating a WebSocket.
+ */
+class SSLDelegate {
+
+ final SSLEngine engine;
+ final EngineWrapper wrapper;
+ final Lock handshaking = new ReentrantLock();
+ final SocketChannel chan;
+
+ SSLDelegate(SSLEngine eng, SocketChannel chan)
+ {
+ this.engine = eng;
+ this.chan = chan;
+ this.wrapper = new EngineWrapper(chan, engine);
+ }
+
+ // alpn[] may be null
+// SSLDelegate(SocketChannel chan, HttpClientImpl client, String[] alpn, String sn)
+// throws IOException
+// {
+// serverName = sn;
+// SSLContext context = client.sslContext();
+// engine = context.createSSLEngine();
+// engine.setUseClientMode(true);
+// SSLParameters sslp = client.sslParameters();
+// sslParameters = Utils.copySSLParameters(sslp);
+// if (sn != null) {
+// SNIHostName sni = new SNIHostName(sn);
+// sslParameters.setServerNames(List.of(sni));
+// }
+// if (alpn != null) {
+// sslParameters.setApplicationProtocols(alpn);
+// Log.logSSL("SSLDelegate: Setting application protocols: {0}" + Arrays.toString(alpn));
+// } else {
+// Log.logSSL("SSLDelegate: No application protocols proposed");
+// }
+// engine.setSSLParameters(sslParameters);
+// wrapper = new EngineWrapper(chan, engine);
+// this.chan = chan;
+// this.client = client;
+// }
+
+// SSLParameters getSSLParameters() {
+// return sslParameters;
+// }
+
+ static long countBytes(ByteBuffer[] buffers, int start, int number) {
+ long c = 0;
+ for (int i=0; i<number; i++) {
+ c+= buffers[start+i].remaining();
+ }
+ return c;
+ }
+
+
+ static class WrapperResult {
+ static WrapperResult createOK() {
+ WrapperResult r = new WrapperResult();
+ r.buf = null;
+ r.result = new SSLEngineResult(Status.OK, NOT_HANDSHAKING, 0, 0);
+ return r;
+ }
+ SSLEngineResult result;
+
+ ByteBuffer buf; // buffer containing result data
+ }
+
+ int app_buf_size;
+ int packet_buf_size;
+
+ enum BufType {
+ PACKET,
+ APPLICATION
+ }
+
+ ByteBuffer allocate (BufType type) {
+ return allocate (type, -1);
+ }
+
+ // TODO: Use buffer pool for this
+ ByteBuffer allocate (BufType type, int len) {
+ assert engine != null;
+ synchronized (this) {
+ int size;
+ if (type == BufType.PACKET) {
+ if (packet_buf_size == 0) {
+ SSLSession sess = engine.getSession();
+ packet_buf_size = sess.getPacketBufferSize();
+ }
+ if (len > packet_buf_size) {
+ packet_buf_size = len;
+ }
+ size = packet_buf_size;
+ } else {
+ if (app_buf_size == 0) {
+ SSLSession sess = engine.getSession();
+ app_buf_size = sess.getApplicationBufferSize();
+ }
+ if (len > app_buf_size) {
+ app_buf_size = len;
+ }
+ size = app_buf_size;
+ }
+ return ByteBuffer.allocate (size);
+ }
+ }
+
+ /* reallocates the buffer by :-
+ * 1. creating a new buffer double the size of the old one
+ * 2. putting the contents of the old buffer into the new one
+ * 3. set xx_buf_size to the new size if it was smaller than new size
+ *
+ * flip is set to true if the old buffer needs to be flipped
+ * before it is copied.
+ */
+ private ByteBuffer realloc (ByteBuffer b, boolean flip, BufType type) {
+ // TODO: there should be the linear growth, rather than exponential as
+ // we definitely know the maximum amount of space required to unwrap
+ synchronized (this) {
+ int nsize = 2 * b.capacity();
+ ByteBuffer n = allocate (type, nsize);
+ if (flip) {
+ b.flip();
+ }
+ n.put(b);
+ b = n;
+ }
+ return b;
+ }
+
+ /**
+ * This is a thin wrapper over SSLEngine and the SocketChannel, which
+ * guarantees the ordering of wraps/unwraps with respect to the underlying
+ * channel read/writes. It handles the UNDER/OVERFLOW status codes
+ * It does not handle the handshaking status codes, or the CLOSED status code
+ * though once the engine is closed, any attempt to read/write to it
+ * will get an exception. The overall result is returned.
+ * It functions synchronously/blocking
+ */
+ class EngineWrapper {
+
+ SocketChannel chan;
+ SSLEngine engine;
+ final Object wrapLock;
+ final Object unwrapLock;
+ ByteBuffer unwrap_src, wrap_dst;
+ boolean closed = false;
+ int u_remaining; // the number of bytes left in unwrap_src after an unwrap()
+
+ EngineWrapper (SocketChannel chan, SSLEngine engine) {
+ this.chan = chan;
+ this.engine = engine;
+ wrapLock = new Object();
+ unwrapLock = new Object();
+ unwrap_src = allocate(BufType.PACKET);
+ wrap_dst = allocate(BufType.PACKET);
+ }
+
+// void close () throws IOException {
+// }
+
+ WrapperResult wrapAndSend(ByteBuffer src, boolean ignoreClose)
+ throws IOException
+ {
+ ByteBuffer[] buffers = new ByteBuffer[1];
+ buffers[0] = src;
+ return wrapAndSend(buffers, 0, 1, ignoreClose);
+ }
+
+ /* try to wrap and send the data in src. Handles OVERFLOW.
+ * Might block if there is an outbound blockage or if another
+ * thread is calling wrap(). Also, might not send any data
+ * if an unwrap is needed.
+ */
+ WrapperResult wrapAndSend(ByteBuffer[] src,
+ int offset,
+ int len,
+ boolean ignoreClose)
+ throws IOException
+ {
+ if (closed && !ignoreClose) {
+ throw new IOException ("Engine is closed");
+ }
+ Status status;
+ WrapperResult r = new WrapperResult();
+ synchronized (wrapLock) {
+ wrap_dst.clear();
+ do {
+ r.result = engine.wrap (src, offset, len, wrap_dst);
+ status = r.result.getStatus();
+ if (status == Status.BUFFER_OVERFLOW) {
+ wrap_dst = realloc (wrap_dst, true, BufType.PACKET);
+ }
+ } while (status == Status.BUFFER_OVERFLOW);
+ if (status == Status.CLOSED && !ignoreClose) {
+ closed = true;
+ return r;
+ }
+ if (r.result.bytesProduced() > 0) {
+ wrap_dst.flip();
+ int l = wrap_dst.remaining();
+ assert l == r.result.bytesProduced();
+ while (l>0) {
+ l -= chan.write (wrap_dst);
+ }
+ }
+ }
+ return r;
+ }
+
+ /* block until a complete message is available and return it
+ * in dst, together with the Result. dst may have been re-allocated
+ * so caller should check the returned value in Result
+ * If handshaking is in progress then, possibly no data is returned
+ */
+ WrapperResult recvAndUnwrap(ByteBuffer dst) throws IOException {
+ Status status;
+ WrapperResult r = new WrapperResult();
+ r.buf = dst;
+ if (closed) {
+ throw new IOException ("Engine is closed");
+ }
+ boolean needData;
+ if (u_remaining > 0) {
+ unwrap_src.compact();
+ unwrap_src.flip();
+ needData = false;
+ } else {
+ unwrap_src.clear();
+ needData = true;
+ }
+ synchronized (unwrapLock) {
+ int x;
+ do {
+ if (needData) {
+ x = chan.read (unwrap_src);
+ if (x == -1) {
+ throw new IOException ("connection closed for reading");
+ }
+ unwrap_src.flip();
+ }
+ r.result = engine.unwrap (unwrap_src, r.buf);
+ status = r.result.getStatus();
+ if (status == Status.BUFFER_UNDERFLOW) {
+ if (unwrap_src.limit() == unwrap_src.capacity()) {
+ /* buffer not big enough */
+ unwrap_src = realloc (
+ unwrap_src, false, BufType.PACKET
+ );
+ } else {
+ /* Buffer not full, just need to read more
+ * data off the channel. Reset pointers
+ * for reading off SocketChannel
+ */
+ unwrap_src.position (unwrap_src.limit());
+ unwrap_src.limit (unwrap_src.capacity());
+ }
+ needData = true;
+ } else if (status == Status.BUFFER_OVERFLOW) {
+ r.buf = realloc (r.buf, true, BufType.APPLICATION);
+ needData = false;
+ } else if (status == Status.CLOSED) {
+ closed = true;
+ r.buf.flip();
+ return r;
+ }
+ } while (status != Status.OK);
+ }
+ u_remaining = unwrap_src.remaining();
+ return r;
+ }
+ }
+
+// WrapperResult sendData (ByteBuffer src) throws IOException {
+// ByteBuffer[] buffers = new ByteBuffer[1];
+// buffers[0] = src;
+// return sendData(buffers, 0, 1);
+// }
+
+ /**
+ * send the data in the given ByteBuffer. If a handshake is needed
+ * then this is handled within this method. When this call returns,
+ * all of the given user data has been sent and any handshake has been
+ * completed. Caller should check if engine has been closed.
+ */
+ WrapperResult sendData (ByteBuffer[] src, int offset, int len) throws IOException {
+ WrapperResult r = WrapperResult.createOK();
+ while (countBytes(src, offset, len) > 0) {
+ r = wrapper.wrapAndSend(src, offset, len, false);
+ Status status = r.result.getStatus();
+ if (status == Status.CLOSED) {
+ doClosure ();
+ return r;
+ }
+ HandshakeStatus hs_status = r.result.getHandshakeStatus();
+ if (hs_status != HandshakeStatus.FINISHED &&
+ hs_status != HandshakeStatus.NOT_HANDSHAKING)
+ {
+ doHandshake(hs_status);
+ }
+ }
+ return r;
+ }
+
+ /**
+ * read data thru the engine into the given ByteBuffer. If the
+ * given buffer was not large enough, a new one is allocated
+ * and returned. This call handles handshaking automatically.
+ * Caller should check if engine has been closed.
+ */
+ WrapperResult recvData (ByteBuffer dst) throws IOException {
+ /* we wait until some user data arrives */
+ int mark = dst.position();
+ WrapperResult r = null;
+ int pos = dst.position();
+ while (dst.position() == pos) {
+ r = wrapper.recvAndUnwrap (dst);
+ dst = (r.buf != dst) ? r.buf: dst;
+ Status status = r.result.getStatus();
+ if (status == Status.CLOSED) {
+ doClosure ();
+ return r;
+ }
+
+ HandshakeStatus hs_status = r.result.getHandshakeStatus();
+ if (hs_status != HandshakeStatus.FINISHED &&
+ hs_status != HandshakeStatus.NOT_HANDSHAKING)
+ {
+ doHandshake (hs_status);
+ }
+ }
+ Utils.flipToMark(dst, mark);
+ return r;
+ }
+
+ /* we've received a close notify. Need to call wrap to send
+ * the response
+ */
+ void doClosure () throws IOException {
+ try {
+ handshaking.lock();
+ ByteBuffer tmp = allocate(BufType.APPLICATION);
+ WrapperResult r;
+ do {
+ tmp.clear();
+ tmp.flip ();
+ r = wrapper.wrapAndSend(tmp, true);
+ } while (r.result.getStatus() != Status.CLOSED);
+ } finally {
+ handshaking.unlock();
+ }
+ }
+
+ /* do the (complete) handshake after acquiring the handshake lock.
+ * If two threads call this at the same time, then we depend
+ * on the wrapper methods being idempotent. eg. if wrapAndSend()
+ * is called with no data to send then there must be no problem
+ */
+ @SuppressWarnings("fallthrough")
+ void doHandshake (HandshakeStatus hs_status) throws IOException {
+ boolean wasBlocking;
+ try {
+ wasBlocking = chan.isBlocking();
+ handshaking.lock();
+ chan.configureBlocking(true);
+ ByteBuffer tmp = allocate(BufType.APPLICATION);
+ while (hs_status != HandshakeStatus.FINISHED &&
+ hs_status != HandshakeStatus.NOT_HANDSHAKING)
+ {
+ WrapperResult r = null;
+ switch (hs_status) {
+ case NEED_TASK:
+ Runnable task;
+ while ((task = engine.getDelegatedTask()) != null) {
+ /* run in current thread, because we are already
+ * running an external Executor
+ */
+ task.run();
+ }
+ /* fall thru - call wrap again */
+ case NEED_WRAP:
+ tmp.clear();
+ tmp.flip();
+ r = wrapper.wrapAndSend(tmp, false);
+ break;
+
+ case NEED_UNWRAP:
+ tmp.clear();
+ r = wrapper.recvAndUnwrap (tmp);
+ if (r.buf != tmp) {
+ tmp = r.buf;
+ }
+ assert tmp.position() == 0;
+ break;
+ }
+ hs_status = r.result.getHandshakeStatus();
+ }
+ Log.logSSL(getSessionInfo());
+ if (!wasBlocking) {
+ chan.configureBlocking(false);
+ }
+ } finally {
+ handshaking.unlock();
+ }
+ }
+
+// static void printParams(SSLParameters p) {
+// System.out.println("SSLParameters:");
+// if (p == null) {
+// System.out.println("Null params");
+// return;
+// }
+// for (String cipher : p.getCipherSuites()) {
+// System.out.printf("cipher: %s\n", cipher);
+// }
+// // JDK 8 EXCL START
+// for (String approto : p.getApplicationProtocols()) {
+// System.out.printf("application protocol: %s\n", approto);
+// }
+// // JDK 8 EXCL END
+// for (String protocol : p.getProtocols()) {
+// System.out.printf("protocol: %s\n", protocol);
+// }
+// if (p.getServerNames() != null) {
+// for (SNIServerName sname : p.getServerNames()) {
+// System.out.printf("server name: %s\n", sname.toString());
+// }
+// }
+// }
+
+ String getSessionInfo() {
+ StringBuilder sb = new StringBuilder();
+ String application = engine.getApplicationProtocol();
+ SSLSession sess = engine.getSession();
+ String cipher = sess.getCipherSuite();
+ String protocol = sess.getProtocol();
+ sb.append("Handshake complete alpn: ")
+ .append(application)
+ .append(", Cipher: ")
+ .append(cipher)
+ .append(", Protocol: ")
+ .append(protocol);
+ return sb.toString();
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<ByteBuffer> buffersSource;
+ private final Object lock = new Object();
+ private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+ private final InternalReadPublisher readPublisher;
+ private final InternalWriteSubscriber writeSubscriber;
+ private final long id = IDS.incrementAndGet();
+
+ public SocketTube(HttpClientImpl client, SocketChannel channel,
+ Supplier<ByteBuffer> buffersSource) {
+ this.client = client;
+ this.channel = channel;
+ this.buffersSource = buffersSource;
+ this.readPublisher = new InternalReadPublisher();
+ this.writeSubscriber = new InternalWriteSubscriber();
+ }
+
+// private static Flow.Subscription nopSubscription() {
+// return new Flow.Subscription() {
+// @Override public void request(long n) { }
+// @Override public void cancel() { }
+// };
+// }
+
+ /**
+ * Returns {@code true} if this flow is finished.
+ * This happens when this flow internal read subscription is completed,
+ * either normally (EOF reading) or exceptionally (EOF writing, or
+ * underlying socket closed, or some exception occurred while reading or
+ * writing to the socket).
+ *
+ * @return {@code true} if this flow is finished.
+ */
+ public boolean isFinished() {
+ InternalReadPublisher.InternalReadSubscription subscription =
+ readPublisher.subscriptionImpl;
+ return subscription != null && subscription.completed
+ || subscription == null && errorRef.get() != null;
+ }
+
+ // ===================================================================== //
+ // Flow.Publisher //
+ // ======================================================================//
+
+ /**
+ * {@inheritDoc }
+ * @apiNote This method should be called first. In particular, the caller
+ * must ensure that this method must be called by the read
+ * subscriber before the write publisher can call {@code onSubscribe}.
+ * Failure to adhere to this contract may result in assertion errors.
+ */
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
+ Objects.requireNonNull(s);
+ assert s instanceof TubeSubscriber : "Expected TubeSubscriber, got:" + s;
+ readPublisher.subscribe(s);
+ }
+
+
+ // ===================================================================== //
+ // Flow.Subscriber //
+ // ======================================================================//
+
+ /**
+ * {@inheritDoc }
+ * @apiNote The caller must ensure that {@code subscribe} is called by
+ * the read subscriber before {@code onSubscribe} is called by
+ * the write publisher.
+ * Failure to adhere to this contract may result in assertion errors.
+ */
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ writeSubscriber.onSubscribe(subscription);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ writeSubscriber.onNext(item);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ writeSubscriber.onError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ writeSubscriber.onComplete();
+ }
+
+ // ===================================================================== //
+ // Events //
+ // ======================================================================//
+
+ /**
+ * A restartable task used to process tasks in sequence.
+ */
+ private static class SocketFlowTask implements RestartableTask {
+ final Runnable task;
+ private final Object monitor = new Object();
+ SocketFlowTask(Runnable task) {
+ this.task = task;
+ }
+ @Override
+ public final void run(DeferredCompleter taskCompleter) {
+ try {
+ // non contentious synchronized for visibility.
+ synchronized(monitor) {
+ task.run();
+ }
+ } finally {
+ taskCompleter.complete();
+ }
+ }
+ }
+
+ // This is best effort - there's no guarantee that the printed set
+ // of values is consistent. It should only be considered as
+ // weakly accurate - in particular in what concerns the events states,
+ // especially when displaying a read event state from a write event
+ // callback and conversely.
+ void debugState(String when) {
+ if (debug.isLoggable(Level.DEBUG)) {
+ StringBuilder state = new StringBuilder();
+
+ InternalReadPublisher.InternalReadSubscription sub =
+ readPublisher.subscriptionImpl;
+ InternalReadPublisher.ReadEvent readEvent =
+ sub == null ? null : sub.readEvent;
+ Demand rdemand = sub == null ? null : sub.demand;
+ InternalWriteSubscriber.WriteEvent writeEvent =
+ writeSubscriber.writeEvent;
+ AtomicLong wdemand = writeSubscriber.writeDemand;
+ int rops = readEvent == null ? 0 : readEvent.interestOps();
+ long rd = rdemand == null ? 0 : rdemand.get();
+ int wops = writeEvent == null ? 0 : writeEvent.interestOps();
+ long wd = wdemand == null ? 0 : wdemand.get();
+
+ state.append(when).append(" Reading: [ops=")
+ .append(rops).append(", demand=").append(rd)
+ .append(", stopped=")
+ .append((sub == null ? false : sub.readScheduler.isStopped()))
+ .append("], Writing: [ops=").append(wops)
+ .append(", demand=").append(wd)
+ .append("]");
+ debug.log(Level.DEBUG, state.toString());
+ }
+ }
+
+ /**
+ * A repeatable event that can be paused or resumed by changing
+ * its interestOps.
+ * When the event is fired, it is first paused before being signaled.
+ * It is the responsibility of the code triggered by {@code signalEvent}
+ * to resume the event if required.
+ */
+ private static abstract class SocketFlowEvent extends AsyncEvent {
+ final SocketChannel channel;
+ final int defaultInterest;
+ volatile int interestOps;
+ volatile boolean registered;
+ SocketFlowEvent(int defaultInterest, SocketChannel channel) {
+ super(AsyncEvent.REPEATING);
+ this.defaultInterest = defaultInterest;
+ this.channel = channel;
+ }
+ final boolean registered() {return registered;}
+ final void resume() {
+ interestOps = defaultInterest;
+ registered = true;
+ }
+ final void pause() {interestOps = 0;}
+ @Override
+ public final SelectableChannel channel() {return channel;}
+ @Override
+ public final int interestOps() {return interestOps;}
+
+ @Override
+ public final void handle() {
+ pause(); // pause, then signal
+ signalEvent(); // won't be fired again until resumed.
+ }
+ @Override
+ public final void abort(IOException error) {
+ debug().log(Level.DEBUG, () -> "abort: " + error);
+ pause(); // pause, then signal
+ signalError(error); // should not be resumed after abort (not checked)
+ }
+
+ protected abstract void signalEvent();
+ protected abstract void signalError(Throwable error);
+ abstract System.Logger debug();
+ }
+
+ // ===================================================================== //
+ // Writing //
+ // ======================================================================//
+
+ // This class makes the assumption that the publisher will call
+ // onNext sequentially, and that onNext won't be called if the demand
+ // has not been incremented by request(1).
+ // It has a 'queue of 1' meaning that it will call request(1) in
+ // onSubscribe, and then only after its 'current' buffer list has been
+ // fully written and current set to null;
+ private final class InternalWriteSubscriber
+ implements Flow.Subscriber<List<ByteBuffer>> {
+
+ volatile Flow.Subscription subscription;
+ volatile List<ByteBuffer> current;
+ volatile boolean completed;
+ final WriteEvent writeEvent = new WriteEvent(channel, this);
+ final AtomicLong writeDemand = new AtomicLong();
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ Flow.Subscription previous = this.subscription;
+ this.subscription = subscription;
+ debug.log(Level.DEBUG, "subscribed for writing");
+ if (current == null) {
+ if (previous == subscription || previous == null) {
+ if (writeDemand.compareAndSet(0, 1)) {
+ subscription.request(1);
+ }
+ } else {
+ writeDemand.set(1);
+ subscription.request(1);
+ }
+ }
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> bufs) {
+ assert current == null; // this is a queue of 1.
+ assert subscription != null;
+ current = bufs;
+ tryFlushCurrent(client.isSelectorThread()); // may be in selector thread
+ // For instance in HTTP/2, a received SETTINGS frame might trigger
+ // the sending of a SETTINGS frame in turn which might cause
+ // onNext to be called from within the same selector thread that the
+ // original SETTINGS frames arrived on. If rs is the read-subscriber
+ // and ws is the write-subscriber then the following can occur:
+ // ReadEvent -> rs.onNext(bytes) -> process server SETTINGS -> write
+ // client SETTINGS -> ws.onNext(bytes) -> tryFlushCurrent
+ debugState("leaving w.onNext");
+ }
+
+ // we don't use a SequentialScheduler here: we rely on
+ // onNext() being called sequentially, and not being called
+ // if we haven't call request(1)
+ // onNext is usually called from within a user/executor thread.
+ // we will perform the initial writing in that thread.
+ // if for some reason, not all data can be written, the writeEvent
+ // will be resumed, and the rest of the data will be written from
+ // the selector manager thread when the writeEvent is fired.
+ // If we are in the selector manager thread, then we will use the executor
+ // to call request(1), ensuring that onNext() won't be called from
+ // within the selector thread.
+ // If we are not in the selector manager thread, then we don't care.
+ void tryFlushCurrent(boolean inSelectorThread) {
+ List<ByteBuffer> bufs = current;
+ if (bufs == null) return;
+ try {
+ assert inSelectorThread == client.isSelectorThread() :
+ "should " + (inSelectorThread ? "" : "not ")
+ + " be in the selector thread";
+ long remaining = Utils.remaining(bufs);
+ debug.log(Level.DEBUG, "trying to write: %d", remaining);
+ long written = writeAvailable(bufs);
+ debug.log(Level.DEBUG, "wrote: %d", remaining);
+ if (written == -1) {
+ signalError(new EOFException("EOF reached while writing"));
+ return;
+ }
+ assert written <= remaining;
+ if (remaining - written == 0) {
+ current = null;
+ writeDemand.decrementAndGet();
+ Runnable requestMore = this::requestMore;
+ if (inSelectorThread) {
+ assert client.isSelectorThread();
+ client.theExecutor().execute(requestMore);
+ } else {
+ assert !client.isSelectorThread();
+ requestMore.run();
+ }
+ } else {
+ resumeWriteEvent(inSelectorThread);
+ }
+ } catch (Throwable t) {
+ signalError(t);
+ subscription.cancel();
+ }
+ }
+
+ void requestMore() {
+ try {
+ if (completed) return;
+ long d = writeDemand.get();
+ if (writeDemand.compareAndSet(0,1)) {
+ debug.log(Level.DEBUG, "write: requesting more...");
+ subscription.request(1);
+ } else {
+ debug.log(Level.DEBUG, "write: no need to request more: %d", d);
+ }
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG, () ->
+ "write: error while requesting more: " + t);
+ signalError(t);
+ subscription.cancel();
+ } finally {
+ debugState("leaving requestMore: ");
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ signalError(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ completed = true;
+ // no need to pause the write event here: the write event will
+ // be paused if there is nothing more to write.
+ List<ByteBuffer> bufs = current;
+ long remaining = bufs == null ? 0 : Utils.remaining(bufs);
+ debug.log(Level.DEBUG, "write completed, %d yet to send", remaining);
+ debugState("InternalWriteSubscriber::onComplete");
+ }
+
+ void resumeWriteEvent(boolean inSelectorThread) {
+ debug.log(Level.DEBUG, "scheduling write event");
+ resumeEvent(writeEvent, this::signalError);
+ }
+
+// void pauseWriteEvent() {
+// debug.log(Level.DEBUG, "pausing write event");
+// pauseEvent(writeEvent, this::signalError);
+// }
+
+ void signalWritable() {
+ debug.log(Level.DEBUG, "channel is writable");
+ tryFlushCurrent(true);
+ }
+
+ void signalError(Throwable error) {
+ debug.log(Level.DEBUG, () -> "write error: " + error);
+ completed = true;
+ readPublisher.signalError(error);
+ }
+
+ // A repeatable WriteEvent which is paused after firing and can
+ // be resumed if required - see SocketFlowEvent;
+ final class WriteEvent extends SocketFlowEvent {
+ final InternalWriteSubscriber sub;
+ WriteEvent(SocketChannel channel, InternalWriteSubscriber sub) {
+ super(SelectionKey.OP_WRITE, channel);
+ this.sub = sub;
+ }
+ @Override
+ protected final void signalEvent() {
+ try {
+ client.eventUpdated(this);
+ sub.signalWritable();
+ } catch(Throwable t) {
+ sub.signalError(t);
+ }
+ }
+
+ @Override
+ protected void signalError(Throwable error) {
+ sub.signalError(error);
+ }
+
+ @Override
+ System.Logger debug() {
+ return debug;
+ }
+
+ }
+
+ }
+
+ // ===================================================================== //
+ // Reading //
+ // ===================================================================== //
+
+ // The InternalReadPublisher uses a SequentialScheduler to ensure that
+ // onNext/onError/onComplete are called sequentially on the caller's
+ // subscriber.
+ // However, it relies on the fact that the only time where
+ // runOrSchedule() is called from a user/executor thread is in signalError,
+ // right after the errorRef has been set.
+ // Because the sequential scheduler's task always checks for errors first,
+ // and always terminate the scheduler on error, then it is safe to assume
+ // that if it reaches the point where it reads from the channel, then
+ // it is running in the SelectorManager thread. This is because all
+ // other invocation of runOrSchedule() are triggered from within a
+ // ReadEvent.
+ //
+ // When pausing/resuming the event, some shortcuts can then be taken
+ // when we know we're running in the selector manager thread
+ // (in that case there's no need to call client.eventUpdated(readEvent);
+ //
+ private final class InternalReadPublisher
+ implements Flow.Publisher<List<ByteBuffer>> {
+ private final InternalReadSubscription subscriptionImpl
+ = new InternalReadSubscription();
+ AtomicReference<ReadSubscription> pendingSubscription = new AtomicReference<>();
+ private volatile ReadSubscription subscription;
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
+ Objects.requireNonNull(s);
+
+ TubeSubscriber sub = FlowTube.asTubeSubscriber(s);
+ ReadSubscription target = new ReadSubscription(subscriptionImpl, sub);
+ ReadSubscription previous = pendingSubscription.getAndSet(target);
+
+ if (previous != null && previous != target) {
+ debug.log(Level.DEBUG,
+ () -> "read publisher: dropping pending subscriber: "
+ + previous.subscriber);
+ previous.errorRef.compareAndSet(null, errorRef.get());
+ previous.signalOnSubscribe();
+ if (subscriptionImpl.completed) {
+ previous.signalCompletion();
+ } else {
+ previous.subscriber.dropSubscription();
+ }
+ }
+
+ debug.log(Level.DEBUG, "read publisher got subscriber");
+ subscriptionImpl.signalSubscribe();
+ debugState("leaving read.subscribe: ");
+ }
+
+ void signalError(Throwable error) {
+ if (!errorRef.compareAndSet(null, error)) {
+ return;
+ }
+ subscriptionImpl.handleError();
+ }
+
+ final class ReadSubscription implements Flow.Subscription {
+ final InternalReadSubscription impl;
+ final TubeSubscriber subscriber;
+ final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+ volatile boolean subscribed;
+ volatile boolean cancelled;
+ volatile boolean completed;
+
+ public ReadSubscription(InternalReadSubscription impl,
+ TubeSubscriber subscriber) {
+ this.impl = impl;
+ this.subscriber = subscriber;
+ }
+
+ @Override
+ public void cancel() {
+ cancelled = true;
+ }
+
+ @Override
+ public void request(long n) {
+ if (!cancelled) {
+ impl.request(n);
+ } else {
+ debug.log(Level.DEBUG,
+ "subscription cancelled, ignoring request %d", n);
+ }
+ }
+
+ void signalCompletion() {
+ assert subscribed || cancelled;
+ if (completed || cancelled) return;
+ synchronized (this) {
+ if (completed) return;
+ completed = true;
+ }
+ Throwable error = errorRef.get();
+ if (error != null) {
+ debug.log(Level.DEBUG, () ->
+ "forwarding error to subscriber: "
+ + error);
+ subscriber.onError(error);
+ } else {
+ debug.log(Level.DEBUG, "completing subscriber");
+ subscriber.onComplete();
+ }
+ }
+
+ void signalOnSubscribe() {
+ if (subscribed || cancelled) return;
+ synchronized (this) {
+ if (subscribed || cancelled) return;
+ subscribed = true;
+ }
+ subscriber.onSubscribe(this);
+ debug.log(Level.DEBUG, "onSubscribe called");
+ if (errorRef.get() != null) {
+ signalCompletion();
+ }
+ }
+ }
+
+ final class InternalReadSubscription implements Flow.Subscription {
+
+ private final Demand demand = new Demand();
+ final SequentialScheduler readScheduler;
+ private volatile boolean completed;
+ private final ReadEvent readEvent;
+ private final AsyncEvent subscribeEvent;
+
+ InternalReadSubscription() {
+ readScheduler = new SequentialScheduler(new SocketFlowTask(this::read));
+ subscribeEvent = new AsyncTriggerEvent(this::signalError,
+ this::handleSubscribeEvent);
+ readEvent = new ReadEvent(channel, this);
+ }
+
+ /*
+ * This method must be invoked before any other method of this class.
+ */
+ final void signalSubscribe() {
+ if (readScheduler.isStopped() || completed) {
+ // if already completed or stopped we can handle any
+ // pending connection directly from here.
+ debug.log(Level.DEBUG,
+ "handling pending subscription while completed");
+ handlePending();
+ } else {
+ try {
+ debug.log(Level.DEBUG,
+ "registering subscribe event");
+ client.registerEvent(subscribeEvent);
+ } catch (Throwable t) {
+ signalError(t);
+ handlePending();
+ }
+ }
+ }
+
+ final void handleSubscribeEvent() {
+ assert client.isSelectorThread();
+ debug.log(Level.DEBUG, "subscribe event raised");
+ readScheduler.runOrSchedule();
+ if (readScheduler.isStopped() || completed) {
+ // if already completed or stopped we can handle any
+ // pending connection directly from here.
+ debug.log(Level.DEBUG,
+ "handling pending subscription when completed");
+ handlePending();
+ }
+ }
+
+
+ /*
+ * Although this method is thread-safe, the Reactive-Streams spec seems
+ * to not require it to be as such. It's a responsibility of the
+ * subscriber to signal demand in a thread-safe manner.
+ *
+ * https://github.com/reactive-streams/reactive-streams-jvm/blob/dd24d2ab164d7de6c316f6d15546f957bec29eaa/README.md
+ * (rules 2.7 and 3.4)
+ */
+ @Override
+ public final void request(long n) {
+ if (n > 0L) {
+ boolean wasFulfilled = demand.increase(n);
+ if (wasFulfilled) {
+ debug.log(Level.DEBUG, "got some demand for reading");
+ resumeReadEvent();
+ // if demand has been changed from fulfilled
+ // to unfulfilled register read event;
+ }
+ } else {
+ signalError(new IllegalArgumentException("non-positive request"));
+ }
+ debugState("leaving request("+n+"): ");
+ }
+
+ @Override
+ public final void cancel() {
+ pauseReadEvent();
+ readScheduler.stop();
+ }
+
+ private void resumeReadEvent() {
+ debug.log(Level.DEBUG, "resuming read event");
+ resumeEvent(readEvent, this::signalError);
+ }
+
+ private void pauseReadEvent() {
+ debug.log(Level.DEBUG, "pausing read event");
+ pauseEvent(readEvent, this::signalError);
+ }
+
+
+ final void handleError() {
+ assert errorRef.get() != null;
+ readScheduler.runOrSchedule();
+ }
+
+ final void signalError(Throwable error) {
+ if (!errorRef.compareAndSet(null, error)) {
+ return;
+ }
+ debug.log(Level.DEBUG, () -> "got read error: " + error);
+ readScheduler.runOrSchedule();
+ }
+
+ final void signalReadable() {
+ readScheduler.runOrSchedule();
+ }
+
+ /** The body of the task that runs in SequentialScheduler. */
+ final void read() {
+ // It is important to only call pauseReadEvent() when stopping
+ // the scheduler. The event is automatically paused before
+ // firing, and trying to pause it again could cause a race
+ // condition between this loop, which calls tryDecrementDemand(),
+ // and the thread that calls request(n), which will try to resume
+ // reading.
+ try {
+ while(!readScheduler.isStopped()) {
+ if (completed) return;
+
+ // make sure we have a subscriber
+ if (handlePending()) {
+ debug.log(Level.DEBUG, "pending subscriber subscribed");
+ return;
+ }
+
+ // If an error was signaled, we might not be in the
+ // the selector thread, and that is OK, because we
+ // will just call onError and return.
+ ReadSubscription current = subscription;
+ TubeSubscriber subscriber = current.subscriber;
+ Throwable error = errorRef.get();
+ if (error != null) {
+ completed = true;
+ // safe to pause here because we're finished anyway.
+ pauseReadEvent();
+ debug.log(Level.DEBUG, () -> "Sending error " + error
+ + " to subscriber " + subscriber);
+ current.errorRef.compareAndSet(null, error);
+ current.signalCompletion();
+ readScheduler.stop();
+ debugState("leaving read() loop with error: ");
+ return;
+ }
+
+ // If we reach here then we must be in the selector thread.
+ assert client.isSelectorThread();
+ if (demand.tryDecrement()) {
+ // we have demand.
+ try {
+ List<ByteBuffer> bytes = readAvailable();
+ if (bytes == EOF) {
+ if (!completed) {
+ debug.log(Level.DEBUG, "got read EOF");
+ completed = true;
+ // safe to pause here because we're finished
+ // anyway.
+ pauseReadEvent();
+ current.signalCompletion();
+ readScheduler.stop();
+ }
+ debugState("leaving read() loop after EOF: ");
+ return;
+ } else if (Utils.remaining(bytes) > 0) {
+ // the subscriber is responsible for offloading
+ // to another thread if needed.
+ debug.log(Level.DEBUG, () -> "read bytes: "
+ + Utils.remaining(bytes));
+ assert !current.completed;
+ subscriber.onNext(bytes);
+ // we could continue looping until the demand
+ // reaches 0. However, that would risk starving
+ // other connections (bound to other socket
+ // channels) - as other selected keys activated
+ // by the selector manager thread might be
+ // waiting for this event to terminate.
+ // So resume the read event and return now...
+ resumeReadEvent();
+ debugState("leaving read() loop after onNext: ");
+ return;
+ } else {
+ // nothing available!
+ debug.log(Level.DEBUG, "no more bytes available");
+ // re-increment the demand and resume the read
+ // event. This ensures that this loop is
+ // executed again when the socket becomes
+ // readable again.
+ demand.increase(1);
+ resumeReadEvent();
+ debugState("leaving read() loop with no bytes");
+ return;
+ }
+ } catch (Throwable x) {
+ signalError(x);
+ continue;
+ }
+ } else {
+ debug.log(Level.DEBUG, "no more demand for reading");
+ // the event is paused just after firing, so it should
+ // still be paused here, unless the demand was just
+ // incremented from 0 to n, in which case, the
+ // event will be resumed, causing this loop to be
+ // invoked again when the socket becomes readable:
+ // This is what we want.
+ // Trying to pause the event here would actually
+ // introduce a race condition between this loop and
+ // request(n).
+ debugState("leaving read() loop with no demand");
+ break;
+ }
+ }
+ } catch (Throwable t) {
+ debug.log(Level.DEBUG, "Unexpected exception in read loop", t);
+ signalError(t);
+ } finally {
+ handlePending();
+ }
+ }
+
+ boolean handlePending() {
+ ReadSubscription pending = pendingSubscription.getAndSet(null);
+ if (pending == null) return false;
+ debug.log(Level.DEBUG, "handling pending subscription for %s",
+ pending.subscriber);
+ ReadSubscription current = subscription;
+ if (current != null && current != pending && !completed) {
+ current.subscriber.dropSubscription();
+ }
+ debug.log(Level.DEBUG, "read demand reset to 0");
+ subscriptionImpl.demand.reset(); // subscriber will increase demand if it needs to.
+ pending.errorRef.compareAndSet(null, errorRef.get());
+ if (!readScheduler.isStopped()) {
+ subscription = pending;
+ } else {
+ debug.log(Level.DEBUG, "socket tube is already stopped");
+ }
+ debug.log(Level.DEBUG, "calling onSubscribe");
+ pending.signalOnSubscribe();
+ if (completed) {
+ pending.errorRef.compareAndSet(null, errorRef.get());
+ pending.signalCompletion();
+ }
+ return true;
+ }
+ }
+
+
+ // A repeatable ReadEvent which is paused after firing and can
+ // be resumed if required - see SocketFlowEvent;
+ final class ReadEvent extends SocketFlowEvent {
+ final InternalReadSubscription sub;
+ ReadEvent(SocketChannel channel, InternalReadSubscription sub) {
+ super(SelectionKey.OP_READ, channel);
+ this.sub = sub;
+ }
+ @Override
+ protected final void signalEvent() {
+ try {
+ client.eventUpdated(this);
+ sub.signalReadable();
+ } catch(Throwable t) {
+ sub.signalError(t);
+ }
+ }
+
+ @Override
+ protected final void signalError(Throwable error) {
+ sub.signalError(error);
+ }
+
+ @Override
+ System.Logger debug() {
+ return debug;
+ }
+ }
+
+ }
+
+ // ===================================================================== //
+ // Socket Channel Read/Write //
+ // ===================================================================== //
+ static final int MAX_BUFFERS = 3;
+ static final List<ByteBuffer> EOF = List.of();
+
+ private List<ByteBuffer> readAvailable() throws IOException {
+ ByteBuffer buf = buffersSource.get();
+ assert buf.hasRemaining();
+
+ int read;
+ int pos = buf.position();
+ List<ByteBuffer> list = null;
+ while (buf.hasRemaining()) {
+ while ((read = channel.read(buf)) > 0) {
+ if (!buf.hasRemaining()) break;
+ }
+
+ // nothing read;
+ if (buf.position() == pos) {
+ // An empty list signal the end of data, and should only be
+ // returned if read == -1.
+ // If we already read some data, then we must return what we have
+ // read, and -1 will be returned next time the caller attempts to
+ // read something.
+ if (list == null && read == -1) { // eof
+ list = EOF;
+ break;
+ }
+ }
+ buf.limit(buf.position());
+ buf.position(pos);
+ if (list == null) {
+ list = List.of(buf);
+ } else {
+ if (!(list instanceof ArrayList)) {
+ list = new ArrayList<>(list);
+ }
+ list.add(buf);
+ }
+ if (read <= 0 || list.size() == MAX_BUFFERS) break;
+ buf = buffersSource.get();
+ pos = buf.position();
+ assert buf.hasRemaining();
+ }
+ return list;
+ }
+
+ private long writeAvailable(List<ByteBuffer> bytes) throws IOException {
+ ByteBuffer[] srcs = bytes.toArray(Utils.EMPTY_BB_ARRAY);
+ final long remaining = Utils.remaining(srcs);
+ long written = 0;
+ while (remaining > written) {
+ long w = channel.write(srcs);
+ if (w == -1 && written == 0) return -1;
+ if (w == 0) break;
+ written += w;
+ }
+ return written;
+ }
+
+ private void resumeEvent(SocketFlowEvent event,
+ Consumer<Throwable> errorSignaler) {
+ boolean registrationRequired;
+ synchronized(lock) {
+ registrationRequired = !event.registered();
+ event.resume();
+ }
+ try {
+ if (registrationRequired) {
+ client.registerEvent(event);
+ } else {
+ client.eventUpdated(event);
+ }
+ } catch(Throwable t) {
+ errorSignaler.accept(t);
+ }
+ }
+
+ private void pauseEvent(SocketFlowEvent event,
+ Consumer<Throwable> errorSignaler) {
+ synchronized(lock) {
+ event.pause();
+ }
+ try {
+ client.eventUpdated(event);
+ } catch(Throwable t) {
+ errorSignaler.accept(t);
+ }
+ }
+
+ @Override
+ public void connectFlows(TubePublisher writePublisher,
+ TubeSubscriber readSubscriber) {
+ debug.log(Level.DEBUG, "connecting flows");
+ this.subscribe(readSubscriber);
+ writePublisher.subscribe(this);
+ }
+
+
+ @Override
+ public String toString() {
+ return dbgString();
+ }
+
+ final String dbgString() {
+ return "SocketTube("+id+")";
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<HttpResponse> and the the first one on the list is completed
+ * with the next response
+ *
+ * getResponseAsync() -- queries list of response CFs and returns first one
+ * if one exists. Otherwise, creates one and adds it to list
+ * and returns it. Completion is achieved through the
+ * incoming() upcall from connection reader thread.
+ *
+ * getResponse() -- calls getResponseAsync() and waits for CF to complete
+ *
+ * responseBodyAsync() -- calls responseBody() in an executor thread.
+ *
+ * incoming() -- entry point called from connection reader thread. Frames are
+ * either handled immediately without blocking or for data frames
+ * placed on the stream's inputQ which is consumed by the stream's
+ * reader thread.
+ *
+ * PushedStream sub class
+ * ======================
+ * Sending side methods are not used because the request comes from a PUSH_PROMISE
+ * frame sent by the server. When a PUSH_PROMISE is received the PushedStream
+ * is created. PushedStream does not use responseCF list as there can be only
+ * one response. The CF is created when the object created and when the response
+ * HEADERS frame is received the object is completed.
+ */
+class Stream<T> extends ExchangeImpl<T> {
+
+ final static boolean DEBUG = Utils.DEBUG; // Revisit: temporary developer's flag
+ final System.Logger debug = Utils.getDebugLogger(this::dbgString, DEBUG);
+
+ final ConcurrentLinkedQueue<Http2Frame> inputQ = new ConcurrentLinkedQueue<>();
+ final SequentialScheduler sched =
+ SequentialScheduler.synchronizedScheduler(this::schedule);
+ final SubscriptionBase userSubscription = new SubscriptionBase(sched, this::cancel);
+
+ /**
+ * This stream's identifier. Assigned lazily by the HTTP2Connection before
+ * the stream's first frame is sent.
+ */
+ protected volatile int streamid;
+
+ long requestContentLen;
+
+ final Http2Connection connection;
+ final HttpRequestImpl request;
+ final DecodingCallback rspHeadersConsumer;
+ HttpHeadersImpl responseHeaders;
+ final HttpHeadersImpl requestPseudoHeaders;
+ volatile HttpResponse.BodySubscriber<T> responseSubscriber;
+ final HttpRequest.BodyPublisher requestPublisher;
+ volatile RequestSubscriber requestSubscriber;
+ volatile int responseCode;
+ volatile Response response;
+ volatile Throwable failed; // The exception with which this stream was canceled.
+ final CompletableFuture<Void> requestBodyCF = new MinimalFuture<>();
+ volatile CompletableFuture<T> responseBodyCF;
+
+ /** True if END_STREAM has been seen in a frame received on this stream. */
+ private volatile boolean remotelyClosed;
+ private volatile boolean closed;
+ private volatile boolean endStreamSent;
+
+ // state flags
+ private boolean requestSent, responseReceived;
+
+ /**
+ * A reference to this Stream's connection Send Window controller. The
+ * stream MUST acquire the appropriate amount of Send Window before
+ * sending any data. Will be null for PushStreams, as they cannot send data.
+ */
+ private final WindowController windowController;
+ private final WindowUpdateSender windowUpdater;
+
+ @Override
+ HttpConnection connection() {
+ return connection.connection;
+ }
+
+ /**
+ * Invoked either from incoming() -> {receiveDataFrame() or receiveResetFrame() }
+ * of after user subscription window has re-opened, from SubscriptionBase.request()
+ */
+ private void schedule() {
+ if (responseSubscriber == null)
+ // can't process anything yet
+ return;
+
+ try {
+ while (!inputQ.isEmpty()) {
+ Http2Frame frame = inputQ.peek();
+ if (frame instanceof ResetFrame) {
+ inputQ.remove();
+ handleReset((ResetFrame)frame);
+ return;
+ }
+ DataFrame df = (DataFrame)frame;
+ boolean finished = df.getFlag(DataFrame.END_STREAM);
+
+ List<ByteBuffer> buffers = df.getData();
+ List<ByteBuffer> dsts = Collections.unmodifiableList(buffers);
+ int size = Utils.remaining(dsts, Integer.MAX_VALUE);
+ if (size == 0 && finished) {
+ inputQ.remove();
+ Log.logTrace("responseSubscriber.onComplete");
+ debug.log(Level.DEBUG, "incoming: onComplete");
+ sched.stop();
+ responseSubscriber.onComplete();
+ setEndStreamReceived();
+ return;
+ } else if (userSubscription.tryDecrement()) {
+ inputQ.remove();
+ Log.logTrace("responseSubscriber.onNext {0}", size);
+ debug.log(Level.DEBUG, "incoming: onNext(%d)", size);
+ responseSubscriber.onNext(dsts);
+ if (consumed(df)) {
+ Log.logTrace("responseSubscriber.onComplete");
+ debug.log(Level.DEBUG, "incoming: onComplete");
+ sched.stop();
+ responseSubscriber.onComplete();
+ setEndStreamReceived();
+ return;
+ }
+ } else {
+ return;
+ }
+ }
+ } catch (Throwable throwable) {
+ failed = throwable;
+ }
+
+ Throwable t = failed;
+ if (t != null) {
+ sched.stop();
+ responseSubscriber.onError(t);
+ close();
+ }
+ }
+
+ // Callback invoked after the Response BodySubscriber has consumed the
+ // buffers contained in a DataFrame.
+ // Returns true if END_STREAM is reached, false otherwise.
+ private boolean consumed(DataFrame df) {
+ // RFC 7540 6.1:
+ // The entire DATA frame payload is included in flow control,
+ // including the Pad Length and Padding fields if present
+ int len = df.payloadLength();
+ connection.windowUpdater.update(len);
+
+ if (!df.getFlag(DataFrame.END_STREAM)) {
+ // Don't send window update on a stream which is
+ // closed or half closed.
+ windowUpdater.update(len);
+ return false; // more data coming
+ }
+ return true; // end of stream
+ }
+
+ @Override
+ CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
+ boolean returnConnectionToPool,
+ Executor executor)
+ {
+ Log.logTrace("Reading body on stream {0}", streamid);
+ BodySubscriber<T> bodySubscriber = handler.apply(responseCode, responseHeaders);
+ CompletableFuture<T> cf = receiveData(bodySubscriber, 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<T> receiveData(BodySubscriber<T> 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<ExchangeImpl<T>> sendBodyAsync() {
+ return sendBodyImpl().thenApply( v -> this);
+ }
+
+ @SuppressWarnings("unchecked")
+ Stream(Http2Connection connection,
+ Exchange<T> e,
+ WindowController windowController)
+ {
+ super(e);
+ this.connection = connection;
+ this.windowController = windowController;
+ this.request = e.request();
+ this.requestPublisher = request.requestPublisher; // may be null
+ responseHeaders = new HttpHeadersImpl();
+ rspHeadersConsumer = (name, value) -> {
+ responseHeaders.addHeader(name.toString(), value.toString());
+ if (Log.headers() && Log.trace()) {
+ Log.logTrace("RECEIVED HEADER (streamid={0}): {1}: {2}",
+ streamid, name, value);
+ }
+ };
+ this.requestPseudoHeaders = new HttpHeadersImpl();
+ // NEW
+ this.windowUpdater = new StreamWindowUpdateSender(connection);
+ }
+
+ /**
+ * Entry point from Http2Connection reader thread.
+ *
+ * Data frames will be removed by response body thread.
+ */
+ void incoming(Http2Frame frame) throws IOException {
+ debug.log(Level.DEBUG, "incoming: %s", frame);
+ if ((frame instanceof HeaderFrame)) {
+ HeaderFrame hframe = (HeaderFrame)frame;
+ if (hframe.endHeaders()) {
+ Log.logTrace("handling response (streamid={0})", streamid);
+ handleResponse();
+ if (hframe.getFlag(HeaderFrame.END_STREAM)) {
+ receiveDataFrame(new DataFrame(streamid, DataFrame.END_STREAM, List.of()));
+ }
+ }
+ } else if (frame instanceof DataFrame) {
+ receiveDataFrame((DataFrame)frame);
+ } else {
+ otherFrame(frame);
+ }
+ }
+
+ void otherFrame(Http2Frame frame) throws IOException {
+ switch (frame.type()) {
+ case WindowUpdateFrame.TYPE:
+ incoming_windowUpdate((WindowUpdateFrame) frame);
+ break;
+ case ResetFrame.TYPE:
+ incoming_reset((ResetFrame) frame);
+ break;
+ case PriorityFrame.TYPE:
+ incoming_priority((PriorityFrame) frame);
+ break;
+ default:
+ String msg = "Unexpected frame: " + frame.toString();
+ throw new IOException(msg);
+ }
+ }
+
+ // The Hpack decoder decodes into one of these consumers of name,value pairs
+
+ DecodingCallback rspHeadersConsumer() {
+ return rspHeadersConsumer;
+ }
+
+ protected void handleResponse() throws IOException {
+ responseCode = (int)responseHeaders
+ .firstValueAsLong(":status")
+ .orElseThrow(() -> new IOException("no statuscode in response"));
+
+ response = new Response(
+ request, exchange, responseHeaders,
+ responseCode, HttpClient.Version.HTTP_2);
+
+ /* TODO: review if needs to be removed
+ the value is not used, but in case `content-length` doesn't parse as
+ long, there will be NumberFormatException. If left as is, make sure
+ code up the stack handles NFE correctly. */
+ responseHeaders.firstValueAsLong("content-length");
+
+ if (Log.headers()) {
+ StringBuilder sb = new StringBuilder("RESPONSE HEADERS:\n");
+ Log.dumpHeaders(sb, " ", responseHeaders);
+ Log.logHeaders(sb.toString());
+ }
+
+ completeResponse(response);
+ }
+
+ void incoming_reset(ResetFrame frame) {
+ Log.logTrace("Received RST_STREAM on stream {0}", streamid);
+ if (endStreamReceived()) {
+ Log.logTrace("Ignoring RST_STREAM frame received on remotely closed stream {0}", streamid);
+ } else if (closed) {
+ Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
+ } else {
+ // put it in the input queue in order to read all
+ // pending data frames first. Indeed, a server may send
+ // RST_STREAM after sending END_STREAM, in which case we should
+ // ignore it. However, we won't know if we have received END_STREAM
+ // or not until all pending data frames are read.
+ receiveResetFrame(frame);
+ // RST_STREAM was pushed to the queue. It will be handled by
+ // asyncReceive after all pending data frames have been
+ // processed.
+ Log.logTrace("RST_STREAM pushed in queue for stream {0}", streamid);
+ }
+ }
+
+ void handleReset(ResetFrame frame) {
+ Log.logTrace("Handling RST_STREAM on stream {0}", streamid);
+ if (!closed) {
+ close();
+ int error = frame.getErrorCode();
+ completeResponseExceptionally(new IOException(ErrorFrame.stringForCode(error)));
+ } else {
+ Log.logTrace("Ignoring RST_STREAM frame received on closed stream {0}", streamid);
+ }
+ }
+
+ void incoming_priority(PriorityFrame frame) {
+ // TODO: implement priority
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ private void incoming_windowUpdate(WindowUpdateFrame frame)
+ throws IOException
+ {
+ int amount = frame.getUpdate();
+ if (amount <= 0) {
+ Log.logTrace("Resetting stream: {0} %d, Window Update amount: %d\n",
+ streamid, streamid, amount);
+ connection.resetStream(streamid, ResetFrame.FLOW_CONTROL_ERROR);
+ } else {
+ assert streamid != 0;
+ boolean success = windowController.increaseStreamWindow(amount, streamid);
+ if (!success) { // overflow
+ connection.resetStream(streamid, ResetFrame.FLOW_CONTROL_ERROR);
+ }
+ }
+ }
+
+ void incoming_pushPromise(HttpRequestImpl pushRequest,
+ PushedStream<T> pushStream)
+ throws IOException
+ {
+ if (Log.requests()) {
+ Log.logRequest("PUSH_PROMISE: " + pushRequest.toString());
+ }
+ PushGroup<T> pushGroup = exchange.getPushGroup();
+ if (pushGroup == null) {
+ Log.logTrace("Rejecting push promise stream " + streamid);
+ connection.resetStream(pushStream.streamid, ResetFrame.REFUSED_STREAM);
+ pushStream.close();
+ return;
+ }
+
+ PushGroup.Acceptor<T> acceptor = pushGroup.acceptPushRequest(pushRequest);
+
+ if (!acceptor.accepted()) {
+ // cancel / reject
+ IOException ex = new IOException("Stream " + streamid + " cancelled by users handler");
+ if (Log.trace()) {
+ Log.logTrace("No body subscriber for {0}: {1}", pushRequest,
+ ex.getMessage());
+ }
+ pushStream.cancelImpl(ex);
+ return;
+ }
+
+ CompletableFuture<HttpResponse<T>> pushResponseCF = acceptor.cf();
+ HttpResponse.BodyHandler<T> pushHandler = acceptor.bodyHandler();
+ assert pushHandler != null;
+
+ pushStream.requestSent();
+ pushStream.setPushHandler(pushHandler); // TODO: could wrap the handler to throw on acceptPushPromise ?
+ // setup housekeeping for when the push is received
+ // TODO: deal with ignoring of CF anti-pattern
+ CompletableFuture<HttpResponse<T>> cf = pushStream.responseCF();
+ cf.whenComplete((HttpResponse<T> resp, Throwable t) -> {
+ t = Utils.getCompletionCause(t);
+ if (Log.trace()) {
+ Log.logTrace("Push completed on stream {0} for {1}{2}",
+ pushStream.streamid, resp,
+ ((t==null) ? "": " with exception " + t));
+ }
+ if (t != null) {
+ pushGroup.pushError(t);
+ pushResponseCF.completeExceptionally(t);
+ } else {
+ pushResponseCF.complete(resp);
+ }
+ pushGroup.pushCompleted();
+ });
+
+ }
+
+ private OutgoingHeaders<Stream<T>> headerFrame(long contentLength) {
+ HttpHeadersImpl h = request.getSystemHeaders();
+ if (contentLength > 0) {
+ h.setHeader("content-length", Long.toString(contentLength));
+ }
+ setPseudoHeaderFields();
+ HttpHeaders sysh = filter(h);
+ HttpHeaders userh = filter(request.getUserHeaders());
+ OutgoingHeaders<Stream<T>> f = new OutgoingHeaders<>(sysh, userh, this);
+ if (contentLength == 0) {
+ f.setFlag(HeadersFrame.END_STREAM);
+ endStreamSent = true;
+ }
+ return f;
+ }
+
+ private boolean hasProxyAuthorization(HttpHeaders headers) {
+ return headers.firstValue("proxy-authorization")
+ .isPresent();
+ }
+
+ // Determines whether we need to build a new HttpHeader object.
+ //
+ // Ideally we should pass the filter to OutgoingHeaders refactor the
+ // code that creates the HeaderFrame to honor the filter.
+ // We're not there yet - so depending on the filter we need to
+ // apply and the content of the header we will try to determine
+ // whether anything might need to be filtered.
+ // If nothing needs filtering then we can just use the
+ // original headers.
+ private boolean needsFiltering(HttpHeaders headers,
+ BiPredicate<String, List<String>> filter) {
+ if (filter == Utils.PROXY_TUNNEL_FILTER || filter == Utils.PROXY_FILTER) {
+ // we're either connecting or proxying
+ // slight optimization: we only need to filter out
+ // disabled schemes, so if there are none just
+ // pass through.
+ return Utils.proxyHasDisabledSchemes(filter == Utils.PROXY_TUNNEL_FILTER)
+ && hasProxyAuthorization(headers);
+ } else {
+ // we're talking to a server, either directly or through
+ // a tunnel.
+ // Slight optimization: we only need to filter out
+ // proxy authorization headers, so if there are none just
+ // pass through.
+ return hasProxyAuthorization(headers);
+ }
+ }
+
+ private HttpHeaders filter(HttpHeaders headers) {
+ HttpConnection conn = connection();
+ BiPredicate<String, List<String>> filter =
+ conn.headerFilter(request);
+ if (needsFiltering(headers, filter)) {
+ return ImmutableHeaders.of(headers.map(), filter);
+ }
+ return headers;
+ }
+
+ private void setPseudoHeaderFields() {
+ HttpHeadersImpl hdrs = requestPseudoHeaders;
+ String method = request.method();
+ hdrs.setHeader(":method", method);
+ URI uri = request.uri();
+ hdrs.setHeader(":scheme", uri.getScheme());
+ // TODO: userinfo deprecated. Needs to be removed
+ hdrs.setHeader(":authority", uri.getAuthority());
+ // TODO: ensure header names beginning with : not in user headers
+ String query = uri.getQuery();
+ String path = uri.getPath();
+ if (path == null || path.isEmpty()) {
+ if (method.equalsIgnoreCase("OPTIONS")) {
+ path = "*";
+ } else {
+ path = "/";
+ }
+ }
+ if (query != null) {
+ path += "?" + query;
+ }
+ hdrs.setHeader(":path", path);
+ }
+
+ HttpHeadersImpl getRequestPseudoHeaders() {
+ return requestPseudoHeaders;
+ }
+
+ /** Sets endStreamReceived. Should be called only once. */
+ void setEndStreamReceived() {
+ assert remotelyClosed == false: "Unexpected endStream already set";
+ remotelyClosed = true;
+ responseReceived();
+ }
+
+ /** Tells whether, or not, the END_STREAM Flag has been seen in any frame
+ * received on this stream. */
+ private boolean endStreamReceived() {
+ return remotelyClosed;
+ }
+
+ @Override
+ CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
+ debug.log(Level.DEBUG, "sendHeadersOnly()");
+ if (Log.requests() && request != null) {
+ Log.logRequest(request.toString());
+ }
+ if (requestPublisher != null) {
+ requestContentLen = requestPublisher.contentLength();
+ } else {
+ requestContentLen = 0;
+ }
+ OutgoingHeaders<Stream<T>> f = headerFrame(requestContentLen);
+ connection.sendFrame(f);
+ CompletableFuture<ExchangeImpl<T>> cf = new MinimalFuture<>();
+ cf.complete(this); // #### good enough for now
+ return cf;
+ }
+
+ @Override
+ void released() {
+ if (streamid > 0) {
+ debug.log(Level.DEBUG, "Released stream %d", streamid);
+ // remove this stream from the Http2Connection map.
+ connection.closeStream(streamid);
+ } else {
+ debug.log(Level.DEBUG, "Can't release stream %d", streamid);
+ }
+ }
+
+ @Override
+ void completed() {
+ // There should be nothing to do here: the stream should have
+ // been already closed (or will be closed shortly after).
+ }
+
+ void registerStream(int id) {
+ this.streamid = id;
+ connection.putStream(this, streamid);
+ debug.log(Level.DEBUG, "Registered stream %d", id);
+ }
+
+ void signalWindowUpdate() {
+ RequestSubscriber subscriber = requestSubscriber;
+ assert subscriber != null;
+ debug.log(Level.DEBUG, "Signalling window update");
+ subscriber.sendScheduler.runOrSchedule();
+ }
+
+ static final ByteBuffer COMPLETED = ByteBuffer.allocate(0);
+ class RequestSubscriber implements Flow.Subscriber<ByteBuffer> {
+ // can be < 0 if the actual length is not known.
+ private final long contentLength;
+ private volatile long remainingContentLength;
+ private volatile Subscription subscription;
+
+ // Holds the outgoing data. There will be at most 2 outgoing ByteBuffers.
+ // 1) The data that was published by the request body Publisher, and
+ // 2) the COMPLETED sentinel, since onComplete can be invoked without demand.
+ final ConcurrentLinkedDeque<ByteBuffer> outgoing = new ConcurrentLinkedDeque<>();
+
+ private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+ // A scheduler used to honor window updates. Writing must be paused
+ // when the window is exhausted, and resumed when the window acquires
+ // some space. The sendScheduler makes it possible to implement this
+ // behaviour in an asynchronous non-blocking way.
+ // See RequestSubscriber::trySend below.
+ final SequentialScheduler sendScheduler;
+
+ RequestSubscriber(long contentLen) {
+ this.contentLength = contentLen;
+ this.remainingContentLength = contentLen;
+ this.sendScheduler =
+ SequentialScheduler.synchronizedScheduler(this::trySend);
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ if (this.subscription != null) {
+ throw new IllegalStateException("already subscribed");
+ }
+ this.subscription = subscription;
+ debug.log(Level.DEBUG, "RequestSubscriber: onSubscribe, request 1");
+ subscription.request(1);
+ }
+
+ @Override
+ public void onNext(ByteBuffer item) {
+ debug.log(Level.DEBUG, "RequestSubscriber: onNext(%d)", item.remaining());
+ int size = outgoing.size();
+ assert size == 0 : "non-zero size: " + size;
+ onNextImpl(item);
+ }
+
+ private void onNextImpl(ByteBuffer item) {
+ // Got some more request body bytes to send.
+ if (requestBodyCF.isDone()) {
+ // stream already cancelled, probably in timeout
+ sendScheduler.stop();
+ subscription.cancel();
+ return;
+ }
+ outgoing.add(item);
+ sendScheduler.runOrSchedule();
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ debug.log(Level.DEBUG, () -> "RequestSubscriber: onError: " + throwable);
+ // ensure that errors are handled within the flow.
+ if (errorRef.compareAndSet(null, throwable)) {
+ sendScheduler.runOrSchedule();
+ }
+ }
+
+ @Override
+ public void onComplete() {
+ debug.log(Level.DEBUG, "RequestSubscriber: onComplete");
+ int size = outgoing.size();
+ assert size == 0 || size == 1 : "non-zero or one size: " + size;
+ // last byte of request body has been obtained.
+ // ensure that everything is completed within the flow.
+ onNextImpl(COMPLETED);
+ }
+
+ // Attempts to send the data, if any.
+ // Handles errors and completion state.
+ // Pause writing if the send window is exhausted, resume it if the
+ // send window has some bytes that can be acquired.
+ void trySend() {
+ try {
+ // handle errors raised by onError;
+ Throwable t = errorRef.get();
+ if (t != null) {
+ sendScheduler.stop();
+ if (requestBodyCF.isDone()) return;
+ subscription.cancel();
+ requestBodyCF.completeExceptionally(t);
+ return;
+ }
+
+ do {
+ // handle COMPLETED;
+ ByteBuffer item = outgoing.peekFirst();
+ if (item == null) return;
+ else if (item == COMPLETED) {
+ sendScheduler.stop();
+ complete();
+ return;
+ }
+
+ // handle bytes to send downstream
+ while (item.hasRemaining()) {
+ debug.log(Level.DEBUG, "trySend: %d", item.remaining());
+ assert !endStreamSent : "internal error, send data after END_STREAM flag";
+ DataFrame df = getDataFrame(item);
+ if (df == null) {
+ debug.log(Level.DEBUG, "trySend: can't send yet: %d",
+ item.remaining());
+ return; // the send window is exhausted: come back later
+ }
+
+ if (contentLength > 0) {
+ remainingContentLength -= df.getDataLength();
+ if (remainingContentLength < 0) {
+ String msg = connection().getConnectionFlow()
+ + " stream=" + streamid + " "
+ + "[" + Thread.currentThread().getName() + "] "
+ + "Too many bytes in request body. Expected: "
+ + contentLength + ", got: "
+ + (contentLength - remainingContentLength);
+ connection.resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
+ throw new IOException(msg);
+ } else if (remainingContentLength == 0) {
+ df.setFlag(DataFrame.END_STREAM);
+ endStreamSent = true;
+ }
+ }
+ debug.log(Level.DEBUG, "trySend: sending: %d", df.getDataLength());
+ connection.sendDataFrame(df);
+ }
+ assert !item.hasRemaining();
+ ByteBuffer b = outgoing.removeFirst();
+ assert b == item;
+ } while (outgoing.peekFirst() != null);
+
+ debug.log(Level.DEBUG, "trySend: request 1");
+ subscription.request(1);
+ } catch (Throwable ex) {
+ debug.log(Level.DEBUG, "trySend: ", ex);
+ sendScheduler.stop();
+ subscription.cancel();
+ requestBodyCF.completeExceptionally(ex);
+ }
+ }
+
+ private void complete() throws IOException {
+ long remaining = remainingContentLength;
+ long written = contentLength - remaining;
+ if (remaining > 0) {
+ connection.resetStream(streamid, ResetFrame.PROTOCOL_ERROR);
+ // let trySend() handle the exception
+ throw new IOException(connection().getConnectionFlow()
+ + " stream=" + streamid + " "
+ + "[" + Thread.currentThread().getName() +"] "
+ + "Too few bytes returned by the publisher ("
+ + written + "/"
+ + contentLength + ")");
+ }
+ if (!endStreamSent) {
+ endStreamSent = true;
+ connection.sendDataFrame(getEmptyEndStreamDataFrame());
+ }
+ requestBodyCF.complete(null);
+ }
+ }
+
+ /**
+ * Send a RESET frame to tell server to stop sending data on this stream
+ */
+ @Override
+ public CompletableFuture<Void> ignoreBody() {
+ try {
+ connection.resetStream(streamid, ResetFrame.STREAM_CLOSED);
+ return MinimalFuture.completedFuture(null);
+ } catch (Throwable e) {
+ Log.logTrace("Error resetting stream {0}", e.toString());
+ return MinimalFuture.failedFuture(e);
+ }
+ }
+
+ DataFrame getDataFrame(ByteBuffer buffer) {
+ int requestAmount = Math.min(connection.getMaxSendFrameSize(), buffer.remaining());
+ // blocks waiting for stream send window, if exhausted
+ int actualAmount = windowController.tryAcquire(requestAmount, streamid, this);
+ if (actualAmount <= 0) return null;
+ ByteBuffer outBuf = Utils.sliceWithLimitedCapacity(buffer, actualAmount);
+ DataFrame df = new DataFrame(streamid, 0 , outBuf);
+ return df;
+ }
+
+ private DataFrame getEmptyEndStreamDataFrame() {
+ return new DataFrame(streamid, DataFrame.END_STREAM, List.of());
+ }
+
+ /**
+ * A List of responses relating to this stream. Normally there is only
+ * one response, but intermediate responses like 100 are allowed
+ * and must be passed up to higher level before continuing. Deals with races
+ * such as if responses are returned before the CFs get created by
+ * getResponseAsync()
+ */
+
+ final List<CompletableFuture<Response>> response_cfs = new ArrayList<>(5);
+
+ @Override
+ CompletableFuture<Response> getResponseAsync(Executor executor) {
+ CompletableFuture<Response> cf;
+ // The code below deals with race condition that can be caused when
+ // completeResponse() is being called before getResponseAsync()
+ synchronized (response_cfs) {
+ if (!response_cfs.isEmpty()) {
+ // This CompletableFuture was created by completeResponse().
+ // it will be already completed.
+ cf = response_cfs.remove(0);
+ // if we find a cf here it should be already completed.
+ // finding a non completed cf should not happen. just assert it.
+ assert cf.isDone() : "Removing uncompleted response: could cause code to hang!";
+ } else {
+ // getResponseAsync() is called first. Create a CompletableFuture
+ // that will be completed by completeResponse() when
+ // completeResponse() is called.
+ cf = new MinimalFuture<>();
+ response_cfs.add(cf);
+ }
+ }
+ if (executor != null && !cf.isDone()) {
+ // protect from executing later chain of CompletableFuture operations from SelectorManager thread
+ cf = cf.thenApplyAsync(r -> r, executor);
+ }
+ Log.logTrace("Response future (stream={0}) is: {1}", streamid, cf);
+ PushGroup<?> pg = exchange.getPushGroup();
+ if (pg != null) {
+ // if an error occurs make sure it is recorded in the PushGroup
+ cf = cf.whenComplete((t,e) -> pg.pushError(Utils.getCompletionCause(e)));
+ }
+ return cf;
+ }
+
+ /**
+ * Completes the first uncompleted CF on list, and removes it. If there is no
+ * uncompleted CF then creates one (completes it) and adds to list
+ */
+ void completeResponse(Response resp) {
+ synchronized (response_cfs) {
+ CompletableFuture<Response> cf;
+ int cfs_len = response_cfs.size();
+ for (int i=0; i<cfs_len; i++) {
+ cf = response_cfs.get(i);
+ if (!cf.isDone()) {
+ Log.logTrace("Completing response (streamid={0}): {1}",
+ streamid, cf);
+ cf.complete(resp);
+ response_cfs.remove(cf);
+ return;
+ } // else we found the previous response: just leave it alone.
+ }
+ cf = MinimalFuture.completedFuture(resp);
+ Log.logTrace("Created completed future (streamid={0}): {1}",
+ streamid, cf);
+ response_cfs.add(cf);
+ }
+ }
+
+ // methods to update state and remove stream when finished
+
+ synchronized void requestSent() {
+ requestSent = true;
+ if (responseReceived) {
+ close();
+ }
+ }
+
+ synchronized void responseReceived() {
+ responseReceived = true;
+ if (requestSent) {
+ close();
+ }
+ }
+
+ /**
+ * same as above but for errors
+ */
+ void completeResponseExceptionally(Throwable t) {
+ synchronized (response_cfs) {
+ // use index to avoid ConcurrentModificationException
+ // caused by removing the CF from within the loop.
+ for (int i = 0; i < response_cfs.size(); i++) {
+ CompletableFuture<Response> cf = response_cfs.get(i);
+ if (!cf.isDone()) {
+ cf.completeExceptionally(t);
+ response_cfs.remove(i);
+ return;
+ }
+ }
+ response_cfs.add(MinimalFuture.failedFuture(t));
+ }
+ }
+
+ CompletableFuture<Void> sendBodyImpl() {
+ requestBodyCF.whenComplete((v, t) -> requestSent());
+ if (requestPublisher != null) {
+ final RequestSubscriber subscriber = new RequestSubscriber(requestContentLen);
+ requestPublisher.subscribe(requestSubscriber = subscriber);
+ } else {
+ // there is no request body, therefore the request is complete,
+ // END_STREAM has already sent with outgoing headers
+ requestBodyCF.complete(null);
+ }
+ return requestBodyCF;
+ }
+
+ @Override
+ void cancel() {
+ cancel(new IOException("Stream " + streamid + " cancelled"));
+ }
+
+ @Override
+ void cancel(IOException cause) {
+ cancelImpl(cause);
+ }
+
+ // This method sends a RST_STREAM frame
+ void cancelImpl(Throwable e) {
+ debug.log(Level.DEBUG, "cancelling stream {0}: {1}", streamid, e);
+ if (Log.trace()) {
+ Log.logTrace("cancelling stream {0}: {1}\n", streamid, e);
+ }
+ boolean closing;
+ if (closing = !closed) { // assigning closing to !closed
+ synchronized (this) {
+ failed = e;
+ if (closing = !closed) { // assigning closing to !closed
+ closed=true;
+ }
+ }
+ }
+ if (closing) { // true if the stream has not been closed yet
+ if (responseSubscriber != null)
+ sched.runOrSchedule();
+ }
+ completeResponseExceptionally(e);
+ if (!requestBodyCF.isDone()) {
+ requestBodyCF.completeExceptionally(e); // we may be sending the body..
+ }
+ if (responseBodyCF != null) {
+ responseBodyCF.completeExceptionally(e);
+ }
+ try {
+ // will send a RST_STREAM frame
+ if (streamid != 0) {
+ connection.resetStream(streamid, ResetFrame.CANCEL);
+ }
+ } catch (IOException ex) {
+ Log.logError(ex);
+ }
+ }
+
+ // This method doesn't send any frame
+ void close() {
+ if (closed) return;
+ synchronized(this) {
+ if (closed) return;
+ closed = true;
+ }
+ Log.logTrace("Closing stream {0}", streamid);
+ connection.closeStream(streamid);
+ Log.logTrace("Stream {0} closed", streamid);
+ }
+
+ static class PushedStream<T> extends Stream<T> {
+ final PushGroup<T> pushGroup;
+ // push streams need the response CF allocated up front as it is
+ // given directly to user via the multi handler callback function.
+ final CompletableFuture<Response> pushCF;
+ CompletableFuture<HttpResponse<T>> responseCF;
+ final HttpRequestImpl pushReq;
+ HttpResponse.BodyHandler<T> pushHandler;
+
+ PushedStream(PushGroup<T> pushGroup,
+ Http2Connection connection,
+ Exchange<T> pushReq) {
+ // ## no request body possible, null window controller
+ super(connection, pushReq, null);
+ this.pushGroup = pushGroup;
+ this.pushReq = pushReq.request();
+ this.pushCF = new MinimalFuture<>();
+ this.responseCF = new MinimalFuture<>();
+
+ }
+
+ CompletableFuture<HttpResponse<T>> responseCF() {
+ return responseCF;
+ }
+
+ synchronized void setPushHandler(HttpResponse.BodyHandler<T> pushHandler) {
+ this.pushHandler = pushHandler;
+ }
+
+ synchronized HttpResponse.BodyHandler<T> getPushHandler() {
+ // ignored parameters to function can be used as BodyHandler
+ return this.pushHandler;
+ }
+
+ // Following methods call the super class but in case of
+ // error record it in the PushGroup. The error method is called
+ // with a null value when no error occurred (is a no-op)
+ @Override
+ CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
+ return super.sendBodyAsync()
+ .whenComplete((ExchangeImpl<T> v, Throwable t)
+ -> pushGroup.pushError(Utils.getCompletionCause(t)));
+ }
+
+ @Override
+ CompletableFuture<ExchangeImpl<T>> sendHeadersAsync() {
+ return super.sendHeadersAsync()
+ .whenComplete((ExchangeImpl<T> ex, Throwable t)
+ -> pushGroup.pushError(Utils.getCompletionCause(t)));
+ }
+
+ @Override
+ CompletableFuture<Response> getResponseAsync(Executor executor) {
+ CompletableFuture<Response> cf = pushCF.whenComplete(
+ (v, t) -> pushGroup.pushError(Utils.getCompletionCause(t)));
+ if(executor!=null && !cf.isDone()) {
+ cf = cf.thenApplyAsync( r -> r, executor);
+ }
+ return cf;
+ }
+
+ @Override
+ CompletableFuture<T> readBodyAsync(
+ HttpResponse.BodyHandler<T> handler,
+ boolean returnConnectionToPool,
+ Executor executor)
+ {
+ return super.readBodyAsync(handler, returnConnectionToPool, executor)
+ .whenComplete((v, t) -> pushGroup.pushError(t));
+ }
+
+ @Override
+ void completeResponse(Response r) {
+ Log.logResponse(r::toString);
+ pushCF.complete(r); // not strictly required for push API
+ // start reading the body using the obtained BodySubscriber
+ CompletableFuture<Void> start = new MinimalFuture<>();
+ start.thenCompose( v -> readBodyAsync(getPushHandler(), false, getExchange().executor()))
+ .whenComplete((T body, Throwable t) -> {
+ if (t != null) {
+ responseCF.completeExceptionally(t);
+ } else {
+ HttpResponseImpl<T> resp =
+ new HttpResponseImpl<>(r.request, r, null, body, getExchange());
+ responseCF.complete(resp);
+ }
+ });
+ start.completeAsync(() -> null, getExchange().executor());
+ }
+
+ @Override
+ void completeResponseExceptionally(Throwable t) {
+ pushCF.completeExceptionally(t);
+ }
+
+// @Override
+// synchronized void responseReceived() {
+// super.responseReceived();
+// }
+
+ // create and return the PushResponseImpl
+ @Override
+ protected void handleResponse() {
+ responseCode = (int)responseHeaders
+ .firstValueAsLong(":status")
+ .orElse(-1);
+
+ if (responseCode == -1) {
+ completeResponseExceptionally(new IOException("No status code"));
+ }
+
+ this.response = new Response(
+ pushReq, exchange, responseHeaders,
+ responseCode, HttpClient.Version.HTTP_2);
+
+ /* TODO: review if needs to be removed
+ the value is not used, but in case `content-length` doesn't parse
+ as long, there will be NumberFormatException. If left as is, make
+ sure code up the stack handles NFE correctly. */
+ responseHeaders.firstValueAsLong("content-length");
+
+ if (Log.headers()) {
+ StringBuilder sb = new StringBuilder("RESPONSE HEADERS");
+ sb.append(" (streamid=").append(streamid).append("): ");
+ Log.dumpHeaders(sb, " ", responseHeaders);
+ Log.logHeaders(sb.toString());
+ }
+
+ // different implementations for normal streams and pushed streams
+ completeResponse(response);
+ }
+ }
+
+ final class StreamWindowUpdateSender extends WindowUpdateSender {
+
+ StreamWindowUpdateSender(Http2Connection connection) {
+ super(connection);
+ }
+
+ @Override
+ int getStreamId() {
+ return streamid;
+ }
+ }
+
+ /**
+ * Returns true if this exchange was canceled.
+ * @return true if this exchange was canceled.
+ */
+ synchronized boolean isCanceled() {
+ return failed != null;
+ }
+
+ /**
+ * Returns the cause for which this exchange was canceled, if available.
+ * @return the cause for which this exchange was canceled, if available.
+ */
+ synchronized Throwable getCancelCause() {
+ return failed;
+ }
+
+ final String dbgString() {
+ return connection.dbgString() + "/Stream("+streamid+")";
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<TimeoutEvent> {
+
+ private static final AtomicLong COUNTER = new AtomicLong();
+ // we use id in compareTo to make compareTo consistent with equals
+ // see TimeoutEvent::compareTo below;
+ private final long id = COUNTER.incrementAndGet();
+ private final Instant deadline;
+
+ TimeoutEvent(Duration duration) {
+ deadline = Instant.now().plus(duration);
+ }
+
+ public abstract void handle();
+
+ public Instant deadline() {
+ return deadline;
+ }
+
+ @Override
+ public int compareTo(TimeoutEvent other) {
+ if (other == this) return 0;
+ // if two events have the same deadline, but are not equals, then the
+ // smaller is the one that was created before (has the smaller id).
+ // This is arbitrary and we don't really care which is smaller or
+ // greater, but we need a total order, so two events with the
+ // same deadline cannot compare == 0 if they are not equals.
+ final int compareDeadline = this.deadline.compareTo(other.deadline);
+ if (compareDeadline == 0 && !this.equals(other)) {
+ long diff = this.id - other.id; // should take care of wrap around
+ if (diff < 0) return -1;
+ else if (diff > 0) return 1;
+ else assert false : "Different events with same id and deadline";
+ }
+ return compareDeadline;
+ }
+
+ @Override
+ public String toString() {
+ return "TimeoutEvent[id=" + id + ", deadline=" + deadline + "]";
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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<T> extends HttpResponse.BodyHandler<T> {
+ void setAccessControlContext(AccessControlContext acc);
+}
--- /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<Integer,Integer> streams = new HashMap<>();
+ /** A Map of streams awaiting Send Window. The key is the stream id. The
+ * value is a pair of the Stream ( representing the key's stream id ) and
+ * the requested amount of send Window. */
+ private final Map<Integer, Map.Entry<Stream<?>, Integer>> pending
+ = new LinkedHashMap<>();
+
+ private final ReentrantLock controllerLock = new ReentrantLock();
+
+ /** A Controller with the default initial window size. */
+ WindowController() {
+ connectionWindowSize = DEFAULT_INITIAL_WINDOW_SIZE;
+ }
+
+// /** A Controller with the given initial window size. */
+// WindowController(int initialConnectionWindowSize) {
+// connectionWindowSize = initialConnectionWindowSize;
+// }
+
+ /** Registers the given stream with this controller. */
+ void registerStream(int streamid, int initialStreamWindowSize) {
+ controllerLock.lock();
+ try {
+ Integer old = streams.put(streamid, initialStreamWindowSize);
+ if (old != null)
+ throw new InternalError("Unexpected entry ["
+ + old + "] for streamid: " + streamid);
+ } finally {
+ controllerLock.unlock();
+ }
+ }
+
+ /** Removes/De-registers the given stream with this controller. */
+ void removeStream(int streamid) {
+ controllerLock.lock();
+ try {
+ Integer old = streams.remove(streamid);
+ // Odd stream numbers (client streams) should have been registered.
+ // Even stream numbers (server streams - aka Push Streams) should
+ // not be registered
+ final boolean isClientStream = (streamid % 2) == 1;
+ if (old == null && isClientStream) {
+ throw new InternalError("Expected entry for streamid: " + streamid);
+ } else if (old != null && !isClientStream) {
+ throw new InternalError("Unexpected entry for streamid: " + streamid);
+ }
+ } finally {
+ controllerLock.unlock();
+ }
+ }
+
+ /**
+ * Attempts to acquire the requested amount of Send Window for the given
+ * stream.
+ *
+ * The actual amount of Send Window available may differ from the requested
+ * amount. The actual amount, returned by this method, is the minimum of,
+ * 1) the requested amount, 2) the stream's Send Window, and 3) the
+ * connection's Send Window.
+ *
+ * A negative or zero value is returned if there's no window available.
+ * When the result is negative or zero, this method arranges for the
+ * given stream's {@link Stream#signalWindowUpdate()} method to be invoke at
+ * a later time when the connection and/or stream window's have been
+ * increased. The {@code tryAcquire} method should then be invoked again to
+ * attempt to acquire the available window.
+ */
+ int tryAcquire(int requestAmount, int streamid, Stream<?> stream) {
+ controllerLock.lock();
+ try {
+ Integer streamSize = streams.get(streamid);
+ if (streamSize == null)
+ throw new InternalError("Expected entry for streamid: "
+ + streamid);
+ int x = Math.min(requestAmount,
+ Math.min(streamSize, connectionWindowSize));
+
+ if (x <= 0) { // stream window size may be negative
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Stream %d requesting %d but only %d available (stream: %d, connection: %d)",
+ streamid, requestAmount, Math.min(streamSize, connectionWindowSize),
+ streamSize, connectionWindowSize);
+ // If there's not enough window size available, put the
+ // caller in a pending list.
+ pending.put(streamid, Map.entry(stream, requestAmount));
+ return x;
+ }
+
+ // Remove the caller from the pending list ( if was waiting ).
+ pending.remove(streamid);
+
+ // Update window sizes and return the allocated amount to the caller.
+ streamSize -= x;
+ streams.put(streamid, streamSize);
+ connectionWindowSize -= x;
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Stream %d amount allocated %d, now %d available (stream: %d, connection: %d)",
+ streamid, x, Math.min(streamSize, connectionWindowSize),
+ streamSize, connectionWindowSize);
+ return x;
+ } finally {
+ controllerLock.unlock();
+ }
+ }
+
+ /**
+ * Increases the Send Window size for the connection.
+ *
+ * A number of awaiting requesters, from unfulfilled tryAcquire requests,
+ * may have their stream's {@link Stream#signalWindowUpdate()} method
+ * scheduled to run ( i.e. awake awaiters ).
+ *
+ * @return false if, and only if, the addition of the given amount would
+ * cause the Send Window to exceed 2^31-1 (overflow), otherwise true
+ */
+ boolean increaseConnectionWindow(int amount) {
+ List<Stream<?>> candidates = null;
+ controllerLock.lock();
+ try {
+ int size = connectionWindowSize;
+ size += amount;
+ if (size < 0)
+ return false;
+ connectionWindowSize = size;
+ DEBUG_LOGGER.log(Level.DEBUG, "Connection window size is now %d", size);
+
+ // Notify waiting streams, until the new increased window size is
+ // effectively exhausted.
+ Iterator<Map.Entry<Integer,Map.Entry<Stream<?>,Integer>>> iter =
+ pending.entrySet().iterator();
+
+ while (iter.hasNext() && size > 0) {
+ Map.Entry<Integer,Map.Entry<Stream<?>,Integer>> item = iter.next();
+ Integer streamSize = streams.get(item.getKey());
+ if (streamSize == null) {
+ iter.remove();
+ } else {
+ Map.Entry<Stream<?>,Integer> e = item.getValue();
+ int requestedAmount = e.getValue();
+ // only wakes up the pending streams for which there is
+ // at least 1 byte of space in both windows
+ int minAmount = 1;
+ if (size >= minAmount && streamSize >= minAmount) {
+ size -= Math.min(streamSize, requestedAmount);
+ iter.remove();
+ if (candidates == null)
+ candidates = new ArrayList<>();
+ candidates.add(e.getKey());
+ }
+ }
+ }
+ } finally {
+ controllerLock.unlock();
+ }
+ if (candidates != null) {
+ candidates.forEach(Stream::signalWindowUpdate);
+ }
+ return true;
+ }
+
+ /**
+ * Increases the Send Window size for the given stream.
+ *
+ * If the given stream is awaiting window size, from an unfulfilled
+ * tryAcquire request, it will have its stream's {@link
+ * Stream#signalWindowUpdate()} method scheduled to run ( i.e. awoken ).
+ *
+ * @return false if, and only if, the addition of the given amount would
+ * cause the Send Window to exceed 2^31-1 (overflow), otherwise true
+ */
+ boolean increaseStreamWindow(int amount, int streamid) {
+ Stream<?> s = null;
+ controllerLock.lock();
+ try {
+ Integer size = streams.get(streamid);
+ if (size == null)
+ throw new InternalError("Expected entry for streamid: " + streamid);
+ size += amount;
+ if (size < 0)
+ return false;
+ streams.put(streamid, size);
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Stream %s window size is now %s", streamid, size);
+
+ Map.Entry<Stream<?>,Integer> p = pending.get(streamid);
+ if (p != null) {
+ int minAmount = 1;
+ // only wakes up the pending stream if there is at least
+ // 1 byte of space in both windows
+ if (size >= minAmount
+ && connectionWindowSize >= minAmount) {
+ pending.remove(streamid);
+ s = p.getKey();
+ }
+ }
+ } finally {
+ controllerLock.unlock();
+ }
+
+ if (s != null)
+ s.signalWindowUpdate();
+
+ return true;
+ }
+
+ /**
+ * Adjusts, either increases or decreases, the active streams registered
+ * with this controller. May result in a stream's Send Window size becoming
+ * negative.
+ */
+ void adjustActiveStreams(int adjustAmount) {
+ assert adjustAmount != 0;
+
+ controllerLock.lock();
+ try {
+ for (Map.Entry<Integer,Integer> entry : streams.entrySet()) {
+ int streamid = entry.getKey();
+ // the API only supports sending on Streams initialed by
+ // the client, i.e. odd stream numbers
+ if (streamid != 0 && (streamid % 2) != 0) {
+ Integer size = entry.getValue();
+ size += adjustAmount;
+ streams.put(streamid, size);
+ DEBUG_LOGGER.log(Level.DEBUG,
+ "Stream %s window size is now %s", streamid, size);
+ }
+ }
+ } finally {
+ controllerLock.unlock();
+ }
+ }
+
+ /** Returns the Send Window size for the connection. */
+ int connectionWindowSize() {
+ controllerLock.lock();
+ try {
+ return connectionWindowSize;
+ } finally {
+ controllerLock.unlock();
+ }
+ }
+
+// /** Returns the Send Window size for the given stream. */
+// int streamWindowSize(int streamid) {
+// controllerLock.lock();
+// try {
+// Integer size = streams.get(streamid);
+// if (size == null)
+// throw new InternalError("Expected entry for streamid: " + streamid);
+// return size;
+// } finally {
+// controllerLock.unlock();
+// }
+// }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/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() + ")";
+ }
+
+}
--- /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<ByteBuffer> pool = new ConcurrentLinkedQueue<>();
+
+ public ByteBufferPool() {
+ }
+
+ public ByteBufferReference get(int size) {
+ ByteBuffer buffer;
+ while ((buffer = pool.poll()) != null) {
+ if (buffer.capacity() >= size) {
+ return ByteBufferReference.of(buffer, this);
+ }
+ }
+ return ByteBufferReference.of(ByteBuffer.allocate(size), this);
+ }
+
+ public void release(ByteBuffer buffer) {
+ buffer.clear();
+ pool.offer(buffer);
+ }
+
+}
--- /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<ByteBuffer> {
+
+ private ByteBuffer buffer;
+ private final ByteBufferPool pool;
+
+ public static ByteBufferReference of(ByteBuffer buffer) {
+ return of(buffer, null);
+ }
+
+ public static ByteBufferReference of(ByteBuffer buffer, ByteBufferPool pool) {
+ Objects.requireNonNull(buffer);
+ return new ByteBufferReference(buffer, pool);
+ }
+
+ public static ByteBuffer[] toBuffers(ByteBufferReference... refs) {
+ ByteBuffer[] bufs = new ByteBuffer[refs.length];
+ for (int i = 0; i < refs.length; i++) {
+ bufs[i] = refs[i].get();
+ }
+ return bufs;
+ }
+
+ public static ByteBufferReference[] toReferences(ByteBuffer... buffers) {
+ ByteBufferReference[] refs = new ByteBufferReference[buffers.length];
+ for (int i = 0; i < buffers.length; i++) {
+ refs[i] = of(buffers[i]);
+ }
+ return refs;
+ }
+
+
+ public static void clear(ByteBufferReference[] refs) {
+ for(ByteBufferReference ref : refs) {
+ ref.clear();
+ }
+ }
+
+ private ByteBufferReference(ByteBuffer buffer, ByteBufferPool pool) {
+ this.buffer = buffer;
+ this.pool = pool;
+ }
+
+ @Override
+ public ByteBuffer get() {
+ ByteBuffer buf = this.buffer;
+ assert buf!=null : "getting ByteBuffer after clearance";
+ return buf;
+ }
+
+ public void clear() {
+ ByteBuffer buf = this.buffer;
+ assert buf!=null : "double ByteBuffer clearance";
+ this.buffer = null;
+ if (pool != null) {
+ pool.release(buf);
+ }
+ }
+}
--- /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);
+ }
+}
--- /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".
+ * <p>
+ * 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<String> 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.
+ * <p>
+ * The logger will add some decoration to the printed message, in the form of
+ * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+ *
+ * @apiNote To obtain a logger that will always print things on stderr in
+ * addition to forwarding to the internal logger, use
+ * {@code new DebugLogger(logger, this::dbgTag, Level.OFF, Level.ALL);}.
+ * To obtain a logger that will only forward to the internal logger,
+ * use {@code new DebugLogger(logger, this::dbgTag, Level.OFF, Level.OFF);}.
+ *
+ * @param logger The internal logger to which messages will be forwarded.
+ * This should be either {@link #HPACK} or {@link #HTTP};
+ *
+ * @param dbgTag A lambda that returns a string that identifies the caller
+ * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
+ * @param outLevel The level above which messages will be also printed on
+ * System.out (in addition to being forwarded to the internal logger).
+ * @param errLevel The level above which messages will be also printed on
+ * System.err (in addition to being forwarded to the internal logger).
+ *
+ * @return A logger for HTTP internal debug traces
+ */
+ private DebugLogger(Logger logger,
+ Supplier<String> dbgTag,
+ Level outLevel,
+ Level errLevel) {
+ this.dbgTag = dbgTag;
+ this.errLevel = errLevel;
+ this.outLevel = outLevel;
+ this.logger = Objects.requireNonNull(logger);
+ // support only static configuration.
+ this.debugOn = isEnabled(Level.DEBUG);
+ this.traceOn = isEnabled(Level.TRACE);
+ }
+
+ @Override
+ public String getName() {
+ return logger.getName();
+ }
+
+ private boolean isEnabled(Level level) {
+ if (level == Level.OFF) return false;
+ int severity = level.getSeverity();
+ return severity >= errLevel.getSeverity()
+ || severity >= outLevel.getSeverity()
+ || logger.isLoggable(level);
+ }
+
+ @Override
+ public boolean isLoggable(Level level) {
+ // fast path, we assume these guys never change.
+ // support only static configuration.
+ if (level == Level.DEBUG) return debugOn;
+ if (level == Level.TRACE) return traceOn;
+ return isEnabled(level);
+ }
+
+ @Override
+ public void log(Level level, ResourceBundle unused,
+ String format, Object... params) {
+ // fast path, we assume these guys never change.
+ // support only static configuration.
+ if (level == Level.DEBUG && !debugOn) return;
+ if (level == Level.TRACE && !traceOn) return;
+
+ int severity = level.getSeverity();
+ if (errLevel != Level.OFF
+ && errLevel.getSeverity() <= severity) {
+ print(System.err, level, format, params, null);
+ }
+ if (outLevel != Level.OFF
+ && outLevel.getSeverity() <= severity) {
+ print(System.out, level, format, params, null);
+ }
+ if (logger.isLoggable(level)) {
+ logger.log(level, unused,
+ getFormat(new StringBuilder(), format, params).toString(),
+ params);
+ }
+ }
+
+ @Override
+ public void log(Level level, ResourceBundle unused, String msg,
+ Throwable thrown) {
+ // fast path, we assume these guys never change.
+ if (level == Level.DEBUG && !debugOn) return;
+ if (level == Level.TRACE && !traceOn) return;
+
+ if (errLevel != Level.OFF
+ && errLevel.getSeverity() <= level.getSeverity()) {
+ print(System.err, level, msg, null, thrown);
+ }
+ if (outLevel != Level.OFF
+ && outLevel.getSeverity() <= level.getSeverity()) {
+ print(System.out, level, msg, null, thrown);
+ }
+ if (logger.isLoggable(level)) {
+ logger.log(level, unused,
+ getFormat(new StringBuilder(), msg, null).toString(),
+ thrown);
+ }
+ }
+
+ private void print(PrintStream out, Level level, String msg,
+ Object[] params, Throwable t) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(level.name()).append(':').append(' ');
+ sb = format(sb, msg, params);
+ if (t != null) sb.append(' ').append(t.toString());
+ out.println(sb.toString());
+ if (t != null) {
+ t.printStackTrace(out);
+ }
+ }
+
+ private StringBuilder decorate(StringBuilder sb, String msg) {
+ String tag = dbgTag == null ? null : dbgTag.get();
+ String res = msg == null ? "" : msg;
+ long elapsed = System.nanoTime() - START_NANOS;
+ long millis = elapsed / 1000_000;
+ long secs = millis / 1000;
+ sb.append('[').append(Thread.currentThread().getName()).append(']')
+ .append(' ').append('[');
+ if (secs > 0) {
+ sb.append(secs).append('s');
+ }
+ millis = millis % 1000;
+ if (millis > 0) {
+ if (secs > 0) sb.append(' ');
+ sb.append(millis).append("ms");
+ }
+ sb.append(']').append(' ');
+ if (tag != null) {
+ sb.append(tag).append(' ');
+ }
+ sb.append(res);
+ return sb;
+ }
+
+
+ private StringBuilder getFormat(StringBuilder sb, String format, Object[] params) {
+ if (format == null || params == null || params.length == 0) {
+ return decorate(sb, format);
+ } else if (format.contains("{0}") || format.contains("{1}")) {
+ return decorate(sb, format);
+ } else if (format.contains("%s") || format.contains("%d")) {
+ try {
+ return decorate(sb, String.format(format, params));
+ } catch (Throwable t) {
+ return decorate(sb, format);
+ }
+ } else {
+ return decorate(sb, format);
+ }
+ }
+
+ private StringBuilder format(StringBuilder sb, String format, Object[] params) {
+ if (format == null || params == null || params.length == 0) {
+ return decorate(sb, format);
+ } else if (format.contains("{0}") || format.contains("{1}")) {
+ return decorate(sb, java.text.MessageFormat.format(format, params));
+ } else if (format.contains("%s") || format.contains("%d")) {
+ try {
+ return decorate(sb, String.format(format, params));
+ } catch (Throwable t) {
+ return decorate(sb, format);
+ }
+ } else {
+ return decorate(sb, format);
+ }
+ }
+
+ public static DebugLogger createHttpLogger(Supplier<String> dbgTag, Level outLevel, Level errLevel) {
+ return new DebugLogger(HTTP, dbgTag, outLevel, errLevel);
+ }
+
+ public static DebugLogger createHpackLogger(Supplier<String> dbgTag, Level outLevel, Level errLevel) {
+ return new DebugLogger(HPACK, dbgTag, outLevel, errLevel);
+ }
+}
--- /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).
+ *
+ * <p> Demand is the aggregated number of elements requested by a Subscriber
+ * which is yet to be delivered (fulfilled) by the Publisher.
+ */
+public final class Demand {
+
+ private final AtomicLong val = new AtomicLong();
+
+ /**
+ * Increases this demand by the specified positive value.
+ *
+ * @param n
+ * increment {@code > 0}
+ *
+ * @return {@code true} iff prior to this operation this demand was fulfilled
+ */
+ public boolean increase(long n) {
+ if (n <= 0) {
+ throw new IllegalArgumentException(String.valueOf(n));
+ }
+ long prev = val.getAndAccumulate(n, (p, i) -> p + i < 0 ? Long.MAX_VALUE : p + i);
+ return prev == 0;
+ }
+
+ /**
+ * Tries to decrease this demand by the specified positive value.
+ *
+ * <p> The actual value this demand has been decreased by might be less than
+ * {@code n}, including {@code 0} (no decrease at all).
+ *
+ * @param n
+ * decrement {@code > 0}
+ *
+ * @return a value {@code m} ({@code 0 <= m <= n}) this demand has been
+ * actually decreased by
+ */
+ public long decreaseAndGet(long n) {
+ if (n <= 0) {
+ throw new IllegalArgumentException(String.valueOf(n));
+ }
+ long p, d;
+ do {
+ d = val.get();
+ p = Math.min(d, n);
+ } while (!val.compareAndSet(d, d - p));
+ return p;
+ }
+
+ /**
+ * Tries to decrease this demand by {@code 1}.
+ *
+ * @return {@code true} iff this demand has been decreased by {@code 1}
+ */
+ public boolean tryDecrement() {
+ return decreaseAndGet(1) == 1;
+ }
+
+ /**
+ * @return {@code true} iff there is no unfulfilled demand
+ */
+ public boolean isFulfilled() {
+ return val.get() == 0;
+ }
+
+ /**
+ * Resets this object to its initial state.
+ */
+ public void reset() {
+ val.set(0);
+ }
+
+ /**
+ * Returns the current value of this demand.
+ *
+ * @return the current value of this demand
+ */
+ public long get() {
+ return val.get();
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(val.get());
+ }
+}
--- /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.
+ * <p>
+ * The {@code connectFlows} method should be called to connect the bidirectional
+ * flow. A FlowTube supports handing over the same read subscription to different
+ * sequential read subscribers over time. When {@code connectFlows(writePublisher,
+ * readSubscriber} is called, the FlowTube will call {@code dropSubscription} on
+ * its former readSubscriber, and {@code onSubscribe} on its new readSubscriber.
+ */
+public interface FlowTube extends
+ Flow.Publisher<List<ByteBuffer>>,
+ Flow.Subscriber<List<ByteBuffer>> {
+
+ /**
+ * A subscriber for reading from the bidirectional flow.
+ * A TubeSubscriber is a {@code Flow.Subscriber} that can be canceled
+ * by calling {@code dropSubscription()}.
+ * Once {@code dropSubscription()} is called, the {@code TubeSubscriber}
+ * should stop calling any method on its subscription.
+ */
+ static interface TubeSubscriber extends Flow.Subscriber<List<ByteBuffer>> {
+
+ /**
+ * Called when the flow is connected again, and the subscription
+ * is handed over to a new subscriber.
+ * Once {@code dropSubscription()} is called, the {@code TubeSubscriber}
+ * should stop calling any method on its subscription.
+ */
+ default void dropSubscription() { }
+
+ }
+
+ /**
+ * A publisher for writing to the bidirectional flow.
+ */
+ static interface TubePublisher extends Flow.Publisher<List<ByteBuffer>> {
+
+ }
+
+ /**
+ * Connects the bidirectional flows to a write {@code Publisher} and a
+ * read {@code Subscriber}. This method can be called sequentially
+ * several times to switch existing publishers and subscribers to a new
+ * pair of write subscriber and read publisher.
+ * @param writePublisher A new publisher for writing to the bidirectional flow.
+ * @param readSubscriber A new subscriber for reading from the bidirectional
+ * flow.
+ */
+ default void connectFlows(TubePublisher writePublisher,
+ TubeSubscriber readSubscriber) {
+
+ this.subscribe(readSubscriber);
+ writePublisher.subscribe(this);
+ }
+
+ /**
+ * Returns true if this flow was completed, either exceptionally
+ * or normally (EOF reached).
+ * @return true if the flow is finished
+ */
+ boolean isFinished();
+
+
+ /**
+ * Returns {@code s} if {@code s} is a {@code TubeSubscriber}, otherwise
+ * wraps it in a {@code TubeSubscriber}.
+ * Using the wrapper is only appropriate in the case where
+ * {@code dropSubscription} doesn't need to be implemented, and the
+ * {@code TubeSubscriber} is subscribed only once.
+ *
+ * @param s a subscriber for reading.
+ * @return a {@code TubeSubscriber}: either {@code s} if {@code s} is a
+ * {@code TubeSubscriber}, otherwise a {@code TubeSubscriber}
+ * wrapper that delegates to {@code s}
+ */
+ static TubeSubscriber asTubeSubscriber(Flow.Subscriber<? super List<ByteBuffer>> s) {
+ if (s instanceof TubeSubscriber) {
+ return (TubeSubscriber) s;
+ }
+ return new AbstractTubeSubscriber.TubeSubscriberWrapper(s);
+ }
+
+ /**
+ * Returns {@code s} if {@code s} is a {@code TubePublisher}, otherwise
+ * wraps it in a {@code TubePublisher}.
+ *
+ * @param p a publisher for writing.
+ * @return a {@code TubePublisher}: either {@code s} if {@code s} is a
+ * {@code TubePublisher}, otherwise a {@code TubePublisher}
+ * wrapper that delegates to {@code s}
+ */
+ static TubePublisher asTubePublisher(Flow.Publisher<List<ByteBuffer>> p) {
+ if (p instanceof TubePublisher) {
+ return (TubePublisher) p;
+ }
+ return new AbstractTubePublisher.TubePublisherWrapper(p);
+ }
+
+ /**
+ * Convenience abstract class for {@code TubePublisher} implementations.
+ * It is not required that a {@code TubePublisher} implementation extends
+ * this class.
+ */
+ static abstract class AbstractTubePublisher implements TubePublisher {
+ static final class TubePublisherWrapper extends AbstractTubePublisher {
+ final Flow.Publisher<List<ByteBuffer>> delegate;
+ public TubePublisherWrapper(Flow.Publisher<List<ByteBuffer>> delegate) {
+ this.delegate = delegate;
+ }
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+ delegate.subscribe(subscriber);
+ }
+ }
+ }
+
+ /**
+ * Convenience abstract class for {@code TubeSubscriber} implementations.
+ * It is not required that a {@code TubeSubscriber} implementation extends
+ * this class.
+ */
+ static abstract class AbstractTubeSubscriber implements TubeSubscriber {
+ static final class TubeSubscriberWrapper extends AbstractTubeSubscriber {
+ final Flow.Subscriber<? super List<ByteBuffer>> delegate;
+ TubeSubscriberWrapper(Flow.Subscriber<? super List<ByteBuffer>> delegate) {
+ this.delegate = delegate;
+ }
+ @Override
+ public void dropSubscription() {}
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ delegate.onSubscribe(subscription);
+ }
+ @Override
+ public void onNext(List<ByteBuffer> item) {
+ delegate.onNext(item);
+ }
+ @Override
+ public void onError(Throwable throwable) {
+ delegate.onError(throwable);
+ }
+ @Override
+ public void onComplete() {
+ delegate.onComplete();
+ }
+ }
+
+ }
+
+}
--- /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<String,List<String>> headers;
+
+ public HttpHeadersImpl() {
+ headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ }
+
+ @Override
+ public Map<String, List<String>> map() {
+ return Collections.unmodifiableMap(headers);
+ }
+
+ // package private mutators
+
+ public HttpHeadersImpl deepCopy() {
+ HttpHeadersImpl h1 = new HttpHeadersImpl();
+ for (Map.Entry<String,List<String>> entry : headers.entrySet()) {
+ List<String> 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<String> values = new ArrayList<>(1); // most headers has one value
+ values.add(value);
+ headers.put(name, values);
+ }
+}
--- /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<String> msgSupplier) {
+ if (ssl()) {
+ logger.log(Level.INFO, "SSL: " + msgSupplier.get());
+ }
+ }
+
+ public static void logTrace(String s, Object... s1) {
+ if (trace()) {
+ String format = "TRACE: " + s;
+ logger.log(Level.INFO, format, s1);
+ }
+ }
+
+ public static void logRequest(String s, Object... s1) {
+ if (requests()) {
+ logger.log(Level.INFO, "REQUEST: " + s, s1);
+ }
+ }
+
+ public static void logResponse(Supplier<String> supplier) {
+ if (requests()) {
+ logger.log(Level.INFO, "RESPONSE: " + supplier.get());
+ }
+ }
+
+ public static void logHeaders(String s, Object... s1) {
+ if (headers()) {
+ logger.log(Level.INFO, "HEADERS: " + s, s1);
+ }
+ }
+
+ public static boolean loggingFrame(Class<? extends Http2Frame> clazz) {
+ if (frametypes == ALL) {
+ return true;
+ }
+ if (clazz == DataFrame.class) {
+ return (frametypes & DATA) != 0;
+ } else if (clazz == WindowUpdateFrame.class) {
+ return (frametypes & WINDOW_UPDATES) != 0;
+ } else {
+ return (frametypes & CONTROL) != 0;
+ }
+ }
+
+ public static void logFrames(Http2Frame f, String direction) {
+ if (frames() && loggingFrame(f.getClass())) {
+ logger.log(Level.INFO, "FRAME: " + direction + ": " + f.toString());
+ }
+ }
+
+ public static void logParams(SSLParameters p) {
+ if (!Log.ssl()) {
+ return;
+ }
+
+ if (p == null) {
+ Log.logSSL("SSLParameters: Null params");
+ return;
+ }
+
+ final StringBuilder sb = new StringBuilder("SSLParameters:");
+ final List<Object> params = new ArrayList<>();
+ if (p.getCipherSuites() != null) {
+ for (String cipher : p.getCipherSuites()) {
+ sb.append("\n cipher: {")
+ .append(params.size()).append("}");
+ params.add(cipher);
+ }
+ }
+
+ // SSLParameters.getApplicationProtocols() can't return null
+ // JDK 8 EXCL START
+ for (String approto : p.getApplicationProtocols()) {
+ sb.append("\n application protocol: {")
+ .append(params.size()).append("}");
+ params.add(approto);
+ }
+ // JDK 8 EXCL END
+
+ if (p.getProtocols() != null) {
+ for (String protocol : p.getProtocols()) {
+ sb.append("\n protocol: {")
+ .append(params.size()).append("}");
+ params.add(protocol);
+ }
+ }
+
+ if (p.getServerNames() != null) {
+ for (SNIServerName sname : p.getServerNames()) {
+ sb.append("\n server name: {")
+ .append(params.size()).append("}");
+ params.add(sname.toString());
+ }
+ }
+ sb.append('\n');
+
+ Log.logSSL(sb.toString(), params.toArray());
+ }
+
+ public static void dumpHeaders(StringBuilder sb, String prefix, HttpHeaders headers) {
+ if (headers != null) {
+ Map<String,List<String>> h = headers.map();
+ Set<Map.Entry<String,List<String>>> entries = h.entrySet();
+ for (Map.Entry<String,List<String>> entry : entries) {
+ String key = entry.getKey();
+ List<String> values = entry.getValue();
+ sb.append(prefix).append(key).append(":");
+ for (String value : values) {
+ sb.append(' ').append(value);
+ }
+ sb.append('\n');
+ }
+ }
+ }
+
+
+ // not instantiable
+ private Log() {}
+}
--- /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<T> extends CompletableFuture<T> {
+
+ @FunctionalInterface
+ public interface ExceptionalSupplier<U> {
+ U get() throws Throwable;
+ }
+
+ private final static AtomicLong TOKENS = new AtomicLong();
+ private final long id;
+
+ public static <U> MinimalFuture<U> completedFuture(U value) {
+ MinimalFuture<U> f = new MinimalFuture<>();
+ f.complete(value);
+ return f;
+ }
+
+ public static <U> CompletableFuture<U> failedFuture(Throwable ex) {
+ requireNonNull(ex);
+ MinimalFuture<U> f = new MinimalFuture<>();
+ f.completeExceptionally(ex);
+ return f;
+ }
+
+ public static <U> CompletableFuture<U> supply(ExceptionalSupplier<U> supplier) {
+ CompletableFuture<U> cf = new MinimalFuture<>();
+ try {
+ U value = supplier.get();
+ cf.complete(value);
+ } catch (Throwable t) {
+ cf.completeExceptionally(t);
+ }
+ return cf;
+ }
+
+ public MinimalFuture() {
+ super();
+ this.id = TOKENS.incrementAndGet();
+ }
+
+ @Override
+ public <U> MinimalFuture<U> newIncompleteFuture() {
+ return new MinimalFuture<>();
+ }
+
+ @Override
+ public void obtrudeValue(T value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void obtrudeException(Throwable ex) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + " (id=" + id +")";
+ }
+
+ public static <U> MinimalFuture<U> of(CompletionStage<U> stage) {
+ MinimalFuture<U> cf = new MinimalFuture<>();
+ stage.whenComplete((r,t) -> complete(cf, r, t));
+ return cf;
+ }
+
+ private static <U> void complete(CompletableFuture<U> cf, U result, Throwable t) {
+ if (t == null) {
+ cf.complete(result);
+ } else {
+ cf.completeExceptionally(t);
+ }
+ }
+}
--- /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<T, U> {
+
+ public Pair(T first, U second) {
+ this.second = second;
+ this.first = first;
+ }
+
+ public final T first;
+ public final U second;
+
+ // Because 'pair()' is shorter than 'new Pair<>()'.
+ // Sometimes this difference might be very significant (especially in a
+ // 80-ish characters boundary). Sorry diamond operator.
+ public static <T, U> Pair<T, U> pair(T first, U second) {
+ return new Pair<>(first, second);
+ }
+
+ @Override
+ public String toString() {
+ return "(" + first + ", " + second + ")";
+ }
+}
--- /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.
+ *
+ * <p> Constructor takes two Flow.Subscribers: one that receives the network
+ * data (after it has been encrypted by SSLFlowDelegate) data, and one that
+ * receives the application data (before it has been encrypted by SSLFlowDelegate).
+ *
+ * <p> Methods upstreamReader() and upstreamWriter() return the corresponding
+ * Flow.Subscribers containing Flows for the encrypted/decrypted upstream data.
+ * See diagram below.
+ *
+ * <p> How Flow.Subscribers are used in this class, and where they come from:
+ * <pre>
+ * {@code
+ *
+ *
+ *
+ * ---------> data flow direction
+ *
+ *
+ * +------------------+
+ * upstreamWriter | | downWriter
+ * ---------------> | | ------------>
+ * obtained from this | | supplied to constructor
+ * | SSLFlowDelegate |
+ * downReader | | upstreamReader
+ * <--------------- | | <--------------
+ * supplied to constructor | | obtained from this
+ * +------------------+
+ * }
+ * </pre>
+ */
+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<Void> cf;
+ final CompletableFuture<String> 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<? super List<ByteBuffer>> downReader,
+ Subscriber<? super List<ByteBuffer>> 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<Void> 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<? super List<ByteBuffer>> downReader,
+ Subscriber<? super List<ByteBuffer>> downWriter) {
+ this.reader.subscribe(downReader);
+ this.writer.subscribe(downWriter);
+ }
+
+ /**
+ * Returns a CompletableFuture<String> which completes after
+ * the initial handshake completes, and which contains the negotiated
+ * alpn.
+ */
+ public CompletableFuture<String> alpn() {
+ return alpnCF;
+ }
+
+ private void setALPN() {
+ // Handshake is finished. So, can retrieve the ALPN now
+ if (alpnCF.isDone())
+ return;
+ String alpn = engine.getApplicationProtocol();
+ 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<ByteBuffer> 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<ByteBuffer> buffers, boolean complete) {
+ synchronized (readBufferLock) {
+ for (ByteBuffer buf : buffers) {
+ readBuf.compact();
+ while (readBuf.remaining() < buf.remaining())
+ reallocReadBuf();
+ readBuf.put(buf);
+ readBuf.flip();
+ }
+ if (complete) {
+ this.completing = complete;
+ }
+ }
+ }
+
+ void schedule() {
+ scheduler.runOrSchedule();
+ }
+
+ void stop() {
+ 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<Void> completion() {
+ return cf;
+ }
+
+ public interface Monitorable {
+ public String getInfo();
+ }
+
+ public static class Monitor extends Thread {
+ final List<Monitorable> list;
+ static Monitor themon;
+
+ static {
+ themon = new Monitor();
+ themon.start(); // uncomment to enable Monitor
+ }
+
+ Monitor() {
+ super("Monitor");
+ setDaemon(true);
+ list = Collections.synchronizedList(new LinkedList<>());
+ }
+
+ void addTarget(Monitorable o) {
+ list.add(o);
+ }
+
+ public static void add(Monitorable o) {
+ themon.addTarget(o);
+ }
+
+ @Override
+ public void run() {
+ System.out.println("Monitor starting");
+ 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<ByteBuffer> 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<ByteBuffer> 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<ByteBuffer> l) {
+ synchronized (l) {
+ Iterator<ByteBuffer> iter = l.iterator();
+ while (iter.hasNext()) {
+ ByteBuffer b = iter.next();
+ if (!b.hasRemaining() && b != SENTINEL) {
+ iter.remove();
+ }
+ }
+ }
+ }
+
+ /**
+ * States for handshake. We avoid races when accessing/updating the AtomicInt
+ * because updates always schedule an additional call to both the read()
+ * and write() functions.
+ */
+ private static final int NOT_HANDSHAKING = 0;
+ private static final int HANDSHAKING = 1;
+ private static final int 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<String> 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<Runnable> 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<Runnable> obtainTasks() {
+ List<Runnable> l = new ArrayList<>();
+ Runnable r;
+ while ((r = engine.getDelegatedTask()) != null) {
+ l.add(r);
+ }
+ return l;
+ }
+
+ private void executeTasks(List<Runnable> tasks) {
+ exec.execute(() -> {
+ try {
+ handshakeState.getAndUpdate((current) -> current | DOING_TASKS);
+ List<Runnable> nextTasks = tasks;
+ do {
+ nextTasks.forEach(Runnable::run);
+ if (engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
+ nextTasks = obtainTasks();
+ } else {
+ break;
+ }
+ } while (true);
+ handshakeState.getAndUpdate((current) -> current & ~DOING_TASKS);
+ writer.addData(HS_TRIGGER);
+ resumeActivity();
+ } catch (Throwable t) {
+ handleError(t);
+ }
+ });
+ }
+
+
+ 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<List<ByteBuffer>> upstreamReader() {
+ return reader;
+ }
+
+ /**
+ * Returns the upstream Flow.Subscriber of the writing (outgoing) side.
+ * This flow contains the plaintext data before it is encrypted.
+ */
+ public Flow.Subscriber<List<ByteBuffer>> upstreamWriter() {
+ return writer;
+ }
+
+ public boolean resumeReader() {
+ return reader.signalScheduling();
+ }
+
+ public void resetReaderDemand() {
+ reader.resetDownstreamDemand();
+ }
+
+ static class EngineResult {
+ final SSLEngineResult result;
+ final ByteBuffer destBuffer;
+
+ // normal result
+ EngineResult(SSLEngineResult result) {
+ this(result, null);
+ }
+
+ EngineResult(SSLEngineResult result, ByteBuffer destBuffer) {
+ this.result = result;
+ this.destBuffer = destBuffer;
+ }
+
+ // 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;
+ }
+ }
+ }
+}
--- /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.
+ * <p>
+ * The following diagram shows a typical usage of the SSLTube, where
+ * the SSLTube wraps a SocketTube on the right hand side, and is connected
+ * to an HttpConnection on the left hand side.
+ *
+ * <preformatted>{@code
+ * +---------- SSLTube -------------------------+
+ * | |
+ * | +---SSLFlowDelegate---+ |
+ * HttpConnection | | | | SocketTube
+ * read sink <- SSLSubscriberW. <- Reader <- upstreamR.() <---- read source
+ * (a subscriber) | | \ / | | (a publisher)
+ * | | SSLEngine | |
+ * HttpConnection | | / \ | | SocketTube
+ * write source -> SSLSubscriptionW. -> upstreamW.() -> Writer ----> write sink
+ * (a publisher) | | | | (a subscriber)
+ * | +---------------------+ |
+ * | |
+ * +---------------------------------------------+
+ * }</preformatted>
+ */
+public class SSLTube implements FlowTube {
+
+ 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<? super List<ByteBuffer>> downReader,
+ Flow.Subscriber<? super List<ByteBuffer>> downWriter) {
+ assert downWriter == tube;
+ assert downReader == readSubscriber;
+
+ // Connect the read sink first. That's the left-hand side
+ // downstream subscriber from the HttpConnection (or more
+ // accurately, the SSLSubscriberWrapper that will wrap it
+ // when SSLTube::connectFlows is called.
+ reader.subscribe(downReader);
+
+ // Connect the right hand side tube (the socket tube).
+ //
+ // The SSLFlowDelegate.writer publishes ByteBuffer to
+ // the SocketTube for writing on the socket, and the
+ // SSLFlowDelegate::upstreamReader subscribes to the
+ // SocketTube to receive ByteBuffers read from the socket.
+ //
+ // Basically this method is equivalent to:
+ // // connect the read source:
+ // // subscribe the SSLFlowDelegate upstream reader
+ // // to the socket tube publisher.
+ // tube.subscribe(upstreamReader());
+ // // connect the write sink:
+ // // subscribe the socket tube write subscriber
+ // // with the SSLFlowDelegate downstream writer.
+ // writer.subscribe(tube);
+ tube.connectFlows(FlowTube.asTubePublisher(writer),
+ FlowTube.asTubeSubscriber(upstreamReader()));
+
+ // Finally connect the write source. That's the left
+ // hand side publisher which will push ByteBuffer for
+ // writing and encryption to the SSLFlowDelegate.
+ // The writeSubscription is in fact the SSLSubscriptionWrapper
+ // that will wrap the subscription provided by the
+ // HttpConnection publisher when SSLTube::connectFlows
+ // is called.
+ upstreamWriter().onSubscribe(writeSubscription);
+ }
+ }
+
+ public CompletableFuture<String> getALPN() {
+ return sslDelegate.alpn();
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
+ readSubscriber.dropSubscription();
+ readSubscriber.setDelegate(s);
+ s.onSubscribe(readSubscription);
+ }
+
+ /**
+ * Tells whether, or not, this FlowTube has finished receiving data.
+ *
+ * @return true when one of this FlowTube Subscriber's OnError or onComplete
+ * methods have been invoked
+ */
+ @Override
+ public boolean isFinished() {
+ return finished;
+ }
+
+ private volatile Flow.Subscription readSubscription;
+
+ // The DelegateWrapper wraps a subscribed {@code Flow.Subscriber} and
+ // tracks the subscriber's state. In particular it makes sure that
+ // onComplete/onError are not called before onSubscribed.
+ final static class DelegateWrapper implements FlowTube.TubeSubscriber {
+ private final FlowTube.TubeSubscriber delegate;
+ private final System.Logger debug;
+ volatile boolean subscribedCalled;
+ volatile boolean subscribedDone;
+ volatile boolean completed;
+ volatile Throwable error;
+ DelegateWrapper(Flow.Subscriber<? super List<ByteBuffer>> 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<ByteBuffer> item) {
+ assert subscribedCalled;
+ delegate.onNext(item);
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ onSubscribe(delegate::onSubscribe, subscription);
+ }
+
+ private void onSubscribe(Consumer<Flow.Subscription> method,
+ Flow.Subscription subscription) {
+ subscribedCalled = true;
+ method.accept(subscription);
+ Throwable x;
+ boolean finished;
+ synchronized (this) {
+ subscribedDone = true;
+ x = error;
+ finished = completed;
+ }
+ if (x != null) {
+ 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<DelegateWrapper> pendingDelegate =
+ new AtomicReference<>();
+ private volatile DelegateWrapper subscribed;
+ private volatile boolean onCompleteReceived;
+ private final AtomicReference<Throwable> errorRef
+ = new AtomicReference<>();
+
+ // setDelegate can be called asynchronously when the SSLTube flow
+ // is connected. At this time the permanent subscriber (this class)
+ // may already be subscribed (readSubscription != null) or not.
+ // 1. If it's already subscribed (readSubscription != null), we
+ // are going to signal the SSLFlowDelegate reader, and make sure
+ // onSubscribed is called within the reader flow
+ // 2. If it's not yet subscribed (readSubscription == null), then
+ // we're going to wait for onSubscribe to be called.
+ //
+ void setDelegate(Flow.Subscriber<? super List<ByteBuffer>> delegate) {
+ 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<ByteBuffer> item) {
+ subscribed.onNext(item);
+ }
+
+ public void onErrorImpl(Throwable throwable) {
+ // The following twisted logic is just here that we don't invoke
+ // onError before onSubscribe. It also prevents race conditions
+ // if onError is invoked concurrently with setDelegate.
+ // See setDelegate.
+
+ errorRef.compareAndSet(null, throwable);
+ Throwable failed = errorRef.get();
+ finished = true;
+ 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<ByteBuffer> 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 + ")";
+ }
+
+}
--- /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.
+ *
+ * <p> This class can be used as a synchronization aid that assists a number of
+ * parties in running a task in a mutually exclusive fashion.
+ *
+ * <p> To run the task, a party invokes {@code runOrSchedule}. To permanently
+ * prevent the task from subsequent runs, the party invokes {@code stop}.
+ *
+ * <p> The parties can, but do not have to, operate in different threads.
+ *
+ * <p> The task can be either synchronous ( completes when its {@code run}
+ * method returns ), or asynchronous ( completed when its
+ * {@code DeferredCompleter} is explicitly completed ).
+ *
+ * <p> The next run of the task will not begin until the previous run has
+ * finished.
+ *
+ * <p> The task may invoke {@code runOrSchedule} itself, which may be a normal
+ * situation.
+ */
+public final class SequentialScheduler {
+
+ /*
+ Since the task is fixed and known beforehand, no blocking synchronization
+ (locks, queues, etc.) is required. The job can be done solely using
+ nonblocking primitives.
+
+ The machinery below addresses two problems:
+
+ 1. Running the task in a sequential order (no concurrent runs):
+
+ begin, end, begin, end...
+
+ 2. Avoiding indefinite recursion:
+
+ begin
+ end
+ begin
+ end
+ ...
+
+ Problem #1 is solved with a finite state machine with 4 states:
+
+ BEGIN, AGAIN, END, and STOP.
+
+ Problem #2 is solved with a "state modifier" OFFLOAD.
+
+ Parties invoke `runOrSchedule()` to signal the task must run. A party
+ that has invoked `runOrSchedule()` either begins the task or exploits the
+ party that is either beginning the task or ending it.
+
+ The party that is trying to end the task either ends it or begins it
+ again.
+
+ To avoid indefinite recursion, before re-running the task the
+ TryEndDeferredCompleter sets the OFFLOAD bit, signalling to its "child"
+ TryEndDeferredCompleter that this ("parent") TryEndDeferredCompleter is
+ available and the "child" must offload the task on to the "parent". Then
+ a race begins. Whichever invocation of TryEndDeferredCompleter.complete
+ manages to unset OFFLOAD bit first does not do the work.
+
+ There is at most 1 thread that is beginning the task and at most 2
+ threads that are trying to end it: "parent" and "child". In case of a
+ synchronous task "parent" and "child" are the same thread.
+ */
+
+ /**
+ * An interface to signal the completion of a {@link RestartableTask}.
+ *
+ * <p> The invocation of {@code complete} completes the task. The invocation
+ * of {@code complete} may restart the task, if an attempt has previously
+ * been made to run the task while it was already running.
+ *
+ * @apiNote {@code DeferredCompleter} is useful when a task is not necessary
+ * complete when its {@code run} method returns, but will complete at a
+ * later time, and maybe in different thread. This type exists for
+ * readability purposes at use-sites only.
+ */
+ public static abstract class DeferredCompleter {
+
+ /** Extensible from this (outer) class ONLY. */
+ private DeferredCompleter() { }
+
+ /** Completes the task. Must be called once, and once only. */
+ public abstract void complete();
+ }
+
+ /**
+ * A restartable task.
+ */
+ @FunctionalInterface
+ public interface RestartableTask {
+
+ /**
+ * The body of the task.
+ *
+ * @param taskCompleter
+ * A completer that must be invoked once, and only once,
+ * when this task is logically finished
+ */
+ void run(DeferredCompleter taskCompleter);
+ }
+
+ /**
+ * A simple and self-contained task that completes once its {@code run}
+ * method returns.
+ */
+ public static abstract class CompleteRestartableTask
+ implements RestartableTask
+ {
+ @Override
+ public final void run(DeferredCompleter taskCompleter) {
+ try {
+ run();
+ } finally {
+ taskCompleter.complete();
+ }
+ }
+
+ /** The body of the task. */
+ protected abstract void run();
+ }
+
+ /**
+ * A task that runs its main loop within a synchronized block to provide
+ * memory visibility between runs. Since the main loop can't run concurrently,
+ * the lock shouldn't be contended and no deadlock should ever be possible.
+ */
+ public static final class SynchronizedRestartableTask
+ extends CompleteRestartableTask {
+
+ private final Runnable mainLoop;
+ private final Object lock = new Object();
+
+ public SynchronizedRestartableTask(Runnable mainLoop) {
+ this.mainLoop = mainLoop;
+ }
+
+ @Override
+ protected void run() {
+ synchronized(lock) {
+ mainLoop.run();
+ }
+ }
+ }
+
+ private static final int OFFLOAD = 1;
+ private static final int AGAIN = 2;
+ private static final int BEGIN = 4;
+ private static final int STOP = 8;
+ private static final int END = 16;
+
+ private final AtomicInteger state = new AtomicInteger(END);
+ private final RestartableTask restartableTask;
+ private final DeferredCompleter completer;
+ private final SchedulableTask schedulableTask;
+
+ /**
+ * An auxiliary task that starts the restartable task:
+ * {@code restartableTask.run(completer)}.
+ */
+ private final class SchedulableTask implements Runnable {
+ @Override
+ public void run() {
+ restartableTask.run(completer);
+ }
+ }
+
+ public SequentialScheduler(RestartableTask restartableTask) {
+ this.restartableTask = requireNonNull(restartableTask);
+ this.completer = new TryEndDeferredCompleter();
+ this.schedulableTask = new SchedulableTask();
+ }
+
+ /**
+ * Runs or schedules the task to be run.
+ *
+ * @implSpec The recursion which is possible here must be bounded:
+ *
+ * <pre>{@code
+ * this.runOrSchedule()
+ * completer.complete()
+ * this.runOrSchedule()
+ * ...
+ * }</pre>
+ *
+ * @implNote The recursion in this implementation has the maximum
+ * depth of 1.
+ */
+ public void runOrSchedule() {
+ runOrSchedule(schedulableTask, null);
+ }
+
+ /**
+ * Executes or schedules the task to be executed in the provided executor.
+ *
+ * <p> This method can be used when potential executing from a calling
+ * thread is not desirable.
+ *
+ * @param executor
+ * An executor in which to execute the task, if the task needs
+ * to be executed.
+ *
+ * @apiNote The given executor can be {@code null} in which case calling
+ * {@code runOrSchedule(null)} is strictly equivalent to calling
+ * {@code runOrSchedule()}.
+ */
+ public void runOrSchedule(Executor executor) {
+ runOrSchedule(schedulableTask, executor);
+ }
+
+ private void runOrSchedule(SchedulableTask task, Executor executor) {
+ while (true) {
+ int s = state.get();
+ if (s == END) {
+ if (state.compareAndSet(END, BEGIN)) {
+ break;
+ }
+ } else if ((s & BEGIN) != 0) {
+ // Tries to change the state to AGAIN, preserving OFFLOAD bit
+ if (state.compareAndSet(s, AGAIN | (s & OFFLOAD))) {
+ return;
+ }
+ } else if ((s & AGAIN) != 0 || s == STOP) {
+ /* In the case of AGAIN the scheduler does not provide
+ happens-before relationship between actions prior to
+ runOrSchedule() and actions that happen in task.run().
+ The reason is that no volatile write is done in this case,
+ and the call piggybacks on the call that has actually set
+ AGAIN state. */
+ return;
+ } else {
+ // Non-existent state, or the one that cannot be offloaded
+ throw new InternalError(String.valueOf(s));
+ }
+ }
+ if (executor == null) {
+ task.run();
+ } else {
+ executor.execute(task);
+ }
+ }
+
+ /** The only concrete {@code DeferredCompleter} implementation. */
+ private class TryEndDeferredCompleter extends DeferredCompleter {
+
+ @Override
+ public void complete() {
+ while (true) {
+ int s;
+ while (((s = state.get()) & OFFLOAD) != 0) {
+ // Tries to offload ending of the task to the parent
+ if (state.compareAndSet(s, s & ~OFFLOAD)) {
+ return;
+ }
+ }
+ while (true) {
+ if ((s & OFFLOAD) != 0) {
+ /* OFFLOAD bit can never be observed here. Otherwise
+ it would mean there is another invocation of
+ "complete" that can run the task. */
+ throw new InternalError(String.valueOf(s));
+ }
+ if (s == BEGIN) {
+ if (state.compareAndSet(BEGIN, END)) {
+ return;
+ }
+ } else if (s == AGAIN) {
+ if (state.compareAndSet(AGAIN, BEGIN | OFFLOAD)) {
+ break;
+ }
+ } else if (s == STOP) {
+ return;
+ } else if (s == END) {
+ throw new IllegalStateException("Duplicate completion");
+ } else {
+ // Non-existent state
+ throw new InternalError(String.valueOf(s));
+ }
+ s = state.get();
+ }
+ restartableTask.run(completer);
+ }
+ }
+ }
+
+ /**
+ * Tells whether, or not, this scheduler has been permanently stopped.
+ *
+ * <p> Should be used from inside the task to poll the status of the
+ * scheduler, pretty much the same way as it is done for threads:
+ * <pre>{@code
+ * if (!Thread.currentThread().isInterrupted()) {
+ * ...
+ * }
+ * }</pre>
+ */
+ public boolean isStopped() {
+ return state.get() == STOP;
+ }
+
+ /**
+ * Stops this scheduler. Subsequent invocations of {@code runOrSchedule}
+ * are effectively no-ops.
+ *
+ * <p> If the task has already begun, this invocation will not affect it,
+ * unless the task itself uses {@code isStopped()} method to check the state
+ * of the handler.
+ */
+ public void stop() {
+ state.set(STOP);
+ }
+
+ /**
+ * Returns a new {@code SequentialScheduler} that executes the provided
+ * {@code mainLoop} from within a {@link SynchronizedRestartableTask}.
+ *
+ * @apiNote This is equivalent to calling
+ * {@code new SequentialScheduler(new SynchronizedRestartableTask(mainLoop))}
+ * The main loop must not perform any blocking operation.
+ *
+ * @param mainLoop The main loop of the new sequential scheduler
+ * @return a new {@code SequentialScheduler} that executes the provided
+ * {@code mainLoop} from within a {@link SynchronizedRestartableTask}.
+ */
+ public static SequentialScheduler synchronizedScheduler(Runnable mainLoop) {
+ return new SequentialScheduler(new SynchronizedRestartableTask(mainLoop));
+ }
+}
--- /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.
+ * <p>
+ * Processing is done by implementing the {@link #incoming(List, boolean)} method
+ * which supplies buffers from upstream. This method (or any other method)
+ * can then call the outgoing() method to deliver processed buffers downstream.
+ * <p>
+ * Upstream error signals are delivered downstream directly. Cancellation from
+ * downstream is also propagated upstream immediately.
+ * <p>
+ * Each SubscriberWrapper has a {@link java.util.concurrent.CompletableFuture}{@code <Void>}
+ * which propagates completion/errors from downstream to upstream. Normal completion
+ * can only occur after onComplete() is called, but errors can be propagated upwards
+ * at any time.
+ */
+public abstract class SubscriberWrapper
+ implements FlowTube.TubeSubscriber, Closeable, Flow.Processor<List<ByteBuffer>,List<ByteBuffer>>
+ // TODO: SSLTube Subscriber will never change? Does this really need to be a TS?
+{
+ 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<? super List<ByteBuffer>> downstreamSubscriber;
+ // processed byte to send to the downstream subscriber.
+ private final ConcurrentLinkedQueue<List<ByteBuffer>> outputQ;
+ private final CompletableFuture<Void> cf;
+ private final SequentialScheduler pushScheduler;
+ private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
+ final AtomicLong upstreamWindow = new AtomicLong(0);
+
+ /**
+ * Wraps the given downstream subscriber. For each call to {@link
+ * #onNext(List<ByteBuffer>) } the given filter function is invoked
+ * and the list (if not empty) returned is passed downstream.
+ *
+ * A {@code CompletableFuture} is supplied which can be used to signal an
+ * error from downstream and which terminates the wrapper or which signals
+ * completion of downstream activity which can be propagated upstream. Error
+ * completion can be signaled at any time, but normal completion must not be
+ * signaled before onComplete() is called.
+ */
+ public SubscriberWrapper()
+ {
+ this.outputQ = new ConcurrentLinkedQueue<>();
+ this.cf = new MinimalFuture<>();
+ this.pushScheduler =
+ SequentialScheduler.synchronizedScheduler(new DownstreamPusher());
+ this.downstreamSubscription = new SubscriptionBase(pushScheduler,
+ this::downstreamCompletion);
+ }
+
+ @Override
+ public final void subscribe(Subscriber<? super List<ByteBuffer>> downstreamSubscriber) {
+ Objects.requireNonNull(downstreamSubscriber);
+ this.downstreamSubscriber = downstreamSubscriber;
+ }
+
+ /**
+ * Wraps the given downstream wrapper in this. For each call to
+ * {@link #onNext(List<ByteBuffer>) } the incoming() method is called.
+ *
+ * The {@code downstreamCF} from the downstream wrapper is linked to this
+ * wrappers notifier.
+ *
+ * @param downstreamWrapper downstream destination
+ */
+ public SubscriberWrapper(Subscriber<? super List<ByteBuffer>> downstreamWrapper)
+ {
+ this();
+ subscribe(downstreamWrapper);
+ }
+
+ /**
+ * Delivers data to be processed by this wrapper. Generated data to be sent
+ * downstream, must be provided to the {@link #outgoing(List, boolean)}}
+ * method.
+ *
+ * @param buffers a List of ByteBuffers.
+ * @param complete if true then no more data will be added to the list
+ */
+ protected abstract void incoming(List<ByteBuffer> buffers, boolean complete);
+
+ /**
+ * This method is called to determine the window size to use at any time. The
+ * current window is supplied together with the current downstream queue size.
+ * {@code 0} should be returned if no change is
+ * required or a positive integer which will be added to the current window.
+ * The default implementation maintains a downstream queue size of no greater
+ * than 5. The method can be overridden if required.
+ *
+ * @param currentWindow the current upstream subscription window
+ * @param downstreamQsize the current number of buffers waiting to be sent
+ * downstream
+ *
+ * @return value to add to currentWindow
+ */
+ protected long upstreamWindowUpdate(long currentWindow, long downstreamQsize) {
+ if (downstreamQsize > 5) {
+ return 0;
+ }
+
+ if (currentWindow == 0) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Override this if anything needs to be done after the upstream subscriber
+ * has subscribed
+ */
+ protected void onSubscribe() {
+ }
+
+ /**
+ * Override this if anything needs to be done before checking for error
+ * and processing the input queue.
+ * @return
+ */
+ protected SchedulingAction enterScheduling() {
+ return SchedulingAction.CONTINUE;
+ }
+
+ protected boolean signalScheduling() {
+ if (downstreamCompleted || pushScheduler.isStopped()) {
+ return false;
+ }
+ pushScheduler.runOrSchedule();
+ return true;
+ }
+
+ /**
+ * Delivers buffers of data downstream. After incoming()
+ * has been called complete == true signifying completion of the upstream
+ * subscription, data may continue to be delivered, up to when outgoing() is
+ * called complete == true, after which, the downstream subscription is
+ * completed.
+ *
+ * It's an error to call outgoing() with complete = true if incoming() has
+ * not previously been called with it.
+ */
+ public void outgoing(ByteBuffer buffer, boolean complete) {
+ Objects.requireNonNull(buffer);
+ assert !complete || !buffer.hasRemaining();
+ outgoing(List.of(buffer), complete);
+ }
+
+ /**
+ * Sometime it might be necessary to complete the downstream subscriber
+ * before the upstream completes. For instance, when an SSL server
+ * sends a notify_close. In that case we should let the outgoing
+ * complete before upstream us completed.
+ * @return true, may be overridden by subclasses.
+ */
+ public boolean closing() {
+ return false;
+ }
+
+ public void outgoing(List<ByteBuffer> buffers, boolean complete) {
+ Objects.requireNonNull(buffers);
+ if (complete) {
+ assert Utils.remaining(buffers) == 0;
+ boolean closing = closing();
+ 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<Void> completion() {
+ return cf;
+ }
+
+ /**
+ * Invoked whenever it 'may' be possible to push buffers downstream.
+ */
+ class DownstreamPusher implements Runnable {
+ @Override
+ public void run() {
+ try {
+ run1();
+ } catch (Throwable t) {
+ errorCommon(t);
+ }
+ }
+
+ private void run1() {
+ if (downstreamCompleted) {
+ 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<ByteBuffer> 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<ByteBuffer> 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<ByteBuffer> 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";
+ }
+}
--- /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();
+ }
+}
--- /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<String> action = () -> System.getProperty("test.src");
+// TESTING = AccessController.doPrivileged(action) != null;
+// } else TESTING = false;
+// }
+ public static final boolean DEBUG = // Revisit: temporary dev flag.
+ getBooleanProperty(DebugLogger.HTTP_NAME, false);
+ public static final boolean DEBUG_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<String> DISALLOWED_HEADERS_SET;
+ static {
+ // A case insensitive TreeSet of strings.
+ TreeSet<String> treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+ treeSet.addAll(Set.of("connection", "content-length",
+ "date", "expect", "from", "host", "origin",
+ "referer", "upgrade",
+ "via", "warning"));
+ DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet);
+ }
+
+ public static final Predicate<String>
+ ALLOWED_HEADERS = header -> !DISALLOWED_HEADERS_SET.contains(header);
+
+ private static final Predicate<String> IS_PROXY_HEADER = (k) ->
+ k != null && k.length() > 6 && "proxy-".equalsIgnoreCase(k.substring(0,6));
+ private static final Predicate<String> NO_PROXY_HEADER =
+ IS_PROXY_HEADER.negate();
+ private static final Predicate<String> ALL_HEADERS = (s) -> true;
+
+ private static final Set<String> PROXY_AUTH_DISABLED_SCHEMES;
+ private static final Set<String> PROXY_AUTH_TUNNEL_DISABLED_SCHEMES;
+ static {
+ String proxyAuthDisabled =
+ getNetProperty("jdk.http.auth.proxying.disabledSchemes");
+ String proxyAuthTunnelDisabled =
+ getNetProperty("jdk.http.auth.tunneling.disabledSchemes");
+ PROXY_AUTH_DISABLED_SCHEMES =
+ proxyAuthDisabled == null ? Set.of() :
+ Stream.of(proxyAuthDisabled.split(","))
+ .map(String::trim)
+ .filter((s) -> !s.isEmpty())
+ .collect(Collectors.toUnmodifiableSet());
+ PROXY_AUTH_TUNNEL_DISABLED_SCHEMES =
+ proxyAuthTunnelDisabled == null ? Set.of() :
+ Stream.of(proxyAuthTunnelDisabled.split(","))
+ .map(String::trim)
+ .filter((s) -> !s.isEmpty())
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ private static final String WSPACES = " \t\r\n";
+ private static final boolean isAllowedForProxy(String name,
+ List<String> value,
+ Set<String> disabledSchemes,
+ Predicate<String> allowedKeys) {
+ if (!allowedKeys.test(name)) return false;
+ if (disabledSchemes.isEmpty()) return true;
+ if (name.equalsIgnoreCase("proxy-authorization")) {
+ if (value.isEmpty()) return false;
+ for (String scheme : disabledSchemes) {
+ int slen = scheme.length();
+ for (String v : value) {
+ int vlen = v.length();
+ if (vlen == slen) {
+ if (v.equalsIgnoreCase(scheme)) {
+ return false;
+ }
+ } else if (vlen > slen) {
+ if (v.substring(0,slen).equalsIgnoreCase(scheme)) {
+ int c = v.codePointAt(slen);
+ if (WSPACES.indexOf(c) > -1
+ || Character.isSpaceChar(c)
+ || Character.isWhitespace(c)) {
+ return false;
+ }
+ }
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ public static final BiPredicate<String, List<String>> PROXY_TUNNEL_FILTER =
+ (s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_TUNNEL_DISABLED_SCHEMES,
+ IS_PROXY_HEADER);
+ public static final BiPredicate<String, List<String>> PROXY_FILTER =
+ (s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_DISABLED_SCHEMES,
+ ALL_HEADERS);
+ public static final BiPredicate<String, List<String>> NO_PROXY_HEADERS_FILTER =
+ (n,v) -> Utils.NO_PROXY_HEADER.test(n);
+
+
+ public static boolean proxyHasDisabledSchemes(boolean tunnel) {
+ return tunnel ? ! PROXY_AUTH_TUNNEL_DISABLED_SCHEMES.isEmpty()
+ : ! PROXY_AUTH_DISABLED_SCHEMES.isEmpty();
+ }
+
+ public static ByteBuffer getBuffer() {
+ return ByteBuffer.allocate(BUFSIZE);
+ }
+
+ public static Throwable getCompletionCause(Throwable x) {
+ if (!(x instanceof CompletionException)
+ && !(x instanceof ExecutionException)) return x;
+ final Throwable cause = x.getCause();
+ if (cause == null) {
+ throw new InternalError("Unexpected null cause", x);
+ }
+ return cause;
+ }
+
+ public static IOException getIOException(Throwable t) {
+ if (t instanceof IOException) {
+ return (IOException) t;
+ }
+ Throwable cause = t.getCause();
+ if (cause != null) {
+ return getIOException(cause);
+ }
+ return new IOException(t);
+ }
+
+ private Utils() { }
+
+ /**
+ * Returns the security permissions required to connect to the proxy, or
+ * {@code null} if none is required or applicable.
+ */
+ public static URLPermission permissionForProxy(InetSocketAddress proxyAddress) {
+ if (proxyAddress == null)
+ return null;
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("socket://")
+ .append(proxyAddress.getHostString()).append(":")
+ .append(proxyAddress.getPort());
+ String urlString = sb.toString();
+ return new URLPermission(urlString, "CONNECT");
+ }
+
+ /**
+ * Returns the security permission required for the given details.
+ */
+ public static URLPermission permissionForServer(URI uri,
+ String method,
+ Stream<String> headers) {
+ String urlString = new StringBuilder()
+ .append(uri.getScheme()).append("://")
+ .append(uri.getAuthority())
+ .append(uri.getPath()).toString();
+
+ StringBuilder actionStringBuilder = new StringBuilder(method);
+ String collected = headers.collect(joining(","));
+ if (!collected.isEmpty()) {
+ actionStringBuilder.append(":").append(collected);
+ }
+ return new URLPermission(urlString, actionStringBuilder.toString());
+ }
+
+
+ // ABNF primitives defined in RFC 7230
+ private static final boolean[] tchar = new boolean[256];
+ private static final boolean[] fieldvchar = new boolean[256];
+
+ static {
+ char[] allowedTokenChars =
+ ("!#$%&'*+-.^_`|~0123456789" +
+ "abcdefghijklmnopqrstuvwxyz" +
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray();
+ for (char c : allowedTokenChars) {
+ tchar[c] = true;
+ }
+ for (char c = 0x21; c < 0xFF; c++) {
+ fieldvchar[c] = true;
+ }
+ fieldvchar[0x7F] = false; // a little hole (DEL) in the range
+ }
+
+ /*
+ * Validates a RFC 7230 field-name.
+ */
+ public static boolean isValidName(String token) {
+ for (int i = 0; i < token.length(); i++) {
+ char c = token.charAt(i);
+ if (c > 255 || !tchar[c]) {
+ return false;
+ }
+ }
+ return !token.isEmpty();
+ }
+
+ /**
+ * 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<Integer>) () ->
+ NetProperties.getInteger(name, defaultValue));
+ }
+
+ static String getNetProperty(String name) {
+ return AccessController.doPrivileged((PrivilegedAction<String>) () ->
+ NetProperties.get(name));
+ }
+
+ static boolean getBooleanProperty(String name, boolean def) {
+ return AccessController.doPrivileged((PrivilegedAction<Boolean>) () ->
+ 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<ByteBuffer> currentList,
+ List<ByteBuffer> buffersToAdd) {
+ long accumulatedBytes = 0;
+ for (ByteBuffer bufferToAdd : buffersToAdd) {
+ int remaining = bufferToAdd.remaining();
+ if (remaining <= 0)
+ continue;
+ int listSize = currentList.size();
+ if (listSize == 0) {
+ currentList.add(bufferToAdd);
+ accumulatedBytes = remaining;
+ continue;
+ }
+
+ ByteBuffer lastBuffer = currentList.get(listSize - 1);
+ int freeSpace = lastBuffer.capacity() - lastBuffer.limit();
+ if (remaining <= COPY_THRESHOLD && freeSpace >= remaining) {
+ // append the new data to the unused space in the last buffer
+ int position = lastBuffer.position();
+ int limit = lastBuffer.limit();
+ lastBuffer.position(limit);
+ lastBuffer.limit(limit + remaining);
+ lastBuffer.put(bufferToAdd);
+ lastBuffer.position(position);
+ } else {
+ currentList.add(bufferToAdd);
+ }
+ accumulatedBytes += remaining;
+ }
+ return accumulatedBytes;
+ }
+
+ public static ByteBuffer copy(ByteBuffer src) {
+ ByteBuffer dst = ByteBuffer.allocate(src.remaining());
+ dst.put(src);
+ dst.flip();
+ return dst;
+ }
+
+ public static String dump(Object... objects) {
+ return Arrays.toString(objects);
+ }
+
+ public static String stringOf(Collection<?> source) {
+ // We don't know anything about toString implementation of this
+ // collection, so let's create an array
+ return Arrays.toString(source.toArray());
+ }
+
+ public static long remaining(ByteBuffer[] bufs) {
+ long remain = 0;
+ for (ByteBuffer buf : bufs) {
+ remain += buf.remaining();
+ }
+ return remain;
+ }
+
+ public static boolean hasRemaining(List<ByteBuffer> bufs) {
+ synchronized (bufs) {
+ for (ByteBuffer buf : bufs) {
+ if (buf.hasRemaining())
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static long remaining(List<ByteBuffer> bufs) {
+ long remain = 0;
+ synchronized (bufs) {
+ for (ByteBuffer buf : bufs) {
+ remain += buf.remaining();
+ }
+ }
+ return remain;
+ }
+
+ public static int remaining(List<ByteBuffer> bufs, int max) {
+ long remain = 0;
+ synchronized (bufs) {
+ for (ByteBuffer buf : bufs) {
+ remain += buf.remaining();
+ if (remain > max) {
+ throw new IllegalArgumentException("too many bytes");
+ }
+ }
+ }
+ return (int) remain;
+ }
+
+ public static long remaining(ByteBufferReference[] refs) {
+ long remain = 0;
+ for (ByteBufferReference ref : refs) {
+ remain += ref.get().remaining();
+ }
+ return remain;
+ }
+
+ public static int remaining(ByteBufferReference[] refs, int max) {
+ long remain = 0;
+ for (ByteBufferReference ref : refs) {
+ remain += ref.get().remaining();
+ if (remain > max) {
+ throw new IllegalArgumentException("too many bytes");
+ }
+ }
+ return (int) remain;
+ }
+
+ public static int remaining(ByteBuffer[] refs, int max) {
+ long remain = 0;
+ for (ByteBuffer b : refs) {
+ remain += b.remaining();
+ if (remain > max) {
+ throw new IllegalArgumentException("too many bytes");
+ }
+ }
+ return (int) remain;
+ }
+
+ public static void close(Closeable... closeables) {
+ for (Closeable c : closeables) {
+ try {
+ c.close();
+ } catch (IOException ignored) { }
+ }
+ }
+
+ // Put all these static 'empty' singletons here
+ public static final ByteBuffer EMPTY_BYTEBUFFER = ByteBuffer.allocate(0);
+ public static final ByteBuffer[] EMPTY_BB_ARRAY = new ByteBuffer[0];
+ public static final List<ByteBuffer> EMPTY_BB_LIST = List.of();
+ public static final ByteBufferReference[] EMPTY_BBR_ARRAY = new ByteBufferReference[0];
+
+ /**
+ * Returns a slice of size {@code amount} from the given buffer. If the
+ * buffer contains more data than {@code amount}, then the slice's capacity
+ * ( and, but not just, its limit ) is set to {@code amount}. If the buffer
+ * does not contain more data than {@code amount}, then the slice's capacity
+ * will be the same as the given buffer's capacity.
+ */
+ public static ByteBuffer sliceWithLimitedCapacity(ByteBuffer buffer, int amount) {
+ final int index = buffer.position() + amount;
+ final int limit = buffer.limit();
+ if (index != limit) {
+ // additional data in the buffer
+ buffer.limit(index); // ensures that the slice does not go beyond
+ } else {
+ // no additional data in the buffer
+ buffer.limit(buffer.capacity()); // allows the slice full capacity
+ }
+
+ ByteBuffer newb = buffer.slice();
+ buffer.position(index);
+ buffer.limit(limit); // restore the original buffer's limit
+ newb.limit(amount); // slices limit to amount (capacity may be greater)
+ return newb;
+ }
+
+ /**
+ * Get the Charset from the Content-encoding header. Defaults to
+ * UTF_8
+ */
+ public static Charset charsetFrom(HttpHeaders headers) {
+ String type = headers.firstValue("Content-type")
+ .orElse("text/html; charset=utf-8");
+ int i = type.indexOf(";");
+ if (i >= 0) type = type.substring(i+1);
+ try {
+ HeaderParser parser = new HeaderParser(type);
+ String value = parser.findValue("charset");
+ if (value == null) return StandardCharsets.UTF_8;
+ return Charset.forName(value);
+ } catch (Throwable x) {
+ Log.logTrace("Can't find charset in \"{0}\" ({1})", type, x);
+ return StandardCharsets.UTF_8;
+ }
+ }
+
+ public static UncheckedIOException unchecked(IOException e) {
+ return new UncheckedIOException(e);
+ }
+
+ /**
+ * Get a logger for debug HTTP traces.
+ *
+ * The logger should only be used with levels whose severity is
+ * {@code <= DEBUG}. By default, this logger will forward all messages
+ * logged to an internal logger named "jdk.internal.httpclient.debug".
+ * In addition, if the property -Djdk.internal.httpclient.debug=true is set,
+ * it will print the messages on stderr.
+ * The logger will add some decoration to the printed message, in the form of
+ * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+ *
+ * @param dbgTag A lambda that returns a string that identifies the caller
+ * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
+ *
+ * @return A logger for HTTP internal debug traces
+ */
+ public static Logger getDebugLogger(Supplier<String> dbgTag) {
+ return getDebugLogger(dbgTag, DEBUG);
+ }
+
+ /**
+ * Get a logger for debug HTTP traces.The logger should only be used
+ * with levels whose severity is {@code <= DEBUG}.
+ *
+ * By default, this logger will forward all messages logged to an internal
+ * logger named "jdk.internal.httpclient.debug".
+ * In addition, if the message severity level is >= to
+ * the provided {@code errLevel} it will print the messages on stderr.
+ * The logger will add some decoration to the printed message, in the form of
+ * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+ *
+ * @apiNote To obtain a logger that will always print things on stderr in
+ * addition to forwarding to the internal logger, use
+ * {@code getDebugLogger(this::dbgTag, Level.ALL);}.
+ * This is also equivalent to calling
+ * {@code getDebugLogger(this::dbgTag, true);}.
+ * To obtain a logger that will only forward to the internal logger,
+ * use {@code getDebugLogger(this::dbgTag, Level.OFF);}.
+ * This is also equivalent to calling
+ * {@code getDebugLogger(this::dbgTag, false);}.
+ *
+ * @param dbgTag A lambda that returns a string that identifies the caller
+ * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
+ * @param errLevel The level above which messages will be also printed on
+ * stderr (in addition to be forwarded to the internal logger).
+ *
+ * @return A logger for HTTP internal debug traces
+ */
+ static Logger getDebugLogger(Supplier<String> dbgTag, Level errLevel) {
+ return DebugLogger.createHttpLogger(dbgTag, Level.OFF, errLevel);
+ }
+
+ /**
+ * Get a logger for debug HTTP traces.The logger should only be used
+ * with levels whose severity is {@code <= DEBUG}.
+ *
+ * By default, this logger will forward all messages logged to an internal
+ * logger named "jdk.internal.httpclient.debug".
+ * In addition, the provided boolean {@code on==true}, it will print the
+ * messages on stderr.
+ * The logger will add some decoration to the printed message, in the form of
+ * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+ *
+ * @apiNote To obtain a logger that will always print things on stderr in
+ * addition to forwarding to the internal logger, use
+ * {@code getDebugLogger(this::dbgTag, true);}.
+ * This is also equivalent to calling
+ * {@code getDebugLogger(this::dbgTag, Level.ALL);}.
+ * To obtain a logger that will only forward to the internal logger,
+ * use {@code getDebugLogger(this::dbgTag, false);}.
+ * This is also equivalent to calling
+ * {@code getDebugLogger(this::dbgTag, Level.OFF);}.
+ *
+ * @param dbgTag A lambda that returns a string that identifies the caller
+ * (e.g: "SocketTube(3)", or "Http2Connection(SocketTube(3))")
+ * @param on Whether messages should also be printed on
+ * stderr (in addition to be forwarded to the internal logger).
+ *
+ * @return A logger for HTTP internal debug traces
+ */
+ public static Logger getDebugLogger(Supplier<String> dbgTag, boolean on) {
+ Level errLevel = on ? Level.ALL : Level.OFF;
+ return getDebugLogger(dbgTag, errLevel);
+ }
+
+ /**
+ * Get a logger for debug HPACK traces.The logger should only be used
+ * with levels whose severity is {@code <= DEBUG}.
+ *
+ * By default, this logger will forward all messages logged to an internal
+ * logger named "jdk.internal.httpclient.hpack.debug".
+ * In addition, if the message severity level is >= to
+ * the provided {@code outLevel} it will print the messages on stdout.
+ * The logger will add some decoration to the printed message, in the form of
+ * {@code <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+ *
+ * @apiNote To obtain a logger that will always print things on 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<String> 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 <Level>:[<thread-name>] [<elapsed-time>] <dbgTag>: <formatted message>}
+ *
+ * @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<String> dbgTag, boolean on) {
+ Level outLevel = on ? Level.ALL : Level.OFF;
+ return getHpackLogger(dbgTag, outLevel);
+ }
+}
--- /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<ByteBuffer> headerBlocks) {
+ super(streamid, flags, headerBlocks);
+ }
+
+ public ContinuationFrame(int streamid, ByteBuffer headersBlock) {
+ this(streamid, 0, List.of(headersBlock));
+ }
+
+ @Override
+ public int type() {
+ return TYPE;
+ }
+
+ @Override
+ int length() {
+ return headerLength;
+ }
+
+}
--- /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<ByteBuffer> data;
+ private final int dataLength;
+
+ public DataFrame(int streamid, int flags, ByteBuffer data) {
+ this(streamid, flags, List.of(data));
+ }
+
+ public DataFrame(int streamid, int flags, List<ByteBuffer> data) {
+ super(streamid, flags);
+ this.data = data;
+ this.dataLength = Utils.remaining(data, Integer.MAX_VALUE);
+ }
+
+ public DataFrame(int streamid, int flags, List<ByteBuffer> data, int padLength) {
+ this(streamid, flags, data);
+ if (padLength > 0) {
+ setPadLength(padLength);
+ }
+ }
+
+ @Override
+ public int type() {
+ return TYPE;
+ }
+
+ @Override
+ int length() {
+ return dataLength + (((flags & PADDED) != 0) ? (padLength + 1) : 0);
+ }
+
+ @Override
+ public String flagAsString(int flag) {
+ switch (flag) {
+ case END_STREAM:
+ return "END_STREAM";
+ case PADDED:
+ return "PADDED";
+ }
+ return super.flagAsString(flag);
+ }
+
+ public List<ByteBuffer> getData() {
+ return data;
+ }
+
+ public int getDataLength() {
+ return dataLength;
+ }
+
+ int getPadLength() {
+ return padLength;
+ }
+
+ public void setPadLength(int padLength) {
+ this.padLength = padLength;
+ flags |= PADDED;
+ }
+
+ public int payloadLength() {
+ // RFC 7540 6.1:
+ // The entire DATA frame payload is included in flow control,
+ // including the Pad Length and Padding fields if present
+ if ((flags & PADDED) != 0) {
+ return dataLength + (1 + padLength);
+ } else {
+ return dataLength;
+ }
+ }
+
+
+}
--- /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;
+ }
+}
--- /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
+ * <p>
+ * collect buffers until frame decoding is possible,
+ * all decoded frames are passed to the FrameProcessor callback in order of decoding.
+ *
+ * It's a stateful class due to the fact that FramesDecoder stores buffers inside.
+ * Should be allocated only the single instance per connection.
+ */
+public class FramesDecoder {
+
+ static final 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<ByteBuffer> tailBuffers = new ArrayDeque<>();
+ private int tailSize = 0;
+
+ private boolean slicedToDataFrame = false;
+
+ private final List<ByteBuffer> prepareToRelease = new ArrayList<>();
+
+ // if true - Frame Header was parsed (9 bytes consumed) and subsequent fields have meaning
+ // otherwise - stopped at frames boundary
+ private boolean frameHeaderParsed = false;
+ private int frameLength;
+ private int frameType;
+ private int frameFlags;
+ private int frameStreamid;
+ private boolean closed;
+
+ /**
+ * Creates Frame Decoder
+ *
+ * @param frameProcessor - callback for decoded frames
+ */
+ public FramesDecoder(FrameProcessor frameProcessor) {
+ this(frameProcessor, 16 * 1024);
+ }
+
+ /**
+ * Creates Frame Decoder
+ * @param frameProcessor - callback for decoded frames
+ * @param maxFrameSize - maxFrameSize accepted by this decoder
+ */
+ public FramesDecoder(FrameProcessor frameProcessor, int maxFrameSize) {
+ this.frameProcessor = frameProcessor;
+ this.maxFrameSize = Math.min(Math.max(16 * 1024, maxFrameSize), 16 * 1024 * 1024 - 1);
+ }
+
+ /** Threshold beyond which data is no longer copied into the current buffer,
+ * if that buffer has enough unused space. */
+ private static final int COPY_THRESHOLD = 8192;
+
+ /**
+ * Adds the data from the given buffer, and performs frame decoding if
+ * possible. Either 1) appends the data from the given buffer to the
+ * current buffer ( if there is enough unused space ), or 2) adds it to the
+ * next buffer in the queue.
+ *
+ * If there is enough data to perform frame decoding then, all buffers are
+ * decoded and the FrameProcessor is invoked.
+ */
+ public void decode(ByteBuffer inBoundBuffer) throws IOException {
+ if (closed) {
+ 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<ByteBuffer> getBuffers(boolean isDataFrame, int bytecount) {
+ List<ByteBuffer> res = new ArrayList<>();
+ while (bytecount > 0) {
+ int remaining = currentBuffer.remaining();
+ int extract = Math.min(remaining, bytecount);
+ ByteBuffer extractedBuf;
+ if (isDataFrame) {
+ extractedBuf = Utils.sliceWithLimitedCapacity(currentBuffer, extract)
+ .asReadOnlyBuffer();
+ slicedToDataFrame = true;
+ } else {
+ // Header frames here
+ // HPACK decoding should performed under lock and immediately after frame decoding.
+ // in that case it is safe to release original buffer,
+ // because of sliced buffer has a very short life
+ extractedBuf = Utils.sliceWithLimitedCapacity(currentBuffer, extract);
+ }
+ res.add(extractedBuf);
+ bytecount -= extract;
+ nextBuffer();
+ }
+ return res;
+ }
+
+ public void close(String msg) {
+ closed = true;
+ tailBuffers.clear();
+ int bytes = tailSize;
+ ByteBuffer b = currentBuffer;
+ if (b != null) {
+ bytes += b.remaining();
+ b.position(b.limit());
+ }
+ tailSize = 0;
+ currentBuffer = null;
+ 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<n; i++) {
+ int id = getShort();
+ int val = getInt();
+ if (id > 0 && id <= SettingsFrame.MAX_PARAM) {
+ // a known parameter. Ignore otherwise
+ sf.setParameter(id, val); // TODO parameters validation
+ }
+ }
+ return sf;
+ }
+
+ private Http2Frame parsePushPromiseFrame(int frameLength, int streamid, int flags) {
+ // non-zero stream
+ if (streamid == 0) {
+ return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
+ "zero streamId for PushPromiseFrame");
+ }
+ int padLength = 0;
+ if ((flags & PushPromiseFrame.PADDED) != 0) {
+ padLength = getByte();
+ frameLength--;
+ }
+ int promisedStream = getInt() & 0x7fffffff;
+ frameLength -= 4;
+ if(frameLength < padLength) {
+ return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
+ "Padding exceeds the size remaining for the PushPromiseFrame");
+ }
+ PushPromiseFrame ppf = new PushPromiseFrame(streamid, flags, promisedStream,
+ getBuffers(false, frameLength - padLength), padLength);
+ skipBytes(padLength);
+ return ppf;
+ }
+
+ private Http2Frame parsePingFrame(int frameLength, int streamid, int flags) {
+ // only zero stream
+ if (streamid != 0) {
+ return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
+ "non-zero streamId for PingFrame");
+ }
+ if(frameLength != 8) {
+ return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
+ "PingFrame length is "+ frameLength+", expected 8");
+ }
+ return new PingFrame(flags, getBytes(8));
+ }
+
+ private Http2Frame parseGoAwayFrame(int frameLength, int streamid, int flags) {
+ // only zero stream; no flags
+ if (streamid != 0) {
+ return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
+ "non-zero streamId for GoAwayFrame");
+ }
+ if (frameLength < 8) {
+ return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
+ "Invalid GoAway frame size");
+ }
+ int lastStream = getInt() & 0x7fffffff;
+ int errorCode = getInt();
+ byte[] debugData = getBytes(frameLength - 8);
+ if (debugData.length > 0) {
+ Log.logError("GoAway debugData " + new String(debugData, UTF_8));
+ }
+ return new GoAwayFrame(lastStream, errorCode, debugData);
+ }
+
+ private Http2Frame parseWindowUpdateFrame(int frameLength, int streamid, int flags) {
+ // any stream; no flags
+ if(frameLength != 4) {
+ return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR,
+ "WindowUpdateFrame length is "+ frameLength+", expected 4");
+ }
+ return new WindowUpdateFrame(streamid, getInt() & 0x7fffffff);
+ }
+
+ private Http2Frame parseContinuationFrame(int frameLength, int streamid, int flags) {
+ // non-zero stream;
+ if (streamid == 0) {
+ return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR,
+ "zero streamId for ContinuationFrame");
+ }
+ return new ContinuationFrame(streamid, flags, getBuffers(false, frameLength));
+ }
+
+}
--- /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<ByteBuffer> encodeFrames(List<HeaderFrame> frames) {
+ List<ByteBuffer> bufs = new ArrayList<>(frames.size() * 2);
+ for (HeaderFrame f : frames) {
+ bufs.addAll(encodeFrame(f));
+ }
+ return bufs;
+ }
+
+ public ByteBuffer encodeConnectionPreface(byte[] preface, SettingsFrame frame) {
+ final int length = frame.length();
+ ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length + preface.length);
+ buf.put(preface);
+ putSettingsFrame(buf, frame, length);
+ buf.flip();
+ return buf;
+ }
+
+ public List<ByteBuffer> encodeFrame(Http2Frame frame) {
+ switch (frame.type()) {
+ case DataFrame.TYPE:
+ return encodeDataFrame((DataFrame) frame);
+ case HeadersFrame.TYPE:
+ return encodeHeadersFrame((HeadersFrame) frame);
+ case PriorityFrame.TYPE:
+ return encodePriorityFrame((PriorityFrame) frame);
+ case ResetFrame.TYPE:
+ return encodeResetFrame((ResetFrame) frame);
+ case SettingsFrame.TYPE:
+ return encodeSettingsFrame((SettingsFrame) frame);
+ case PushPromiseFrame.TYPE:
+ return encodePushPromiseFrame((PushPromiseFrame) frame);
+ case PingFrame.TYPE:
+ return encodePingFrame((PingFrame) frame);
+ case GoAwayFrame.TYPE:
+ return encodeGoAwayFrame((GoAwayFrame) frame);
+ case WindowUpdateFrame.TYPE:
+ return encodeWindowUpdateFrame((WindowUpdateFrame) frame);
+ case ContinuationFrame.TYPE:
+ return encodeContinuationFrame((ContinuationFrame) frame);
+ default:
+ throw new UnsupportedOperationException("Not supported frame "+frame.type()+" ("+frame.getClass().getName()+")");
+ }
+ }
+
+ private static final int NO_FLAGS = 0;
+ private static final int ZERO_STREAM = 0;
+
+ private List<ByteBuffer> encodeDataFrame(DataFrame frame) {
+ // non-zero stream
+ assert frame.streamid() != 0;
+ ByteBuffer buf = encodeDataFrameStart(frame);
+ if (frame.getFlag(DataFrame.PADDED)) {
+ return joinWithPadding(buf, frame.getData(), frame.getPadLength());
+ } else {
+ return join(buf, frame.getData());
+ }
+ }
+
+ private ByteBuffer encodeDataFrameStart(DataFrame frame) {
+ boolean isPadded = frame.getFlag(DataFrame.PADDED);
+ final int length = frame.getDataLength() + (isPadded ? (frame.getPadLength() + 1) : 0);
+ ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 1 : 0));
+ putHeader(buf, length, DataFrame.TYPE, frame.getFlags(), frame.streamid());
+ if (isPadded) {
+ buf.put((byte) frame.getPadLength());
+ }
+ buf.flip();
+ return buf;
+ }
+
+ private List<ByteBuffer> encodeHeadersFrame(HeadersFrame frame) {
+ // non-zero stream
+ assert frame.streamid() != 0;
+ ByteBuffer buf = encodeHeadersFrameStart(frame);
+ if (frame.getFlag(HeadersFrame.PADDED)) {
+ return joinWithPadding(buf, frame.getHeaderBlock(), frame.getPadLength());
+ } else {
+ return join(buf, frame.getHeaderBlock());
+ }
+ }
+
+ private ByteBuffer encodeHeadersFrameStart(HeadersFrame frame) {
+ boolean isPadded = frame.getFlag(HeadersFrame.PADDED);
+ boolean hasPriority = frame.getFlag(HeadersFrame.PRIORITY);
+ final int length = frame.getHeaderLength() + (isPadded ? (frame.getPadLength() + 1) : 0) + (hasPriority ? 5 : 0);
+ ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 1 : 0) + (hasPriority ? 5 : 0));
+ putHeader(buf, length, HeadersFrame.TYPE, frame.getFlags(), frame.streamid());
+ if (isPadded) {
+ buf.put((byte) frame.getPadLength());
+ }
+ if (hasPriority) {
+ putPriority(buf, frame.getExclusive(), frame.getStreamDependency(), frame.getWeight());
+ }
+ buf.flip();
+ return buf;
+ }
+
+ private List<ByteBuffer> encodePriorityFrame(PriorityFrame frame) {
+ // non-zero stream; no flags
+ assert frame.streamid() != 0;
+ final int length = 5;
+ ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
+ putHeader(buf, length, PriorityFrame.TYPE, NO_FLAGS, frame.streamid());
+ putPriority(buf, frame.exclusive(), frame.streamDependency(), frame.weight());
+ buf.flip();
+ return List.of(buf);
+ }
+
+ private List<ByteBuffer> encodeResetFrame(ResetFrame frame) {
+ // non-zero stream; no flags
+ assert frame.streamid() != 0;
+ final int length = 4;
+ ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
+ putHeader(buf, length, ResetFrame.TYPE, NO_FLAGS, frame.streamid());
+ buf.putInt(frame.getErrorCode());
+ buf.flip();
+ return List.of(buf);
+ }
+
+ private List<ByteBuffer> encodeSettingsFrame(SettingsFrame frame) {
+ // only zero stream
+ assert frame.streamid() == 0;
+ final int length = frame.length();
+ ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
+ putSettingsFrame(buf, frame, length);
+ buf.flip();
+ return List.of(buf);
+ }
+
+ private List<ByteBuffer> encodePushPromiseFrame(PushPromiseFrame frame) {
+ // non-zero stream
+ assert frame.streamid() != 0;
+ boolean isPadded = frame.getFlag(PushPromiseFrame.PADDED);
+ final int length = frame.getHeaderLength() + (isPadded ? 5 : 4);
+ ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + (isPadded ? 5 : 4));
+ putHeader(buf, length, PushPromiseFrame.TYPE, frame.getFlags(), frame.streamid());
+ if (isPadded) {
+ buf.put((byte) frame.getPadLength());
+ }
+ buf.putInt(frame.getPromisedStream());
+ buf.flip();
+
+ if (frame.getFlag(PushPromiseFrame.PADDED)) {
+ return joinWithPadding(buf, frame.getHeaderBlock(), frame.getPadLength());
+ } else {
+ return join(buf, frame.getHeaderBlock());
+ }
+ }
+
+ private List<ByteBuffer> encodePingFrame(PingFrame frame) {
+ // only zero stream
+ assert frame.streamid() == 0;
+ final int length = 8;
+ ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
+ putHeader(buf, length, PingFrame.TYPE, frame.getFlags(), ZERO_STREAM);
+ buf.put(frame.getData());
+ buf.flip();
+ return List.of(buf);
+ }
+
+ private List<ByteBuffer> encodeGoAwayFrame(GoAwayFrame frame) {
+ // only zero stream; no flags
+ assert frame.streamid() == 0;
+ byte[] debugData = frame.getDebugData();
+ final int length = 8 + debugData.length;
+ ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
+ putHeader(buf, length, GoAwayFrame.TYPE, NO_FLAGS, ZERO_STREAM);
+ buf.putInt(frame.getLastStream());
+ buf.putInt(frame.getErrorCode());
+ if (debugData.length > 0) {
+ buf.put(debugData);
+ }
+ buf.flip();
+ return List.of(buf);
+ }
+
+ private List<ByteBuffer> encodeWindowUpdateFrame(WindowUpdateFrame frame) {
+ // any stream; no flags
+ final int length = 4;
+ ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length);
+ putHeader(buf, length, WindowUpdateFrame.TYPE, NO_FLAGS, frame.streamid);
+ buf.putInt(frame.getUpdate());
+ buf.flip();
+ return List.of(buf);
+ }
+
+ private List<ByteBuffer> encodeContinuationFrame(ContinuationFrame frame) {
+ // non-zero stream;
+ assert frame.streamid() != 0;
+ final int length = frame.getHeaderLength();
+ ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE);
+ putHeader(buf, length, ContinuationFrame.TYPE, frame.getFlags(), frame.streamid());
+ buf.flip();
+ return join(buf, frame.getHeaderBlock());
+ }
+
+ private List<ByteBuffer> joinWithPadding(ByteBuffer buf, List<ByteBuffer> data, int padLength) {
+ int len = data.size();
+ if (len == 0) return List.of(buf, getPadding(padLength));
+ else if (len == 1) return List.of(buf, data.get(0), getPadding(padLength));
+ else if (len == 2) return List.of(buf, data.get(0), data.get(1), getPadding(padLength));
+ List<ByteBuffer> res = new ArrayList<>(len+2);
+ res.add(buf);
+ res.addAll(data);
+ res.add(getPadding(padLength));
+ return res;
+ }
+
+ private List<ByteBuffer> join(ByteBuffer buf, List<ByteBuffer> data) {
+ int len = data.size();
+ if (len == 0) return List.of(buf);
+ else if (len == 1) return List.of(buf, data.get(0));
+ else if (len == 2) return List.of(buf, data.get(0), data.get(1));
+ List<ByteBuffer> joined = new ArrayList<>(len + 1);
+ joined.add(buf);
+ joined.addAll(data);
+ return joined;
+ }
+
+ private void putSettingsFrame(ByteBuffer buf, SettingsFrame frame, int length) {
+ // only zero stream;
+ assert frame.streamid() == 0;
+ putHeader(buf, length, SettingsFrame.TYPE, frame.getFlags(), ZERO_STREAM);
+ frame.toByteBuffer(buf);
+ }
+
+ private void putHeader(ByteBuffer buf, int length, int type, int flags, int streamId) {
+ int x = (length << 8) + type;
+ buf.putInt(x);
+ buf.put((byte) flags);
+ buf.putInt(streamId);
+ }
+
+ private void putPriority(ByteBuffer buf, boolean exclusive, int streamDependency, int weight) {
+ buf.putInt(exclusive ? (1 << 31) + streamDependency : streamDependency);
+ buf.put((byte) weight);
+ }
+
+ private ByteBuffer getBuffer(int capacity) {
+ return ByteBuffer.allocate(capacity);
+ }
+
+ public ByteBuffer getPadding(int length) {
+ if (length > 255) {
+ throw new IllegalArgumentException("Padding too big");
+ }
+ return ByteBuffer.allocate(length); // zeroed!
+ }
+
+}
--- /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();
+ }
+
+}
--- /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<ByteBuffer> headerBlocks;
+
+ public static final int END_STREAM = 0x1;
+ public static final int END_HEADERS = 0x4;
+
+ public HeaderFrame(int streamid, int flags, List<ByteBuffer> headerBlocks) {
+ super(streamid, flags);
+ this.headerBlocks = headerBlocks;
+ this.headerLength = Utils.remaining(headerBlocks, Integer.MAX_VALUE);
+ }
+
+ @Override
+ public String flagAsString(int flag) {
+ switch (flag) {
+ case END_HEADERS:
+ return "END_HEADERS";
+ case END_STREAM:
+ return "END_STREAM";
+ }
+ return super.flagAsString(flag);
+ }
+
+
+ public List<ByteBuffer> getHeaderBlock() {
+ return headerBlocks;
+ }
+
+ int getHeaderLength() {
+ return headerLength;
+ }
+
+ /**
+ * Returns true if this block is the final block of headers.
+ */
+ public boolean endHeaders() {
+ return getFlag(END_HEADERS);
+ }
+}
--- /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<ByteBuffer> headerBlocks, int padLength) {
+ super(streamid, flags, headerBlocks);
+ if (padLength > 0) {
+ setPadLength(padLength);
+ }
+ }
+
+ public HeadersFrame(int streamid, int flags, List<ByteBuffer> headerBlocks) {
+ super(streamid, flags, headerBlocks);
+ }
+
+ public HeadersFrame(int streamid, int flags, ByteBuffer headerBlock) {
+ this(streamid, flags, List.of(headerBlock));
+ }
+
+ @Override
+ public int type() {
+ return TYPE;
+ }
+
+ @Override
+ int length() {
+ return headerLength
+ + ((flags & PADDED) != 0 ? (1 + padLength) : 0)
+ + ((flags & PRIORITY) != 0 ? 5 : 0);
+ }
+
+ @Override
+ public String flagAsString(int flag) {
+ switch (flag) {
+ case END_STREAM:
+ return "END_STREAM";
+ case PADDED:
+ return "PADDED";
+ case PRIORITY:
+ return "PRIORITY";
+ }
+ return super.flagAsString(flag);
+ }
+
+ public void setPadLength(int padLength) {
+ this.padLength = padLength;
+ flags |= PADDED;
+ }
+
+ int getPadLength() {
+ return padLength;
+ }
+
+ public void setPriority(int streamDependency, boolean exclusive, int weight) {
+ this.streamDependency = streamDependency;
+ this.exclusive = exclusive;
+ this.weight = weight;
+ this.flags |= PRIORITY;
+ }
+
+ public int getStreamDependency() {
+ return streamDependency;
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+
+ public boolean getExclusive() {
+ return exclusive;
+ }
+
+}
--- /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";
+ }
+
+}
--- /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;
+ }
+}
--- /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<T> extends Http2Frame {
+
+ int streamDependency;
+ int weight;
+ boolean exclusive;
+ T attachment;
+
+ public static final int PRIORITY = 0x20;
+
+ HttpHeaders user, system;
+
+ public OutgoingHeaders(HttpHeaders hdrs1, HttpHeaders hdrs2, T attachment) {
+ super(0, 0);
+ this.user = hdrs2;
+ this.system = hdrs1;
+ this.attachment = attachment;
+ }
+
+ public void setPriority(int streamDependency, boolean exclusive, int weight) {
+ this.streamDependency = streamDependency;
+ this.exclusive = exclusive;
+ this.weight = weight;
+ this.flags |= PRIORITY;
+ }
+
+ public int getStreamDependency() {
+ return streamDependency;
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+
+ public boolean getExclusive() {
+ return exclusive;
+ }
+
+ public T getAttachment() {
+ return attachment;
+ }
+
+ public HttpHeaders getUserHeaders() {
+ return user;
+ }
+
+ public HttpHeaders getSystemHeaders() {
+ return system;
+ }
+
+}
--- /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();
+ }
+
+}
--- /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;
+ }
+
+}
--- /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<ByteBuffer> buffers, int padLength) {
+ super(streamid, flags, buffers);
+ this.promisedStream = promisedStream;
+ if(padLength > 0 ) {
+ setPadLength(padLength);
+ }
+ }
+
+ @Override
+ public int type() {
+ return TYPE;
+ }
+
+ @Override
+ int length() {
+ return headerLength + ((flags & PADDED) != 0 ? 5 : 4);
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + " promisedStreamid: " + promisedStream
+ + " headerLength: " + headerLength;
+ }
+
+ @Override
+ public String flagAsString(int flag) {
+ switch (flag) {
+ case PADDED:
+ return "PADDED";
+ case END_HEADERS:
+ return "END_HEADERS";
+ }
+ return super.flagAsString(flag);
+ }
+
+ public void setPadLength(int padLength) {
+ this.padLength = padLength;
+ flags |= PADDED;
+ }
+
+ public int getPadLength() {
+ return padLength;
+ }
+
+ public int getPromisedStream() {
+ return promisedStream;
+ }
+
+ @Override
+ public boolean endHeaders() {
+ return getFlag(END_HEADERS);
+ }
+
+}
--- /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;
+ }
+
+}
--- /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;
+ }
+}
--- /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;
+ }
+
+}
--- /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();
+}
--- /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<Integer> maxSizes;
+ private boolean writing;
+ private boolean configured;
+
+ BulkSizeUpdateWriter maxHeaderTableSizes(Iterable<Integer> sizes) {
+ if (configured) {
+ throw new IllegalStateException("Already configured");
+ }
+ requireNonNull(sizes, "sizes");
+ maxSizes = sizes.iterator();
+ configured = true;
+ return this;
+ }
+
+ @Override
+ public boolean write(HeaderTable table, ByteBuffer destination) {
+ if (!configured) {
+ throw new IllegalStateException("Configure first");
+ }
+ while (true) {
+ if (writing) {
+ if (!writer.write(table, destination)) {
+ return false;
+ }
+ writing = false;
+ } else if (maxSizes.hasNext()) {
+ writing = true;
+ writer.reset();
+ writer.maxHeaderTableSize(maxSizes.next());
+ } else {
+ configured = false;
+ return true;
+ }
+ }
+ }
+
+ @Override
+ public BulkSizeUpdateWriter reset() {
+ maxSizes = null;
+ writing = false;
+ configured = false;
+ return this;
+ }
+}
--- /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.
+ *
+ * <p> Typical lifecycle looks like this:
+ *
+ * <p> {@link #Decoder(int) new Decoder}
+ * ({@link #setMaxCapacity(int) setMaxCapacity}?
+ * {@link #decode(ByteBuffer, boolean, DecodingCallback) decode})*
+ *
+ * @apiNote
+ *
+ * <p> The design intentions behind Decoder were to facilitate flexible and
+ * incremental style of processing.
+ *
+ * <p> {@code Decoder} does not require a complete header block in a single
+ * {@code ByteBuffer}. The header block can be spread across many buffers of any
+ * size and decoded one-by-one the way it makes most sense for the user. This
+ * way also allows not to limit the size of the header block.
+ *
+ * <p> Headers are delivered to the {@linkplain DecodingCallback callback} as
+ * soon as they become decoded. Using the callback also gives the user a freedom
+ * to decide how headers are processed. The callback does not limit the number
+ * of headers decoded during single decoding operation.
+ *
+ * @since 9
+ */
+public final class Decoder {
+
+ private final Logger logger;
+ private static final AtomicLong DECODERS_IDS = new AtomicLong();
+
+ @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.
+ *
+ * <p> The value has to be agreed between decoder and encoder out-of-band,
+ * e.g. by a protocol that uses HPACK
+ * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
+ *
+ * @param capacity
+ * a non-negative integer
+ *
+ * @throws IllegalArgumentException
+ * if capacity is negative
+ */
+ public Decoder(int capacity) {
+ id = DECODERS_IDS.incrementAndGet();
+ logger = HPACK.getLogger().subLogger("Decoder#" + id);
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("new decoder with maximum table size %s",
+ capacity));
+ }
+ if (logger.isLoggable(NORMAL)) {
+ /* To correlate with logging outside HPACK, knowing
+ hashCode/toString is important */
+ logger.log(NORMAL, () -> {
+ String hashCode = Integer.toHexString(
+ System.identityHashCode(this));
+ return format("toString='%s', identityHashCode=%s",
+ toString(), hashCode);
+ });
+ }
+ setMaxCapacity0(capacity);
+ table = new 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.
+ *
+ * <p> The value has to be agreed between decoder and encoder out-of-band,
+ * e.g. by a protocol that uses HPACK
+ * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
+ *
+ * @param capacity
+ * a non-negative integer
+ *
+ * @throws IllegalArgumentException
+ * if capacity is negative
+ */
+ public void setMaxCapacity(int capacity) {
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("setting maximum table size to %s",
+ capacity));
+ }
+ setMaxCapacity0(capacity);
+ }
+
+ private void setMaxCapacity0(int capacity) {
+ if (capacity < 0) {
+ throw new IllegalArgumentException("capacity >= 0: " + capacity);
+ }
+ // FIXME: await capacity update if less than what was prior to it
+ this.capacity = capacity;
+ }
+
+ /**
+ * Decodes a header block from the given buffer to the given callback.
+ *
+ * <p> Suppose a header block is represented by a sequence of
+ * {@code ByteBuffer}s in the form of {@code Iterator<ByteBuffer>}. And the
+ * consumer of decoded headers is represented by the callback. Then to
+ * decode the header block, the following approach might be used:
+ *
+ * <pre>{@code
+ * while (buffers.hasNext()) {
+ * ByteBuffer input = buffers.next();
+ * decoder.decode(input, callback, !buffers.hasNext());
+ * }
+ * }</pre>
+ *
+ * <p> The decoder reads as much as possible of the header block from the
+ * given buffer, starting at the buffer's position, and increments its
+ * position to reflect the bytes read. The buffer's mark and limit will not
+ * be modified.
+ *
+ * <p> Once the method is invoked with {@code endOfHeaderBlock == true}, the
+ * current header block is deemed ended, and inconsistencies, if any, are
+ * reported immediately by throwing an {@code IOException}.
+ *
+ * <p> Each callback method is called only after the implementation has
+ * processed the corresponding bytes. If the bytes revealed a decoding
+ * error, the callback method is not called.
+ *
+ * <p> In addition to exceptions thrown directly by the method, any
+ * exceptions thrown from the {@code callback} will bubble up.
+ *
+ * @apiNote The method asks for {@code endOfHeaderBlock} flag instead of
+ * returning it for two reasons. The first one is that the user of the
+ * decoder always knows which chunk is the last. The second one is to throw
+ * the most detailed exception possible, which might be useful for
+ * diagnosing issues.
+ *
+ * @implNote This implementation is not atomic in respect to decoding
+ * errors. In other words, if the decoding operation has thrown a decoding
+ * error, the decoder is no longer usable.
+ *
+ * @param headerBlock
+ * the chunk of the header block, may be empty
+ * @param endOfHeaderBlock
+ * true if the chunk is the final (or the only one) in the sequence
+ *
+ * @param consumer
+ * the callback
+ * @throws IOException
+ * in case of a decoding error
+ * @throws NullPointerException
+ * if either headerBlock or consumer are null
+ */
+ public void decode(ByteBuffer headerBlock,
+ boolean endOfHeaderBlock,
+ DecodingCallback consumer) throws IOException {
+ requireNonNull(headerBlock, "headerBlock");
+ requireNonNull(consumer, "consumer");
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("reading %s, end of header block? %s",
+ headerBlock, endOfHeaderBlock));
+ }
+ while (headerBlock.hasRemaining()) {
+ proceed(headerBlock, consumer);
+ }
+ if (endOfHeaderBlock && state != State.READY) {
+ logger.log(NORMAL, () -> format("unexpected end of %s representation",
+ state));
+ throw new IOException("Unexpected end of header block");
+ }
+ }
+
+ private void proceed(ByteBuffer input, DecodingCallback action)
+ throws IOException {
+ switch (state) {
+ case READY:
+ resumeReady(input);
+ break;
+ case INDEXED:
+ resumeIndexed(input, action);
+ break;
+ case LITERAL:
+ resumeLiteral(input, action);
+ break;
+ case LITERAL_WITH_INDEXING:
+ resumeLiteralWithIndexing(input, action);
+ break;
+ case LITERAL_NEVER_INDEXED:
+ resumeLiteralNeverIndexed(input, action);
+ break;
+ case SIZE_UPDATE:
+ resumeSizeUpdate(input, action);
+ break;
+ default:
+ throw new InternalError("Unexpected decoder state: " + state);
+ }
+ }
+
+ private void resumeReady(ByteBuffer input) {
+ int b = input.get(input.position()) & 0xff; // absolute read
+ State s = states[b];
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("next binary representation %s (first byte 0x%02x)",
+ s, b));
+ }
+ switch (s) {
+ case INDEXED:
+ integerReader.configure(7);
+ state = State.INDEXED;
+ firstValueIndex = true;
+ break;
+ case LITERAL:
+ state = State.LITERAL;
+ firstValueIndex = (b & 0b0000_1111) != 0;
+ if (firstValueIndex) {
+ integerReader.configure(4);
+ }
+ break;
+ case LITERAL_WITH_INDEXING:
+ state = State.LITERAL_WITH_INDEXING;
+ firstValueIndex = (b & 0b0011_1111) != 0;
+ if (firstValueIndex) {
+ integerReader.configure(6);
+ }
+ break;
+ case LITERAL_NEVER_INDEXED:
+ state = State.LITERAL_NEVER_INDEXED;
+ firstValueIndex = (b & 0b0000_1111) != 0;
+ if (firstValueIndex) {
+ integerReader.configure(4);
+ }
+ break;
+ case SIZE_UPDATE:
+ integerReader.configure(5);
+ state = State.SIZE_UPDATE;
+ firstValueIndex = true;
+ break;
+ default:
+ throw new InternalError(String.valueOf(s));
+ }
+ if (!firstValueIndex) {
+ input.get(); // advance, next stop: "String Literal"
+ }
+ }
+
+ // 0 1 2 3 4 5 6 7
+ // +---+---+---+---+---+---+---+---+
+ // | 1 | Index (7+) |
+ // +---+---------------------------+
+ //
+ private void resumeIndexed(ByteBuffer input, DecodingCallback action)
+ throws IOException {
+ if (!integerReader.read(input)) {
+ return;
+ }
+ intValue = integerReader.get();
+ integerReader.reset();
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("indexed %s", intValue));
+ }
+ try {
+ 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;
+ }
+}
--- /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}.
+ *
+ * <p> Methods of the callback are never called by a decoder with any of the
+ * arguments being {@code null}.
+ *
+ * @apiNote
+ *
+ * <p> The callback provides methods for all possible
+ * <a href="https://tools.ietf.org/html/rfc7541#section-6">binary representations</a>.
+ * This could be useful for implementing an intermediary, logging, debugging,
+ * etc.
+ *
+ * <p> The callback is an interface in order to interoperate with lambdas (in
+ * the most common use case):
+ * <pre>{@code
+ * DecodingCallback callback = (name, value) -> System.out.println(name + ", " + value);
+ * }</pre>
+ *
+ * <p> 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.
+ *
+ * <p> 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.
+ *
+ * <p> Value sensitivity must be considered, for example, when implementing
+ * an intermediary. A {@code value} is sensitive if it was represented as <a
+ * href="https://tools.ietf.org/html/rfc7541#section-6.2.3">Literal Header
+ * Field Never Indexed</a>.
+ *
+ * <p> 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
+ *
+ * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.1">Indexed
+ * Header Field</a> decoded.
+ *
+ * @implSpec
+ *
+ * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.2.2">Literal
+ * Header Field without Indexing</a> decoded, where a {@code name} was
+ * referred by an {@code index}.
+ *
+ * @implSpec
+ *
+ * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.2.2">Literal
+ * Header Field without Indexing</a> decoded, where both a {@code name} and
+ * a {@code value} were literal.
+ *
+ * @implSpec
+ *
+ * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">Literal
+ * Header Field Never Indexed</a> decoded, where a {@code name}
+ * was referred by an {@code index}.
+ *
+ * @implSpec
+ *
+ * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">Literal
+ * Header Field Never Indexed</a> decoded, where both a {@code
+ * name} and a {@code value} were literal.
+ *
+ * @implSpec
+ *
+ * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.2.1">Literal
+ * Header Field with Incremental Indexing</a> decoded, where a {@code name}
+ * was referred by an {@code index}.
+ *
+ * @implSpec
+ *
+ * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.2.1">Literal
+ * Header Field with Incremental Indexing</a> decoded, where both a {@code
+ * name} and a {@code value} were literal.
+ *
+ * @implSpec
+ *
+ * <p> 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 <a href="https://tools.ietf.org/html/rfc7541#section-6.3">Dynamic Table
+ * Size Update</a> decoded.
+ *
+ * @implSpec
+ *
+ * <p> The default implementation does nothing.
+ *
+ * @param capacity
+ * new capacity of the header table
+ */
+ default void onSizeUpdate(int capacity) { }
+}
--- /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.
+ *
+ * <p> Typical lifecycle looks like this:
+ *
+ * <p> {@link #Encoder(int) new Encoder}
+ * ({@link #setMaxCapacity(int) setMaxCapacity}?
+ * {@link #encode(ByteBuffer) encode})*
+ *
+ * <p> Suppose headers are represented by {@code Map<String, List<String>>}.
+ * A supplier and a consumer of {@link ByteBuffer}s in forms of
+ * {@code Supplier<ByteBuffer>} and {@code Consumer<ByteBuffer>} respectively.
+ * Then to encode headers, the following approach might be used:
+ *
+ * <pre>{@code
+ * for (Map.Entry<String, List<String>> 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);
+ * }
+ * }
+ * }</pre>
+ *
+ * <p> Though the specification <a href="https://tools.ietf.org/html/rfc7541#section-2">does not define</a>
+ * how an encoder is to be implemented, a default implementation is provided by
+ * the method {@link #header(CharSequence, CharSequence, boolean)}.
+ *
+ * <p> 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
+ *
+ * <p> 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.
+ *
+ * <p> 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.
+ *
+ * <p> 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
+ *
+ * <p> 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.
+ *
+ * <p> The value has to be agreed between decoder and encoder out-of-band,
+ * e.g. by a protocol that uses HPACK
+ * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
+ *
+ * @param 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)}.
+ *
+ * <p> 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.
+ *
+ * <p> If the {@code value} is sensitive (think security, secrecy, etc.)
+ * this encoder will compress it using a special representation
+ * (see <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">6.2.3. Literal Header Field Never Indexed</a>).
+ *
+ * <p> 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.
+ *
+ * <p> The value has to be agreed between decoder and encoder out-of-band,
+ * e.g. by a protocol that uses HPACK
+ * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
+ *
+ * <p> May be called any number of times after or before a complete header
+ * has been encoded.
+ *
+ * <p> 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.
+ *
+ * <p> Default implementation does not add anything to the headers table,
+ * hence this method returns {@code 0}.
+ *
+ * <p> 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.
+ *
+ * <p> 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.
+ *
+ * <p> 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<Integer> 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");
+ }
+ }
+}
--- /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<String, Level> logLevels =
+ Map.of("NORMAL", NORMAL, "EXTRA", EXTRA);
+
+ static {
+ String PROPERTY = "jdk.internal.httpclient.hpack.log.level";
+
+ String value = AccessController.doPrivileged(
+ (PrivilegedAction<String>) () -> 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<String> 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);
+ }
+
+ }
+}
--- /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<String, LinkedHashMap<String, Integer>> 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<String, Integer> 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<String, Integer> 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<x, Integer>. 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<String, Map<String, Long>> map;
+ private final CircularBuffer<HeaderField> buffer;
+ private long counter = 1;
+
+ Table(int capacity) {
+ buffer = new CircularBuffer<>(capacity);
+ map = new HashMap<>(capacity);
+ }
+
+ void add(HeaderField f) {
+ buffer.add(f);
+ Map<String, Long> 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<String, Long> 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<String, Long> 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<E> {
+
+ 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;
+ }
+ }
+}
--- /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.
+ *
+ * <p> 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;
+ }
+ }
+}
--- /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;
+ }
+ }
+}
--- /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;
+ }
+}
--- /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;
+ }
+}
--- /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;
+ }
+}
--- /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;
+ }
+}
--- /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);
+ }
+}
--- /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();
+ }
+}
--- /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);
+ }
+}
--- /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;
+ }
+}
--- /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;
+ }
+}
--- /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;
+ }
+}
--- /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
+ * <a href="https://tools.ietf.org/html/rfc7541">RFC 7541</a>.
+ *
+ * <p> Headers can be decoded and encoded by {@link jdk.internal.net.http.hpack.Decoder}
+ * and {@link jdk.internal.net.http.hpack.Encoder} respectively.
+ *
+ * <p> Instances of these classes are not safe for use by multiple threads.
+ */
+package jdk.internal.net.http.hpack;
--- /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> proxySelector;
+ private final Collection<Pair<String, String>> headers;
+ private final Collection<String> 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> proxySelector,
+ Collection<Pair<String, String>> headers,
+ Collection<String> 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<String> 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<WebSocket> 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<Pair<String, String>> getHeaders() { return headers; }
+
+ Collection<String> getSubprotocols() { return subprotocols; }
+
+ Duration getConnectTimeout() { return timeout; }
+
+ Optional<ProxySelector> 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;
+ }
+}
--- /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);
+ }
+}
--- /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);
+ }
+}
--- /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);
+ }
+ }
+}
--- /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));
+ }
+ }
+}
--- /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);
+}
--- /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<String> 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<String> 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<String, String> 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<String> createRequestSubprotocols(
+ Collection<String> subprotocols)
+ {
+ LinkedHashSet<String> 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<Result> send() {
+ PrivilegedAction<CompletableFuture<Result>> 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<Result> 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<String> 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<String> 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<String> values = responseHeaders.allValues(headerName);
+ if (!values.isEmpty()) {
+ throw checkFailed(format("Response field '%s' present: %s",
+ headerName,
+ stringOf(values)));
+ }
+ }
+
+ private static Optional<String> requireAtMostOne(HttpHeaders responseHeaders,
+ String headerName)
+ {
+ List<String> 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<String> 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<ProxySelector> selector, URI uri) {
+ if (!selector.isPresent()) {
+ return null;
+ }
+ URI requestURI = createRequestURI(uri); // Based on the HTTP scheme
+ List<Proxy> 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<String> 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);
+ }
+ }
+}
--- /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;
+ }
+ }
+}
--- /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.
+ *
+ * <p> Once shutdown for reading then further reads on the channel will
+ * return {@code null}, the end-of-stream indication. If the input side of
+ * the connection is already shutdown then invoking this method has no
+ * effect.
+ *
+ * @throws ClosedChannelException
+ * If this channel is closed
+ * @throws IOException
+ * If some other I/O error occurs
+ */
+ void shutdownInput() throws IOException;
+
+ /**
+ * Shutdown the connection for writing without closing the channel.
+ *
+ * <p> Once shutdown for writing then further attempts to write to the
+ * channel will throw {@link ClosedChannelException}. If the output side of
+ * the connection is already shutdown then invoking this method has no
+ * effect.
+ *
+ * @throws ClosedChannelException
+ * If this channel is closed
+ * @throws IOException
+ * If some other I/O error occurs
+ */
+ void shutdownOutput() throws IOException;
+
+ /**
+ * Closes this channel.
+ *
+ * @throws IOException
+ * If an I/O error occurs
+ */
+ @Override
+ void close() throws IOException;
+}
--- /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;
+ }
+ }
+}
--- /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<T> sendMessage(...)
+ * (3) CompletableFuture<T> 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<T> used is Transport<WebSocket>. 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<K> returned from Transport, to CompletableFuture<V>
+ * needed by the said class.
+ */
+public interface Transport<T> {
+
+ CompletableFuture<T> sendText(CharSequence message, boolean isLast);
+
+ CompletableFuture<T> sendBinary(ByteBuffer message, boolean isLast);
+
+ CompletableFuture<T> sendPing(ByteBuffer message);
+
+ CompletableFuture<T> sendPong(ByteBuffer message);
+
+ CompletableFuture<T> 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;
+}
--- /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 {
+
+ <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
+ MessageStreamConsumer consumer);
+}
--- /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 <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
+ MessageStreamConsumer consumer) {
+ return new TransportImpl<T>(sendResultSupplier, consumer, channel);
+ }
+}
--- /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<T> implements Transport<T> {
+
+ /* This flag is used solely for assertions */
+ private final AtomicBoolean busy = new AtomicBoolean();
+ private OutgoingMessage message;
+ private Consumer<Exception> completionHandler;
+ private final RawChannel channel;
+ private final RawChannel.RawEvent writeEvent = createWriteEvent();
+ private final SequentialScheduler sendScheduler = new SequentialScheduler(new SendTask());
+ private final Queue<Pair<OutgoingMessage, CompletableFuture<T>>>
+ queue = new ConcurrentLinkedQueue<>();
+ private final OutgoingMessage.Context context = new OutgoingMessage.Context();
+ private final Supplier<T> 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<T> 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<Exception> 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<Exception> 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<T> 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<T> sendBinary(ByteBuffer message,
+ boolean isLast) {
+ return enqueue(new OutgoingMessage.Binary(message, isLast));
+ }
+
+ public CompletableFuture<T> sendPing(ByteBuffer message) {
+ OutgoingMessage.Ping m;
+ try {
+ m = new OutgoingMessage.Ping(message);
+ } catch (IllegalArgumentException e) {
+ return failedFuture(e);
+ }
+ return enqueue(m);
+ }
+
+ public CompletableFuture<T> sendPong(ByteBuffer message) {
+ OutgoingMessage.Pong m;
+ try {
+ m = new OutgoingMessage.Pong(message);
+ } catch (IllegalArgumentException e) {
+ return failedFuture(e);
+ }
+ return enqueue(m);
+ }
+
+ public CompletableFuture<T> 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<T> enqueue(OutgoingMessage m) {
+ CompletableFuture<T> 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<OutgoingMessage, CompletableFuture<T>> p = queue.poll();
+ if (p == null) {
+ taskCompleter.complete();
+ return;
+ }
+ OutgoingMessage message = p.first;
+ CompletableFuture<T> cf = p.second;
+ try {
+ if (!message.contextualize(context)) { // Do not send the message
+ cf.complete(resultSupplier.get());
+ repeat(taskCompleter);
+ return;
+ }
+ Consumer<Exception> 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();
+ }
+ }
+ }
+ }
+ }
+}
--- /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();
+ }
+}
--- /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> 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<Throwable> error = new AtomicReference<>();
+
+ private final URI uri;
+ private final String subprotocol;
+ private final Listener listener;
+
+ private final AtomicBoolean outstandingSend = new AtomicBoolean();
+ private final Transport<WebSocket> transport;
+ private final SequentialScheduler receiveScheduler = new SequentialScheduler(new ReceiveTask());
+ private final Demand demand = new Demand();
+
+ public static CompletableFuture<WebSocket> newInstanceAsync(BuilderImpl b) {
+ Function<Result, WebSocket> 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<WebSocket> sendText(CharSequence message,
+ boolean isLast) {
+ Objects.requireNonNull(message);
+ if (!outstandingSend.compareAndSet(false, true)) {
+ return failedFuture(new IllegalStateException("Send pending"));
+ }
+ CompletableFuture<WebSocket> cf = transport.sendText(message, isLast);
+ return cf.whenComplete((r, e) -> outstandingSend.set(false));
+ }
+
+ @Override
+ public CompletableFuture<WebSocket> sendBinary(ByteBuffer message,
+ boolean isLast) {
+ Objects.requireNonNull(message);
+ if (!outstandingSend.compareAndSet(false, true)) {
+ return failedFuture(new IllegalStateException("Send pending"));
+ }
+ CompletableFuture<WebSocket> 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<WebSocket> sendPing(ByteBuffer message) {
+ return transport.sendPing(message);
+ }
+
+ @Override
+ public CompletableFuture<WebSocket> sendPong(ByteBuffer message) {
+ return transport.sendPong(message);
+ }
+
+ @Override
+ public CompletableFuture<WebSocket> 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<WebSocket> 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<WebSocket> 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<WebSocket> transport() {
+ return transport;
+ }
+}
--- /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);
+}
--- 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
--- 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
--- 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
*/
--- 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
--- 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
--- 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
--- 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
--- 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;
--- 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
*/
--- 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
--- 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
*/
--- 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
*/
--- 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
*/
--- 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
--- 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
--- 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
--- 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
*/
--- 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;
--- 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
--- 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
*/
--- 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 { }
--- 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 { }
--- 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 { }
--- 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 { }
--- 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 { }
--- 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 { }
--- 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 { }
--- 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;
--- 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
--- 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
*/
--- 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
*/
--- 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;
--- 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
+}
--- 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<? extends ByteBuffer> 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<ByteBuffer> 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);
- }
-}
--- 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}.
- *
- * <p> 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().
- *
- * <p> 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<? extends ByteBuffer> relocateBuffers(
- Iterable<? extends ByteBuffer> source) {
- return () ->
- new Iterator<ByteBuffer>() {
-
- private final Iterator<? extends ByteBuffer> 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<? extends ByteBuffer> injectEmptyBuffers(
- Iterable<? extends ByteBuffer> source) {
- return injectEmptyBuffers(source, () -> allocate(0));
- }
-
- public static Iterable<? extends ByteBuffer> injectEmptyBuffers(
- Iterable<? extends ByteBuffer> source,
- Supplier<? extends ByteBuffer> emptyBufferFactory) {
-
- return () ->
- new Iterator<ByteBuffer>() {
-
- private final Iterator<? extends ByteBuffer> 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<? extends ByteBuffer> split) {
- return concat(split, ByteBuffer::allocate);
- }
-
- public static ByteBuffer concat(Iterable<? extends ByteBuffer> split,
- Function<? super Integer, ? extends ByteBuffer> 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<? super Iterable<? extends ByteBuffer>> action) {
- forEachSplit(bb.remaining(),
- (lengths) -> {
- int end = bb.position();
- List<ByteBuffer> 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<? super Iterable<? extends Integer>> action) {
- forEachSplit(n, new Stack<>(), action);
- }
-
- private static void forEachSplit(int n, Stack<Integer> path,
- Consumer<? super Iterable<? extends Integer>> 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<ByteBuffer> buffers = Arrays.asList(
-// (ByteBuffer) allocate(3).position(1).limit(2),
-// allocate(0),
-// allocate(7));
-//
-// Iterable<? extends ByteBuffer> buf = relocateBuffers(injectEmptyBuffers(buffers));
-// List<ByteBuffer> result = new ArrayList<>();
-// buf.forEach(result::add);
-// System.out.println(result);
-// }
-}
--- 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<Integer> buffer = new CircularBuffer<>(16);
- buffer.resize(15);
- }
-
- private void resizeOnce(int capacity) {
-
- int nextNumberToPut = 0;
-
- Queue<Integer> referenceQueue = new ArrayBlockingQueue<>(capacity);
- CircularBuffer<Integer> 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<Integer> referenceQueue = new ArrayBlockingQueue<>(capacity);
- CircularBuffer<Integer> 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);
- }
- }
- }
-}
--- 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<Decoder> 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<Decoder> supplier, String hexdump,
- String expectedHeaderTable, String expectedHeaderList) {
- ByteBuffer source = SpecHelper.toBytes(hexdump);
-
- BuffersTestingKit.forEachSplit(source, iterable -> {
- List<String> actual = new LinkedList<>();
- Iterator<? extends ByteBuffer> 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<String> 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) -> { };
- }
-}
--- 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<Consumer<Encoder>> 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<Consumer<Encoder>> 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<ByteBuffer> copy = new LinkedList<>();
- iterable.forEach(b -> copy.add(ByteBuffer.allocate(b.remaining())));
- Iterator<ByteBuffer> 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<Encoder> 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<Integer, Encoder> 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<Integer, Encoder> 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<Integer, Encoder> 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<Integer, Encoder> 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<Integer, Encoder> encoder,
- int initialSize,
- List<Integer> updates,
- List<Integer> 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<Integer> 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();
- }
-}
--- 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<Integer, HeaderField> staticHeaderFields = createStaticEntries();
-
- Map<String, Integer> minimalIndexes = new HashMap<>();
-
- for (Map.Entry<Integer, HeaderField> 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<Integer, HeaderField> createStaticEntries() {
- Pattern line = Pattern.compile(
- "\\|\\s*(?<index>\\d+?)\\s*\\|\\s*(?<name>.+?)\\s*\\|\\s*(?<value>.*?)\\s*\\|");
- Matcher m = line.matcher(SPEC);
- Map<Integer, HeaderField> 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
- }
-}
--- 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*(?<ascii>\\d+)\\s*\\)\\s*(?<binary>(\\|(0|1)+)+)\\s*" +
- "(?<hex>[0-9a-zA-Z]+)\\s*\\[\\s*(?<len>\\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<Huffman.Node> 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<Huffman.Node> 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<Object> 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;
- }
-}
--- 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<String> 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<String> 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(" "));
- }
-}
--- 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 extends Throwable> T assertVoidThrows(Class<T> clazz, Block<?> code) {
- return assertThrows(clazz, () -> {
- code.run();
- return null;
- });
- }
-
- public static <T extends Throwable> T assertThrows(Class<T> 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> T assertDoesNotThrow(ReturningBlock<T> 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> {
- T run() throws Throwable;
- }
-
- public interface Block<T> {
- 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();
- }
-}
--- /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<? extends ByteBuffer> 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<ByteBuffer> 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);
+ }
+}
--- /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}.
+ *
+ * <p> 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().
+ *
+ * <p> 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<? extends ByteBuffer> relocateBuffers(
+ Iterable<? extends ByteBuffer> source) {
+ return () ->
+ new Iterator<ByteBuffer>() {
+
+ private final Iterator<? extends ByteBuffer> 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<? extends ByteBuffer> injectEmptyBuffers(
+ Iterable<? extends ByteBuffer> source) {
+ return injectEmptyBuffers(source, () -> allocate(0));
+ }
+
+ public static Iterable<? extends ByteBuffer> injectEmptyBuffers(
+ Iterable<? extends ByteBuffer> source,
+ Supplier<? extends ByteBuffer> emptyBufferFactory) {
+
+ return () ->
+ new Iterator<ByteBuffer>() {
+
+ private final Iterator<? extends ByteBuffer> 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<? extends ByteBuffer> split) {
+ return concat(split, ByteBuffer::allocate);
+ }
+
+ public static ByteBuffer concat(Iterable<? extends ByteBuffer> split,
+ Function<? super Integer, ? extends ByteBuffer> 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<? super Iterable<? extends ByteBuffer>> action) {
+ forEachSplit(bb.remaining(),
+ (lengths) -> {
+ int end = bb.position();
+ List<ByteBuffer> 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<? super Iterable<? extends Integer>> action) {
+ forEachSplit(n, new Stack<>(), action);
+ }
+
+ private static void forEachSplit(int n, Stack<Integer> path,
+ Consumer<? super Iterable<? extends Integer>> 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<ByteBuffer> buffers = Arrays.asList(
+// (ByteBuffer) allocate(3).position(1).limit(2),
+// allocate(0),
+// allocate(7));
+//
+// Iterable<? extends ByteBuffer> buf = relocateBuffers(injectEmptyBuffers(buffers));
+// List<ByteBuffer> result = new ArrayList<>();
+// buf.forEach(result::add);
+// System.out.println(result);
+// }
+}
--- /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<Integer> buffer = new CircularBuffer<>(16);
+ buffer.resize(15);
+ }
+
+ private void resizeOnce(int capacity) {
+
+ int nextNumberToPut = 0;
+
+ Queue<Integer> referenceQueue = new ArrayBlockingQueue<>(capacity);
+ CircularBuffer<Integer> 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<Integer> referenceQueue = new ArrayBlockingQueue<>(capacity);
+ CircularBuffer<Integer> 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);
+ }
+ }
+ }
+}
--- /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<Decoder> 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<Decoder> supplier, String hexdump,
+ String expectedHeaderTable, String expectedHeaderList) {
+ ByteBuffer source = SpecHelper.toBytes(hexdump);
+
+ BuffersTestingKit.forEachSplit(source, iterable -> {
+ List<String> actual = new LinkedList<>();
+ Iterator<? extends ByteBuffer> 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<String> 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) -> { };
+ }
+}
--- /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<Consumer<Encoder>> 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<Consumer<Encoder>> 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<ByteBuffer> copy = new LinkedList<>();
+ iterable.forEach(b -> copy.add(ByteBuffer.allocate(b.remaining())));
+ Iterator<ByteBuffer> 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<Encoder> 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<Integer, Encoder> 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<Integer, Encoder> 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<Integer, Encoder> 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<Integer, Encoder> 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<Integer, Encoder> encoder,
+ int initialSize,
+ List<Integer> updates,
+ List<Integer> 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<Integer> 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();
+ }
+}
--- /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<Integer, HeaderField> staticHeaderFields = createStaticEntries();
+
+ Map<String, Integer> minimalIndexes = new HashMap<>();
+
+ for (Map.Entry<Integer, HeaderField> 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<Integer, HeaderField> createStaticEntries() {
+ Pattern line = Pattern.compile(
+ "\\|\\s*(?<index>\\d+?)\\s*\\|\\s*(?<name>.+?)\\s*\\|\\s*(?<value>.*?)\\s*\\|");
+ Matcher m = line.matcher(SPEC);
+ Map<Integer, HeaderField> 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
+ }
+}
--- /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*(?<ascii>\\d+)\\s*\\)\\s*(?<binary>(\\|(0|1)+)+)\\s*" +
+ "(?<hex>[0-9a-zA-Z]+)\\s*\\[\\s*(?<len>\\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<Huffman.Node> 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<Huffman.Node> 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<Object> 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;
+ }
+}
--- /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<String> 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<String> 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(" "));
+ }
+}
--- /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 extends Throwable> T assertVoidThrows(Class<T> clazz, Block<?> code) {
+ return assertThrows(clazz, () -> {
+ code.run();
+ return null;
+ });
+ }
+
+ public static <T extends Throwable> T assertThrows(Class<T> 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> T assertDoesNotThrow(ReturningBlock<T> 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> {
+ T run() throws Throwable;
+ }
+
+ public interface Block<T> {
+ 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();
+ }
+}
--- 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
--- 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
--- 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() {}
--- 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() {}
--- 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 {
--- 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 {
--- 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 {
--- 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
--- 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
--- 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
--- 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
--- 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 {
--- 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 { }
--- 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 { }
--- 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 { }
--- 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 { }
--- 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 { }
--- 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<String> 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<String, CompletionStage<?>> 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() { };
- }
-}
--- 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);
- });
- }
-}
--- 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)
- };
- }
-}
--- 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<Invocation> 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<Invocation> 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<? extends Throwable> 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<? extends Throwable> clazz;
-
- public OnError(WebSocket webSocket, Class<? extends Throwable> 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);
- }
- }
-}
--- 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<T> implements Transport<T> {
-
- private final long startTime = System.currentTimeMillis();
- private final Queue<Invocation> output = new ConcurrentLinkedQueue<>();
- private final Queue<CompletableFuture<Consumer<MessageStreamConsumer>>>
- input = new ConcurrentLinkedQueue<>();
- private final Supplier<T> supplier;
- private final MessageStreamConsumer consumer;
- private final SequentialScheduler scheduler
- = new SequentialScheduler(new ReceiveTask());
- private final Demand demand = new Demand();
-
- public MockTransport(Supplier<T> sendResultSupplier,
- MessageStreamConsumer consumer) {
- this.supplier = sendResultSupplier;
- this.consumer = consumer;
- input.addAll(receive());
- }
-
- @Override
- public final CompletableFuture<T> 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<T> sendText0(CharSequence message,
- boolean isLast) {
- return defaultSend();
- }
-
- protected CompletableFuture<T> defaultSend() {
- return CompletableFuture.completedFuture(result());
- }
-
- @Override
- public final CompletableFuture<T> 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<T> sendBinary0(ByteBuffer message,
- boolean isLast) {
- return defaultSend();
- }
-
- @Override
- public final CompletableFuture<T> sendPing(ByteBuffer message) {
- output.add(Invocation.sendPing(message));
- return send(String.format("sendPing(%s)", message),
- () -> sendPing0(message));
- }
-
- protected CompletableFuture<T> sendPing0(ByteBuffer message) {
- return defaultSend();
- }
-
- @Override
- public final CompletableFuture<T> sendPong(ByteBuffer message) {
- output.add(Invocation.sendPong(message));
- return send(String.format("sendPong(%s)", message),
- () -> sendPong0(message));
- }
-
- protected CompletableFuture<T> sendPong0(ByteBuffer message) {
- return defaultSend();
- }
-
- @Override
- public final CompletableFuture<T> 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<T> sendClose0(int statusCode, String reason) {
- return defaultSend();
- }
-
- protected Collection<CompletableFuture<Consumer<MessageStreamConsumer>>> receive() {
- return List.of();
- }
-
- public static Consumer<MessageStreamConsumer> onText(CharSequence data,
- MessagePart part) {
- return c -> c.onText(data.toString(), part);
- }
-
- public static Consumer<MessageStreamConsumer> onBinary(ByteBuffer data,
- MessagePart part) {
- return c -> c.onBinary(fullCopy(data), part);
- }
-
- public static Consumer<MessageStreamConsumer> onPing(ByteBuffer data) {
- return c -> c.onPing(fullCopy(data));
- }
-
- public static Consumer<MessageStreamConsumer> onPong(ByteBuffer data) {
- return c -> c.onPong(fullCopy(data));
- }
-
- public static Consumer<MessageStreamConsumer> onClose(int statusCode,
- String reason) {
- return c -> c.onClose(statusCode, reason);
- }
-
- public static Consumer<MessageStreamConsumer> onError(Throwable error) {
- return c -> c.onError(error);
- }
-
- public static Consumer<MessageStreamConsumer> 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<Invocation> invocations() {
- return new LinkedList<>(output);
- }
-
- protected final T result() {
- return supplier.get();
- }
-
- private CompletableFuture<T> send(String name,
- Supplier<CompletableFuture<T>> supplier) {
- begin(name);
- CompletableFuture<T> 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<Consumer<MessageStreamConsumer>> 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();
- }
- }
-}
--- 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<Boolean> fin = Optional.empty();
- private Optional<Boolean> rsv1 = Optional.empty();
- private Optional<Boolean> rsv2 = Optional.empty();
- private Optional<Boolean> rsv3 = Optional.empty();
- private Optional<Opcode> opcode = Optional.empty();
- private Optional<Boolean> 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);
- }
- }
-}
--- 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 <A, B, R> Iterator<R> cartesianIterator(List<A> a,
- List<B> b,
- F2<A, B, R> f2) {
- @SuppressWarnings("unchecked")
- F<R> t = p -> f2.apply((A) p[0], (B) p[1]);
- return cartesianIterator(of(a, b), t);
- }
-
- static <A, B, C, R> Iterator<R> cartesianIterator(List<A> a,
- List<B> b,
- List<C> c,
- F3<A, B, C, R> f3) {
- @SuppressWarnings("unchecked")
- F<R> t = p -> f3.apply((A) p[0], (B) p[1], (C) p[2]);
- return cartesianIterator(of(a, b, c), t);
- }
-
- static <A, B, C, D, R> Iterator<R> cartesianIterator(List<A> a,
- List<B> b,
- List<C> c,
- List<D> d,
- F4<A, B, C, D, R> f4) {
- @SuppressWarnings("unchecked")
- F<R> 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 <A, B, C, D, E, R> Iterator<R> cartesianIterator(List<A> a,
- List<B> b,
- List<C> c,
- List<D> d,
- List<E> e,
- F5<A, B, C, D, E, R> f5) {
- @SuppressWarnings("unchecked")
- F<R> 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 <R> Iterator<R> cartesianIterator(List<? extends List<?>> params,
- F<R> 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<A, R> {
- R apply(A a);
- }
-
- @FunctionalInterface
- public interface F2<A, B, R> {
- R apply(A a, B b);
- }
-
- @FunctionalInterface
- public interface F3<A, B, C, R> {
- R apply(A a, B b, C c);
- }
-
- @FunctionalInterface
- public interface F4<A, B, C, D, R> {
- R apply(A a, B b, C c, D d);
- }
-
- @FunctionalInterface
- public interface F5<A, B, C, D, E, R> {
- R apply(A a, B b, C c, D d, E e);
- }
-
- @FunctionalInterface
- public interface F<R> {
- R apply(Object[] args);
- }
-
- static <T> Iterator<T> iteratorOf1(T element) {
- return List.of(element).iterator();
- }
-
- @SafeVarargs
- static <T> Iterator<T> iteratorOf(T... elements) {
- return List.of(elements).iterator();
- }
-
- static <T> Iterator<T> limit(int maxElements, Iterator<? extends T> 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<? super Iterable<? extends ByteBuffer>> action) {
- forEachPartition(src.remaining(),
- (lengths) -> {
- int end = src.position();
- List<ByteBuffer> 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<? super Iterable<Integer>> action) {
- forEachPartition(n, new Stack<>(), action);
- }
-
- private static void forEachPartition(int n,
- Stack<Integer> path,
- Consumer<? super Iterable<Integer>> 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<? super int[]> 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<? super int[]> 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 extends Throwable> T assertThrows(Class<? extends T> 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 extends Throwable> T assertThrows(Class<? extends T> clazz,
- String messageRegex,
- ThrowingProcedure code) {
- requireNonNull(messageRegex, "messagePattern");
- Predicate<Throwable> p = e -> clazz.isInstance(e)
- && Pattern.matches(messageRegex, e.getMessage());
- return (T) assertThrows(p, code);
- }
-
- static Throwable assertThrows(Predicate<? super Throwable> 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<? extends Throwable> 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);
- }
- }
-}
--- 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<MockListener.Invocation> 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<MockListener.Invocation> 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<MockListener.Invocation> 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<MockListener.Invocation> 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<MockListener.Invocation> 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 <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
- MessageStreamConsumer consumer) {
-
- final Random r = new Random();
-
- return new MockTransport<>(sendResultSupplier, consumer) {
- @Override
- protected CompletableFuture<T> defaultSend() {
- return millis(r.nextInt(100), result());
- }
- };
- }
- });
- int NUM_MESSAGES = 512;
- CompletableFuture<WebSocket> current = CompletableFuture.completedFuture(ws);
- for (int i = 0; i < NUM_MESSAGES; i++) {
- current = current.thenCompose(w -> w.sendText(" ", true));
- }
- current.join();
- MockTransport<WebSocket> transport = (MockTransport<WebSocket>) ws.transport();
- assertEquals(transport.invocations().size(), NUM_MESSAGES);
- }
-
- @Test
- public void testManyBinaryMessages() {
- WebSocketImpl ws = newInstance(
- new MockListener(1),
- new TransportFactory() {
- @Override
- public <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
- MessageStreamConsumer consumer) {
-
- final Random r = new Random();
-
- return new MockTransport<>(sendResultSupplier, consumer) {
- @Override
- protected CompletableFuture<T> defaultSend() {
- return millis(r.nextInt(150), result());
- }
- };
- }
- });
- CompletableFuture<WebSocket> start = new CompletableFuture<>();
-
- int NUM_MESSAGES = 512;
- CompletableFuture<WebSocket> 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<WebSocket> transport = (MockTransport<WebSocket>) ws.transport();
- assertEquals(transport.invocations().size(), NUM_MESSAGES);
- }
-
-
- @Test
- public void sendTextImmediately() {
- WebSocketImpl ws = newInstance(
- new MockListener(1),
- new TransportFactory() {
- @Override
- public <T> Transport<T> createTransport(Supplier<T> 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<WebSocket> transport = (MockTransport<WebSocket>) ws.transport();
- assertEquals(transport.invocations().size(), 3);
- }
-
- @Test
- public void sendTextWithDelay() {
- MockListener listener = new MockListener(1);
- WebSocketImpl ws = newInstance(
- listener,
- new TransportFactory() {
- @Override
- public <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
- MessageStreamConsumer consumer) {
- return new MockTransport<>(sendResultSupplier, consumer) {
- @Override
- protected CompletableFuture<T> 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<WebSocket> transport = (MockTransport<WebSocket>) 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 <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
- MessageStreamConsumer consumer) {
- return new MockTransport<>(sendResultSupplier, consumer) {
- @Override
- protected CompletableFuture<T> 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<WebSocket> transport = (MockTransport<WebSocket>) 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 <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
- MessageStreamConsumer consumer) {
- return new MockTransport<>(sendResultSupplier, consumer) {
- @Override
- protected CompletableFuture<T> 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<WebSocket> transport = (MockTransport<WebSocket>) ws.transport();
- assertEquals(transport.invocations().size(), 3); // 6 minus 3 that were not accepted
- }
-
- private static <T> CompletableFuture<T> seconds(long val, T result) {
- return new CompletableFuture<T>()
- .completeOnTimeout(result, val, TimeUnit.SECONDS);
- }
-
- private static <T> CompletableFuture<T> millis(long val, T result) {
- return new CompletableFuture<T>()
- .completeOnTimeout(result, val, TimeUnit.MILLISECONDS);
- }
-
- private static <T> CompletableFuture<T> now(T result) {
- return CompletableFuture.completedFuture(result);
- }
-
- private static WebSocketImpl newInstance(
- WebSocket.Listener listener,
- Collection<CompletableFuture<Consumer<MessageStreamConsumer>>> input) {
- TransportFactory factory = new TransportFactory() {
- @Override
- public <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
- MessageStreamConsumer consumer) {
- return new MockTransport<T>(sendResultSupplier, consumer) {
- @Override
- protected Collection<CompletableFuture<Consumer<MessageStreamConsumer>>> 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);
- }
-}
--- /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<String> 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<String, CompletionStage<?>> 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() { };
+ }
+}
--- /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);
+ });
+ }
+}
--- /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)
+ };
+ }
+}
--- /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<Invocation> 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<Invocation> 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<? extends Throwable> 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<? extends Throwable> clazz;
+
+ public OnError(WebSocket webSocket, Class<? extends Throwable> 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);
+ }
+ }
+}
--- /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<T> implements Transport<T> {
+
+ private final long startTime = System.currentTimeMillis();
+ private final Queue<Invocation> output = new ConcurrentLinkedQueue<>();
+ private final Queue<CompletableFuture<Consumer<MessageStreamConsumer>>>
+ input = new ConcurrentLinkedQueue<>();
+ private final Supplier<T> supplier;
+ private final MessageStreamConsumer consumer;
+ private final SequentialScheduler scheduler
+ = new SequentialScheduler(new ReceiveTask());
+ private final Demand demand = new Demand();
+
+ public MockTransport(Supplier<T> sendResultSupplier,
+ MessageStreamConsumer consumer) {
+ this.supplier = sendResultSupplier;
+ this.consumer = consumer;
+ input.addAll(receive());
+ }
+
+ @Override
+ public final CompletableFuture<T> 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<T> sendText0(CharSequence message,
+ boolean isLast) {
+ return defaultSend();
+ }
+
+ protected CompletableFuture<T> defaultSend() {
+ return CompletableFuture.completedFuture(result());
+ }
+
+ @Override
+ public final CompletableFuture<T> 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<T> sendBinary0(ByteBuffer message,
+ boolean isLast) {
+ return defaultSend();
+ }
+
+ @Override
+ public final CompletableFuture<T> sendPing(ByteBuffer message) {
+ output.add(Invocation.sendPing(message));
+ return send(String.format("sendPing(%s)", message),
+ () -> sendPing0(message));
+ }
+
+ protected CompletableFuture<T> sendPing0(ByteBuffer message) {
+ return defaultSend();
+ }
+
+ @Override
+ public final CompletableFuture<T> sendPong(ByteBuffer message) {
+ output.add(Invocation.sendPong(message));
+ return send(String.format("sendPong(%s)", message),
+ () -> sendPong0(message));
+ }
+
+ protected CompletableFuture<T> sendPong0(ByteBuffer message) {
+ return defaultSend();
+ }
+
+ @Override
+ public final CompletableFuture<T> 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<T> sendClose0(int statusCode, String reason) {
+ return defaultSend();
+ }
+
+ protected Collection<CompletableFuture<Consumer<MessageStreamConsumer>>> receive() {
+ return List.of();
+ }
+
+ public static Consumer<MessageStreamConsumer> onText(CharSequence data,
+ MessagePart part) {
+ return c -> c.onText(data.toString(), part);
+ }
+
+ public static Consumer<MessageStreamConsumer> onBinary(ByteBuffer data,
+ MessagePart part) {
+ return c -> c.onBinary(fullCopy(data), part);
+ }
+
+ public static Consumer<MessageStreamConsumer> onPing(ByteBuffer data) {
+ return c -> c.onPing(fullCopy(data));
+ }
+
+ public static Consumer<MessageStreamConsumer> onPong(ByteBuffer data) {
+ return c -> c.onPong(fullCopy(data));
+ }
+
+ public static Consumer<MessageStreamConsumer> onClose(int statusCode,
+ String reason) {
+ return c -> c.onClose(statusCode, reason);
+ }
+
+ public static Consumer<MessageStreamConsumer> onError(Throwable error) {
+ return c -> c.onError(error);
+ }
+
+ public static Consumer<MessageStreamConsumer> 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<Invocation> invocations() {
+ return new LinkedList<>(output);
+ }
+
+ protected final T result() {
+ return supplier.get();
+ }
+
+ private CompletableFuture<T> send(String name,
+ Supplier<CompletableFuture<T>> supplier) {
+ begin(name);
+ CompletableFuture<T> 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<Consumer<MessageStreamConsumer>> 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();
+ }
+ }
+}
--- /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<Boolean> fin = Optional.empty();
+ private Optional<Boolean> rsv1 = Optional.empty();
+ private Optional<Boolean> rsv2 = Optional.empty();
+ private Optional<Boolean> rsv3 = Optional.empty();
+ private Optional<Opcode> opcode = Optional.empty();
+ private Optional<Boolean> 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);
+ }
+ }
+}
--- /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 <A, B, R> Iterator<R> cartesianIterator(List<A> a,
+ List<B> b,
+ F2<A, B, R> f2) {
+ @SuppressWarnings("unchecked")
+ F<R> t = p -> f2.apply((A) p[0], (B) p[1]);
+ return cartesianIterator(of(a, b), t);
+ }
+
+ static <A, B, C, R> Iterator<R> cartesianIterator(List<A> a,
+ List<B> b,
+ List<C> c,
+ F3<A, B, C, R> f3) {
+ @SuppressWarnings("unchecked")
+ F<R> t = p -> f3.apply((A) p[0], (B) p[1], (C) p[2]);
+ return cartesianIterator(of(a, b, c), t);
+ }
+
+ static <A, B, C, D, R> Iterator<R> cartesianIterator(List<A> a,
+ List<B> b,
+ List<C> c,
+ List<D> d,
+ F4<A, B, C, D, R> f4) {
+ @SuppressWarnings("unchecked")
+ F<R> 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 <A, B, C, D, E, R> Iterator<R> cartesianIterator(List<A> a,
+ List<B> b,
+ List<C> c,
+ List<D> d,
+ List<E> e,
+ F5<A, B, C, D, E, R> f5) {
+ @SuppressWarnings("unchecked")
+ F<R> 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 <R> Iterator<R> cartesianIterator(List<? extends List<?>> params,
+ F<R> 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<A, R> {
+ R apply(A a);
+ }
+
+ @FunctionalInterface
+ public interface F2<A, B, R> {
+ R apply(A a, B b);
+ }
+
+ @FunctionalInterface
+ public interface F3<A, B, C, R> {
+ R apply(A a, B b, C c);
+ }
+
+ @FunctionalInterface
+ public interface F4<A, B, C, D, R> {
+ R apply(A a, B b, C c, D d);
+ }
+
+ @FunctionalInterface
+ public interface F5<A, B, C, D, E, R> {
+ R apply(A a, B b, C c, D d, E e);
+ }
+
+ @FunctionalInterface
+ public interface F<R> {
+ R apply(Object[] args);
+ }
+
+ static <T> Iterator<T> iteratorOf1(T element) {
+ return List.of(element).iterator();
+ }
+
+ @SafeVarargs
+ static <T> Iterator<T> iteratorOf(T... elements) {
+ return List.of(elements).iterator();
+ }
+
+ static <T> Iterator<T> limit(int maxElements, Iterator<? extends T> 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<? super Iterable<? extends ByteBuffer>> action) {
+ forEachPartition(src.remaining(),
+ (lengths) -> {
+ int end = src.position();
+ List<ByteBuffer> 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<? super Iterable<Integer>> action) {
+ forEachPartition(n, new Stack<>(), action);
+ }
+
+ private static void forEachPartition(int n,
+ Stack<Integer> path,
+ Consumer<? super Iterable<Integer>> 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<? super int[]> 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<? super int[]> 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 extends Throwable> T assertThrows(Class<? extends T> 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 extends Throwable> T assertThrows(Class<? extends T> clazz,
+ String messageRegex,
+ ThrowingProcedure code) {
+ requireNonNull(messageRegex, "messagePattern");
+ Predicate<Throwable> p = e -> clazz.isInstance(e)
+ && Pattern.matches(messageRegex, e.getMessage());
+ return (T) assertThrows(p, code);
+ }
+
+ static Throwable assertThrows(Predicate<? super Throwable> 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<? extends Throwable> 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);
+ }
+ }
+}
--- /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<MockListener.Invocation> 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<MockListener.Invocation> 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<MockListener.Invocation> 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<MockListener.Invocation> 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<MockListener.Invocation> 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 <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
+ MessageStreamConsumer consumer) {
+
+ final Random r = new Random();
+
+ return new MockTransport<>(sendResultSupplier, consumer) {
+ @Override
+ protected CompletableFuture<T> defaultSend() {
+ return millis(r.nextInt(100), result());
+ }
+ };
+ }
+ });
+ int NUM_MESSAGES = 512;
+ CompletableFuture<WebSocket> current = CompletableFuture.completedFuture(ws);
+ for (int i = 0; i < NUM_MESSAGES; i++) {
+ current = current.thenCompose(w -> w.sendText(" ", true));
+ }
+ current.join();
+ MockTransport<WebSocket> transport = (MockTransport<WebSocket>) ws.transport();
+ assertEquals(transport.invocations().size(), NUM_MESSAGES);
+ }
+
+ @Test
+ public void testManyBinaryMessages() {
+ WebSocketImpl ws = newInstance(
+ new MockListener(1),
+ new TransportFactory() {
+ @Override
+ public <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
+ MessageStreamConsumer consumer) {
+
+ final Random r = new Random();
+
+ return new MockTransport<>(sendResultSupplier, consumer) {
+ @Override
+ protected CompletableFuture<T> defaultSend() {
+ return millis(r.nextInt(150), result());
+ }
+ };
+ }
+ });
+ CompletableFuture<WebSocket> start = new CompletableFuture<>();
+
+ int NUM_MESSAGES = 512;
+ CompletableFuture<WebSocket> 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<WebSocket> transport = (MockTransport<WebSocket>) ws.transport();
+ assertEquals(transport.invocations().size(), NUM_MESSAGES);
+ }
+
+
+ @Test
+ public void sendTextImmediately() {
+ WebSocketImpl ws = newInstance(
+ new MockListener(1),
+ new TransportFactory() {
+ @Override
+ public <T> Transport<T> createTransport(Supplier<T> 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<WebSocket> transport = (MockTransport<WebSocket>) ws.transport();
+ assertEquals(transport.invocations().size(), 3);
+ }
+
+ @Test
+ public void sendTextWithDelay() {
+ MockListener listener = new MockListener(1);
+ WebSocketImpl ws = newInstance(
+ listener,
+ new TransportFactory() {
+ @Override
+ public <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
+ MessageStreamConsumer consumer) {
+ return new MockTransport<>(sendResultSupplier, consumer) {
+ @Override
+ protected CompletableFuture<T> 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<WebSocket> transport = (MockTransport<WebSocket>) 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 <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
+ MessageStreamConsumer consumer) {
+ return new MockTransport<>(sendResultSupplier, consumer) {
+ @Override
+ protected CompletableFuture<T> 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<WebSocket> transport = (MockTransport<WebSocket>) 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 <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
+ MessageStreamConsumer consumer) {
+ return new MockTransport<>(sendResultSupplier, consumer) {
+ @Override
+ protected CompletableFuture<T> 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<WebSocket> transport = (MockTransport<WebSocket>) ws.transport();
+ assertEquals(transport.invocations().size(), 3); // 6 minus 3 that were not accepted
+ }
+
+ private static <T> CompletableFuture<T> seconds(long val, T result) {
+ return new CompletableFuture<T>()
+ .completeOnTimeout(result, val, TimeUnit.SECONDS);
+ }
+
+ private static <T> CompletableFuture<T> millis(long val, T result) {
+ return new CompletableFuture<T>()
+ .completeOnTimeout(result, val, TimeUnit.MILLISECONDS);
+ }
+
+ private static <T> CompletableFuture<T> now(T result) {
+ return CompletableFuture.completedFuture(result);
+ }
+
+ private static WebSocketImpl newInstance(
+ WebSocket.Listener listener,
+ Collection<CompletableFuture<Consumer<MessageStreamConsumer>>> input) {
+ TransportFactory factory = new TransportFactory() {
+ @Override
+ public <T> Transport<T> createTransport(Supplier<T> sendResultSupplier,
+ MessageStreamConsumer consumer) {
+ return new MockTransport<T>(sendResultSupplier, consumer) {
+ @Override
+ protected Collection<CompletableFuture<Consumer<MessageStreamConsumer>>> 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);
+ }
+}
--- 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
*/
--- 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
*/
--- 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
*/
--- 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
*/
--- 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
*/
--- 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
*/
--- 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
*/
--- 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
*/
--- 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
*/
--- 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
*/
--- 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)
- }
-}
--- 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<List<ByteBuffer>> p =
- new SubmissionPublisher<>(ForkJoinPool.commonPool(),
- Integer.MAX_VALUE);
- FlowTube.TubePublisher begin = p::subscribe;
- CompletableFuture<Void> 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<ByteBuffer> 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<ByteBuffer> 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;
- }
- }
-}
--- 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<List<ByteBuffer>> 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<Void> 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<List<ByteBuffer>> reader = sslClient.upstreamReader();
- Subscriber<List<ByteBuffer>> 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.
- * <p>
- * 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<List<ByteBuffer>> {
- private final BlockingQueue<ByteBuffer> buffer;
- private final Socket clientSock;
- private final SSLSocket serverSock;
- private final Thread thread1, thread2, thread3;
- private volatile Flow.Subscription clientSubscription;
- private final SubmissionPublisher<List<ByteBuffer>> 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<List<ByteBuffer>> returnSubscriber) {
- publisher.subscribe(returnSubscriber);
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- clientSubscription = subscription;
- clientSubscription.request(5);
- }
-
- @Override
- public void onNext(List<ByteBuffer> 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<List<ByteBuffer>> {
-
- private final long nbytes;
-
- private final AtomicLong counter;
- private volatile Flow.Subscription subscription;
- private final CompletableFuture<Void> completion;
- private final CountDownLatch allBytesReceived;
-
- EndSubscriber(long nbytes,
- CompletableFuture<Void> 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<ByteBuffer> 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<ByteBuffer> 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.
- * <p>
- * Using this class with a security manager requires the following
- * permissions to be granted:
- * <p>
- * 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();
- }
- }
-}
--- 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
- * }
- * <p>
- * 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<? super List<ByteBuffer>> 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<ByteBuffer> 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
- * }
- * <p>
- * 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<Flow.Publisher<List<ByteBuffer>>> futurePublisher
- = new CompletableFuture<>();
- final ConcurrentLinkedQueue<Consumer<Flow.Subscriber<? super List<ByteBuffer>>>> queue
- = new ConcurrentLinkedQueue<>();
- AtomicReference<Flow.Subscriber<? super List<ByteBuffer>>> subscriberRef = new AtomicReference<>();
- SequentialScheduler scheduler = SequentialScheduler.synchronizedScheduler(this::loop);
- AtomicReference<Throwable> errorRef = new AtomicReference<>();
- private volatile boolean finished;
- private volatile boolean completed;
-
-
- public void start(Flow.Publisher<List<ByteBuffer>> publisher,
- Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
- subscriberRef.set(subscriber);
- futurePublisher.complete(publisher);
- scheduler.runOrSchedule();
- }
-
- @Override
- public boolean isFinished() {
- return finished;
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> 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<ByteBuffer> 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<? super List<ByteBuffer>> subscriber = subscriberRef.get();
- if (subscriber == null) return;
- try {
- Consumer<Flow.Subscriber<? super List<ByteBuffer>>> 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<Object> 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<? super List<ByteBuffer>> subscriber;
-
- private EchoTube(int maxBufferSize) {
- if (maxBufferSize < 1)
- throw new IllegalArgumentException();
- this.maxQueueSize = maxBufferSize;
- }
-
- @Override
- public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> 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<ByteBuffer> 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<ByteBuffer> bytes = (List<ByteBuffer>) 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;
- }
- }
- };
- }
- }
- }
--- 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<ByteBuffer> buffer;
- private final Socket clientSock;
- private final SSLSocket serverSock;
- private final Thread thread1, thread2, thread3;
- private volatile Flow.Subscription clientSubscription;
- private final SubmissionPublisher<List<ByteBuffer>> 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<List<ByteBuffer>> returnSubscriber) {
- publisher.subscribe(returnSubscriber);
- }
-
- @Override
- public void onSubscribe(Flow.Subscription subscription) {
- clientSubscription = subscription;
- clientSubscription.request(5);
- }
-
- @Override
- public void onNext(List<ByteBuffer> 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<? super List<ByteBuffer>> subscriber) {
- publisher.subscribe(subscriber);
- }
- }
-
-}
--- 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<List<ByteBuffer>> publisher;
- final SubscriberWrapper sub1, sub2, sub3;
- final ExecutorService executor = Executors.newCachedThreadPool();
- volatile int hipricount = 0;
-
- void errorHandler(Flow.Subscriber<? super List<ByteBuffer>> 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<Void> 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<ByteBuffer> list, boolean complete) {
- List<ByteBuffer> 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<List<ByteBuffer>> downstreamSubscriber)
- {
- super();
- subscribe(downstreamSubscriber);
- }
-
- // Inserts up to NUM_HI_PRI hi priority buffers into flow
- protected void incoming(List<ByteBuffer> 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<List<ByteBuffer>> {
- volatile Flow.Subscription subscription;
- volatile int hipriCounter=0;
- volatile int lopriCounter=0;
- final CompletableFuture<Void> cf;
-
- LastSubscriber(CompletableFuture<Void> 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<ByteBuffer> 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<ByteBuffer> getBuffer(int c) {
- ByteBuffer buf = ByteBuffer.allocate(BUFSIZE+4);
- buf.putInt(LO_PRI);
- for (int i=0; i<BUFSIZE_INT; i++) {
- buf.putInt(c++);
- }
- buf.flip();
- return List.of(buf);
- }
-
- boolean errorTest = false;
-
- @Test
- public void run() throws InterruptedException {
- try {
- CompletableFuture<Void> completion = sub3.completion();
- publisher.subscribe(sub3);
- // now submit a load of data
- int counter = 0;
- for (int i = 0; i < TOTAL; i++) {
- List<ByteBuffer> 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();
- }
-*/
-}
--- 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<count; i++) {
- InetSocketAddress addr = InetSocketAddress.createUnresolved("foo"+i, 80);
- keepAlives[i] = random.nextInt(10) * 10 + 10;
- connections[i] = new HttpConnectionStub(client, addr, proxy, true);
- System.out.println("Adding connection: " + now
- + " keepAlive: " + keepAlives[i]
- + " /" + connections[i]);
- pool.returnToPool(connections[i], now, keepAlives[i]);
- expected = Math.min(expected, keepAlives[i] * 1000);
- purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
- if (purge != expected) {
- throw new RuntimeException("Bad purge delay: " + purge
- + ", expected " + expected);
- }
- }
- int min = IntStream.of(keepAlives).min().getAsInt();
- int max = IntStream.of(keepAlives).max().getAsInt();
- int mean = (min + max)/2;
- System.out.println("min=" + min + ", max=" + max + ", mean=" + mean);
- purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
- System.out.println("first purge would be in " + purge + " ms");
- if (Math.abs(purge/1000 - min) > 0) {
- throw new RuntimeException("expected " + min + " got " + purge/1000);
- }
- long opened = java.util.stream.Stream.of(connections)
- .filter(HttpConnectionStub::connected).count();
- if (opened != count) {
- throw new RuntimeException("Opened: expected "
- + count + " got " + opened);
- }
- purge = mean * 1000;
- System.out.println("start purging at " + purge + " ms");
- Instant next = now;
- do {
- System.out.println("next purge is in " + purge + " ms");
- next = next.plus(purge, ChronoUnit.MILLIS);
- purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(next);
- long k = now.until(next, ChronoUnit.SECONDS);
- System.out.println("now is " + k + "s from start");
- for (int i=0; i<count; i++) {
- if (connections[i].connected() != (k < keepAlives[i])) {
- throw new RuntimeException("Bad connection state for "
- + i
- + "\n\t connected=" + connections[i].connected()
- + "\n\t keepAlive=" + keepAlives[i]
- + "\n\t elapsed=" + k);
- }
- }
- } while (purge > 0);
- opened = java.util.stream.Stream.of(connections)
- .filter(HttpConnectionStub::connected).count();
- if (opened != 0) {
- throw new RuntimeException("Closed: expected "
- + count + " got "
- + (count-opened));
- }
- }
-
- static <T> T error() {
- throw new InternalError("Should not reach here: wrong test assumptions!");
- }
-
- static class FlowTubeStub implements FlowTube {
- final HttpConnectionStub conn;
- FlowTubeStub(HttpConnectionStub conn) { this.conn = conn; }
- @Override
- public void onSubscribe(Flow.Subscription subscription) { }
- @Override public void onError(Throwable error) { error(); }
- @Override public void onComplete() { error(); }
- @Override public void onNext(List<ByteBuffer> item) { error();}
- @Override
- public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
- }
- @Override public boolean isFinished() { return conn.closed; }
- }
-
- // Emulates an HttpConnection that has a strong reference to its HttpClient.
- static class HttpConnectionStub extends HttpConnection {
-
- public HttpConnectionStub(HttpClient client,
- InetSocketAddress address,
- InetSocketAddress proxy,
- boolean secured) {
- super(address, null);
- this.key = ConnectionPool.cacheKey(address, proxy);
- this.address = address;
- this.proxy = proxy;
- this.secured = secured;
- this.client = client;
- this.flow = new FlowTubeStub(this);
- }
-
- final InetSocketAddress proxy;
- final InetSocketAddress address;
- final boolean secured;
- final ConnectionPool.CacheKey key;
- final HttpClient client;
- final FlowTubeStub flow;
- volatile boolean closed;
-
- // All these return something
- @Override boolean connected() {return !closed;}
- @Override boolean isSecure() {return secured;}
- @Override boolean isProxied() {return proxy!=null;}
- @Override ConnectionPool.CacheKey cacheKey() {return key;}
- @Override void shutdownInput() throws IOException {}
- @Override void shutdownOutput() throws IOException {}
- @Override
- public void close() {
- closed=true;
- System.out.println("closed: " + this);
- }
- @Override
- public String toString() {
- return "HttpConnectionStub: " + address + " proxy: " + proxy;
- }
-
- // All these throw errors
- @Override public HttpPublisher publisher() {return error();}
- @Override public CompletableFuture<Void> connectAsync() {return error();}
- @Override SocketChannel channel() {return error();}
- @Override
- HttpConnection.DetachedConnectionChannel detachChannel() {
- return error();
- }
- @Override
- FlowTube getConnectionFlow() {return flow;}
- }
- // Emulates an HttpClient that has a strong reference to its connection pool.
- static class HttpClientStub extends HttpClient {
- public HttpClientStub(ConnectionPool pool) {
- this.pool = pool;
- }
- final ConnectionPool pool;
- @Override public Optional<CookieHandler> cookieHandler() {return error();}
- @Override public HttpClient.Redirect followRedirects() {return error();}
- @Override public Optional<ProxySelector> proxy() {return error();}
- @Override public SSLContext sslContext() {return error();}
- @Override public SSLParameters sslParameters() {return error();}
- @Override public Optional<Authenticator> authenticator() {return error();}
- @Override public HttpClient.Version version() {return HttpClient.Version.HTTP_1_1;}
- @Override public Optional<Executor> executor() {return error();}
- @Override
- public <T> HttpResponse<T> send(HttpRequest req,
- HttpResponse.BodyHandler<T> responseBodyHandler)
- throws IOException, InterruptedException {
- return error();
- }
- @Override
- public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest req,
- HttpResponse.BodyHandler<T> responseBodyHandler) {
- return error();
- }
- @Override
- public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest req,
- HttpResponse.BodyHandler<T> bodyHandler,
- HttpResponse.PushPromiseHandler<T> multiHandler) {
- return error();
- }
- }
-
-}
--- a/test/jdk/java/net/httpclient/whitebox/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<String> responses = new ArrayList<>();
-
- String[] basic =
- { "HTTP/1.1 200 OK\r\n\r\n",
-
- "HTTP/1.1 200 OK\r\n" +
- "Date: Mon, 15 Jan 2001 12:18:21 GMT\r\n" +
- "Server: Apache/1.3.14 (Unix)\r\n" +
- "Connection: close\r\n" +
- "Content-Type: text/html; charset=iso-8859-1\r\n" +
- "Content-Length: 10\r\n\r\n" +
- "123456789",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 9\r\n" +
- "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
- "XXXXX",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 9\r\n" +
- "Content-Type: text/html; charset=UTF-8\r\n\r\n" + // more than one SP after ':'
- "XXXXX",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length:\t10\r\n" +
- "Content-Type:\ttext/html; charset=UTF-8\r\n\r\n" + // HT separator
- "XXXXX",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length:\t\t10\r\n" +
- "Content-Type:\t\ttext/html; charset=UTF-8\r\n\r\n" + // more than one HT after ':'
- "XXXXX",
-
- "HTTP/1.1 407 Proxy Authorization Required\r\n" +
- "Proxy-Authenticate: Basic realm=\"a fake realm\"\r\n\r\n",
-
- "HTTP/1.1 401 Unauthorized\r\n" +
- "WWW-Authenticate: Digest realm=\"wally land\" domain=/ " +
- "nonce=\"2B7F3A2B\" qop=\"auth\"\r\n\r\n",
-
- "HTTP/1.1 200 OK\r\n" +
- "X-Foo:\r\n\r\n", // no value
-
- "HTTP/1.1 200 OK\r\n" +
- "X-Foo:\r\n\r\n" + // no value, with response body
- "Some Response Body",
-
- "HTTP/1.1 200 OK\r\n" +
- "X-Foo:\r\n" + // no value, followed by another header
- "Content-Length: 10\r\n\r\n" +
- "Some Response Body",
-
- "HTTP/1.1 200 OK\r\n" +
- "X-Foo:\r\n" + // no value, followed by another header, with response body
- "Content-Length: 10\r\n\r\n",
-
- "HTTP/1.1 200 OK\r\n" +
- "X-Foo: chegar\r\n" +
- "X-Foo: dfuchs\r\n" + // same header appears multiple times
- "Content-Length: 0\r\n" +
- "X-Foo: michaelm\r\n" +
- "X-Foo: prappo\r\n\r\n",
-
- "HTTP/1.1 200 OK\r\n" +
- "X-Foo:\r\n" + // no value, same header appears multiple times
- "X-Foo: dfuchs\r\n" +
- "Content-Length: 0\r\n" +
- "X-Foo: michaelm\r\n" +
- "X-Foo: prappo\r\n\r\n",
-
- "HTTP/1.1 200 OK\r\n" +
- "Accept-Ranges: bytes\r\n" +
- "Cache-control: max-age=0, no-cache=\"set-cookie\"\r\n" +
- "Content-Length: 132868\r\n" +
- "Content-Type: text/html; charset=UTF-8\r\n" +
- "Date: Sun, 05 Nov 2017 22:24:03 GMT\r\n" +
- "Server: Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips Communique/4.2.2\r\n" +
- "Set-Cookie: AWSELB=AF7927F5100F4202119876ED2436B5005EE;PATH=/;MAX-AGE=900\r\n" +
- "Vary: Host,Accept-Encoding,User-Agent\r\n" +
- "X-Mod-Pagespeed: 1.12.34.2-0\r\n" +
- "Connection: keep-alive\r\n\r\n"
- };
- Arrays.stream(basic).forEach(responses::add);
-
- String[] foldingTemplate =
- { "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 9\r\n" +
- "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r'
- " charset=UTF-8\r\n" + // one preceding SP
- "Connection: close\r\n\r\n" +
- "XXYYZZAABBCCDDEE",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 19\r\n" +
- "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
- " charset=UTF-8\r\n" + // more than one preceding SP
- "Connection: keep-alive\r\n\r\n" +
- "XXYYZZAABBCCDDEEFFGG",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 999\r\n" +
- "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
- "\tcharset=UTF-8\r\n" + // one preceding HT
- "Connection: close\r\n\r\n" +
- "XXYYZZAABBCCDDEE",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 54\r\n" +
- "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
- "\t\t\tcharset=UTF-8\r\n" + // more than one preceding HT
- "Connection: keep-alive\r\n\r\n" +
- "XXYYZZAABBCCDDEEFFGG",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: -1\r\n" +
- "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
- "\t \t \tcharset=UTF-8\r\n" + // mix of preceding HT and SP
- "Connection: keep-alive\r\n\r\n" +
- "XXYYZZAABBCCDDEEFFGGHH",
-
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 65\r\n" +
- "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
- " \t \t charset=UTF-8\r\n" + // mix of preceding SP and HT
- "Connection: keep-alive\r\n\r\n" +
- "XXYYZZAABBCCDDEEFFGGHHII",
-
- "HTTP/1.1 401 Unauthorized\r\n" +
- "WWW-Authenticate: Digest realm=\"wally land\","
- +"$NEWLINE domain=/,"
- +"$NEWLINE nonce=\"2B7F3A2B\","
- +"$NEWLINE\tqop=\"auth\"\r\n\r\n",
-
- };
- for (String newLineChar : new String[] { "\n", "\r", "\r\n" }) {
- for (String template : foldingTemplate)
- responses.add(template.replace("$NEWLINE", newLineChar));
- }
-
- String[] bad = // much of this is to retain parity with legacy MessageHeaders
- { "HTTP/1.1 200 OK\r\n" +
- "Connection:\r\n\r\n", // empty value, no body
-
- "HTTP/1.1 200 OK\r\n" +
- "Connection:\r\n\r\n" + // empty value, with body
- "XXXXX",
-
- "HTTP/1.1 200 OK\r\n" +
- ": no header\r\n\r\n", // no/empty header-name, no body, no following header
-
- "HTTP/1.1 200 OK\r\n" +
- ": no; header\r\n" + // no/empty header-name, no body, following header
- "Content-Length: 65\r\n\r\n",
-
- "HTTP/1.1 200 OK\r\n" +
- ": no header\r\n" + // no/empty header-name
- "Content-Length: 65\r\n\r\n" +
- "XXXXX",
-
- "HTTP/1.1 200 OK\r\n" +
- ": no header\r\n\r\n" + // no/empty header-name, followed by header
- "XXXXX",
-
- "HTTP/1.1 200 OK\r\n" +
- "Conte\r" +
- " nt-Length: 9\r\n" + // fold/bad header name ???
- "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
- "XXXXX",
-
- "HTTP/1.1 200 OK\r\n" +
- "Conte\r" +
- "nt-Length: 9\r\n" + // fold/bad header name ??? without preceding space
- "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
- "XXXXXYYZZ",
-
- "HTTP/1.0 404 Not Found\r\n" +
- "header-without-colon\r\n\r\n",
-
- "HTTP/1.0 404 Not Found\r\n" +
- "header-without-colon\r\n\r\n" +
- "SOMEBODY",
-
- };
- Arrays.stream(bad).forEach(responses::add);
-
- return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
- }
-
- @Test(dataProvider = "responses")
- public void verifyHeaders(String respString) throws Exception {
- byte[] bytes = respString.getBytes(US_ASCII);
- ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
- MessageHeader m = new MessageHeader(bais);
- Map<String,List<String>> messageHeaderMap = m.getHeaders();
- int available = bais.available();
-
- Http1HeaderParser decoder = new Http1HeaderParser();
- ByteBuffer b = ByteBuffer.wrap(bytes);
- decoder.parse(b);
- Map<String,List<String>> decoderMap1 = decoder.headers().map();
- assertEquals(available, b.remaining(),
- "stream available not equal to remaining");
-
- // assert status-line
- String statusLine1 = messageHeaderMap.get(null).get(0);
- String statusLine2 = decoder.statusLine();
- if (statusLine1.startsWith("HTTP")) {// skip the case where MH's messes up the status-line
- assertEquals(statusLine1, statusLine2, "Status-line not equal");
- } else {
- assertTrue(statusLine2.startsWith("HTTP/1."), "Status-line not HTTP/1.");
- }
-
- // remove the null'th entry with is the status-line
- Map<String,List<String>> map = new HashMap<>();
- for (Map.Entry<String,List<String>> e : messageHeaderMap.entrySet()) {
- if (e.getKey() != null) {
- map.put(e.getKey(), e.getValue());
- }
- }
- messageHeaderMap = map;
-
- assertHeadersEqual(messageHeaderMap, decoderMap1,
- "messageHeaderMap not equal to decoderMap1");
-
- // byte at a time
- decoder = new Http1HeaderParser();
- List<ByteBuffer> buffers = IntStream.range(0, bytes.length)
- .mapToObj(i -> ByteBuffer.wrap(bytes, i, 1))
- .collect(toList());
- while (decoder.parse(buffers.remove(0)) != true);
- Map<String,List<String>> decoderMap2 = decoder.headers().map();
- assertEquals(available, buffers.size(),
- "stream available not equals to remaining buffers");
- assertEquals(decoderMap1, decoderMap2, "decoder maps not equal");
- }
-
- @DataProvider(name = "errors")
- public Object[][] errors() {
- List<String> responses = new ArrayList<>();
-
- // These responses are parsed, somewhat, by MessageHeaders but give
- // nonsensible results. They, correctly, fail with the Http1HeaderParser.
- String[] bad =
- {// "HTTP/1.1 402 Payment Required\r\n" +
- // "Content-Length: 65\r\n\r", // missing trailing LF //TODO: incomplete
-
- "HTTP/1.1 402 Payment Required\r\n" +
- "Content-Length: 65\r\n\rT\r\n\r\nGGGGGG",
-
- "HTTP/1.1 200OK\r\n\rT",
-
- "HTTP/1.1 200OK\rT",
- };
- Arrays.stream(bad).forEach(responses::add);
-
- return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
- }
-
- @Test(dataProvider = "errors", expectedExceptions = ProtocolException.class)
- public void errors(String respString) throws ProtocolException {
- byte[] bytes = respString.getBytes(US_ASCII);
- Http1HeaderParser decoder = new Http1HeaderParser();
- ByteBuffer b = ByteBuffer.wrap(bytes);
- decoder.parse(b);
- }
-
- void assertHeadersEqual(Map<String,List<String>> expected,
- Map<String,List<String>> actual,
- String msg) {
-
- if (expected.equals(actual))
- return;
-
- assertEquals(expected.size(), actual.size(),
- format("%s. Expected size %d, actual size %s. %nexpected= %s,%n actual=%s.",
- msg, expected.size(), actual.size(), mapToString(expected), mapToString(actual)));
-
- for (Map.Entry<String,List<String>> e : expected.entrySet()) {
- String key = e.getKey();
- List<String> values = e.getValue();
-
- boolean found = false;
- for (Map.Entry<String,List<String>> other: actual.entrySet()) {
- if (key.equalsIgnoreCase(other.getKey())) {
- found = true;
- List<String> otherValues = other.getValue();
- assertEquals(values.size(), otherValues.size(),
- format("%s. Expected list size %d, actual size %s",
- msg, values.size(), otherValues.size()));
- if (!(values.containsAll(otherValues) && otherValues.containsAll(values)))
- assertTrue(false, format("Lists are unequal [%s] [%s]", values, otherValues));
- break;
- }
- }
- assertTrue(found, format("header name, %s, not found in %s", key, actual));
- }
- }
-
- static String mapToString(Map<String,List<String>> map) {
- StringBuilder sb = new StringBuilder();
- List<String> sortedKeys = new ArrayList(map.keySet());
- Collections.sort(sortedKeys);
- for (String key : sortedKeys) {
- List<String> values = map.get(key);
- sb.append("\n\t" + key + " | " + values);
- }
- return sb.toString();
- }
-
- // ---
-
- /* Main entry point for standalone testing of the main functional test. */
- public static void main(String... args) throws Exception {
- Http1HeaderParserTest test = new Http1HeaderParserTest();
- int count = 0;
- for (Object[] objs : test.responses()) {
- out.println("Testing " + count++ + ", " + objs[0]);
- test.verifyHeaders((String) objs[0]);
- }
- for (Object[] objs : test.errors()) {
- out.println("Testing " + count++ + ", " + objs[0]);
- try {
- test.errors((String) objs[0]);
- throw new RuntimeException("Expected ProtocolException for " + objs[0]);
- } catch (ProtocolException expected) { /* Ok */ }
- }
- }
-}
--- a/test/jdk/java/net/httpclient/whitebox/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)];
- }
-}
--- 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);
- }
- }
-}
--- 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<Throwable> 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);
- }
-}
--- 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<Object> 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<Object> 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<Object> mf = new MinimalFuture<>();
- mf.completeExceptionally(new Throwable());
-
- MinimalFuture<Object> 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()));
- }
-}
--- 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<ByteBuffer> data1 = List.of(ByteBuffer.wrap("XXXX".getBytes(UTF_8)));
- DataFrame dataFrame1 = new DataFrame(1, 0, data1);
- List<ByteBuffer> data2 = List.of(ByteBuffer.wrap("YYYY".getBytes(UTF_8)));
- DataFrame dataFrame2 = new DataFrame(1, 0, data2);
-
- List<ByteBuffer> 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<ByteBuffer> 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<ByteBuffer> data1 = List.of(ByteBuffer.wrap("XXXX".getBytes(UTF_8)));
- DataFrame dataFrame1 = new DataFrame(1, 0, data1);
-
- List<ByteBuffer> 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<ByteBuffer> 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);
- }
-}
--- /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)
+ }
+}
--- /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<List<ByteBuffer>> p =
+ new SubmissionPublisher<>(ForkJoinPool.commonPool(),
+ Integer.MAX_VALUE);
+ FlowTube.TubePublisher begin = p::subscribe;
+ CompletableFuture<Void> 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<ByteBuffer> 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<ByteBuffer> 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;
+ }
+ }
+}
--- /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<count; i++) {
+ InetSocketAddress addr = InetSocketAddress.createUnresolved("foo"+i, 80);
+ keepAlives[i] = random.nextInt(10) * 10 + 10;
+ connections[i] = new HttpConnectionStub(client, addr, proxy, true);
+ System.out.println("Adding connection: " + now
+ + " keepAlive: " + keepAlives[i]
+ + " /" + connections[i]);
+ pool.returnToPool(connections[i], now, keepAlives[i]);
+ expected = Math.min(expected, keepAlives[i] * 1000);
+ purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
+ if (purge != expected) {
+ throw new RuntimeException("Bad purge delay: " + purge
+ + ", expected " + expected);
+ }
+ }
+ int min = IntStream.of(keepAlives).min().getAsInt();
+ int max = IntStream.of(keepAlives).max().getAsInt();
+ int mean = (min + max)/2;
+ System.out.println("min=" + min + ", max=" + max + ", mean=" + mean);
+ purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(now);
+ System.out.println("first purge would be in " + purge + " ms");
+ if (Math.abs(purge/1000 - min) > 0) {
+ throw new RuntimeException("expected " + min + " got " + purge/1000);
+ }
+ long opened = java.util.stream.Stream.of(connections)
+ .filter(HttpConnectionStub::connected).count();
+ if (opened != count) {
+ throw new RuntimeException("Opened: expected "
+ + count + " got " + opened);
+ }
+ purge = mean * 1000;
+ System.out.println("start purging at " + purge + " ms");
+ Instant next = now;
+ do {
+ System.out.println("next purge is in " + purge + " ms");
+ next = next.plus(purge, ChronoUnit.MILLIS);
+ purge = pool.purgeExpiredConnectionsAndReturnNextDeadline(next);
+ long k = now.until(next, ChronoUnit.SECONDS);
+ System.out.println("now is " + k + "s from start");
+ for (int i=0; i<count; i++) {
+ if (connections[i].connected() != (k < keepAlives[i])) {
+ throw new RuntimeException("Bad connection state for "
+ + i
+ + "\n\t connected=" + connections[i].connected()
+ + "\n\t keepAlive=" + keepAlives[i]
+ + "\n\t elapsed=" + k);
+ }
+ }
+ } while (purge > 0);
+ opened = java.util.stream.Stream.of(connections)
+ .filter(HttpConnectionStub::connected).count();
+ if (opened != 0) {
+ throw new RuntimeException("Closed: expected "
+ + count + " got "
+ + (count-opened));
+ }
+ }
+
+ static <T> T error() {
+ throw new InternalError("Should not reach here: wrong test assumptions!");
+ }
+
+ static class FlowTubeStub implements FlowTube {
+ final HttpConnectionStub conn;
+ FlowTubeStub(HttpConnectionStub conn) { this.conn = conn; }
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) { }
+ @Override public void onError(Throwable error) { error(); }
+ @Override public void onComplete() { error(); }
+ @Override public void onNext(List<ByteBuffer> item) { error();}
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+ }
+ @Override public boolean isFinished() { return conn.closed; }
+ }
+
+ // Emulates an HttpConnection that has a strong reference to its HttpClient.
+ static class HttpConnectionStub extends HttpConnection {
+
+ public HttpConnectionStub(HttpClient client,
+ InetSocketAddress address,
+ InetSocketAddress proxy,
+ boolean secured) {
+ super(address, null);
+ this.key = ConnectionPool.cacheKey(address, proxy);
+ this.address = address;
+ this.proxy = proxy;
+ this.secured = secured;
+ this.client = client;
+ this.flow = new FlowTubeStub(this);
+ }
+
+ final InetSocketAddress proxy;
+ final InetSocketAddress address;
+ final boolean secured;
+ final ConnectionPool.CacheKey key;
+ final HttpClient client;
+ final FlowTubeStub flow;
+ volatile boolean closed;
+
+ // All these return something
+ @Override boolean connected() {return !closed;}
+ @Override boolean isSecure() {return secured;}
+ @Override boolean isProxied() {return proxy!=null;}
+ @Override ConnectionPool.CacheKey cacheKey() {return key;}
+ @Override void shutdownInput() throws IOException {}
+ @Override void shutdownOutput() throws IOException {}
+ @Override
+ public void close() {
+ closed=true;
+ System.out.println("closed: " + this);
+ }
+ @Override
+ public String toString() {
+ return "HttpConnectionStub: " + address + " proxy: " + proxy;
+ }
+
+ // All these throw errors
+ @Override public HttpPublisher publisher() {return error();}
+ @Override public CompletableFuture<Void> connectAsync() {return error();}
+ @Override SocketChannel channel() {return error();}
+ @Override
+ HttpConnection.DetachedConnectionChannel detachChannel() {
+ return error();
+ }
+ @Override
+ FlowTube getConnectionFlow() {return flow;}
+ }
+ // Emulates an HttpClient that has a strong reference to its connection pool.
+ static class HttpClientStub extends HttpClient {
+ public HttpClientStub(ConnectionPool pool) {
+ this.pool = pool;
+ }
+ final ConnectionPool pool;
+ @Override public Optional<CookieHandler> cookieHandler() {return error();}
+ @Override public HttpClient.Redirect followRedirects() {return error();}
+ @Override public Optional<ProxySelector> proxy() {return error();}
+ @Override public SSLContext sslContext() {return error();}
+ @Override public SSLParameters sslParameters() {return error();}
+ @Override public Optional<Authenticator> authenticator() {return error();}
+ @Override public HttpClient.Version version() {return HttpClient.Version.HTTP_1_1;}
+ @Override public Optional<Executor> executor() {return error();}
+ @Override
+ public <T> HttpResponse<T> send(HttpRequest req,
+ HttpResponse.BodyHandler<T> responseBodyHandler)
+ throws IOException, InterruptedException {
+ return error();
+ }
+ @Override
+ public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest req,
+ HttpResponse.BodyHandler<T> responseBodyHandler) {
+ return error();
+ }
+ @Override
+ public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest req,
+ HttpResponse.BodyHandler<T> bodyHandler,
+ HttpResponse.PushPromiseHandler<T> multiHandler) {
+ return error();
+ }
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/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<List<ByteBuffer>> 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<Void> 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<List<ByteBuffer>> reader = sslClient.upstreamReader();
+ Subscriber<List<ByteBuffer>> 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.
+ * <p>
+ * 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<List<ByteBuffer>> {
+ private final BlockingQueue<ByteBuffer> buffer;
+ private final Socket clientSock;
+ private final SSLSocket serverSock;
+ private final Thread thread1, thread2, thread3;
+ private volatile Flow.Subscription clientSubscription;
+ private final SubmissionPublisher<List<ByteBuffer>> 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<List<ByteBuffer>> returnSubscriber) {
+ publisher.subscribe(returnSubscriber);
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ clientSubscription = subscription;
+ clientSubscription.request(5);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> 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<List<ByteBuffer>> {
+
+ private final long nbytes;
+
+ private final AtomicLong counter;
+ private volatile Flow.Subscription subscription;
+ private final CompletableFuture<Void> completion;
+ private final CountDownLatch allBytesReceived;
+
+ EndSubscriber(long nbytes,
+ CompletableFuture<Void> 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<ByteBuffer> 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<ByteBuffer> 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.
+ * <p>
+ * Using this class with a security manager requires the following
+ * permissions to be granted:
+ * <p>
+ * 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();
+ }
+ }
+}
--- /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<String> responses = new ArrayList<>();
+
+ String[] basic =
+ { "HTTP/1.1 200 OK\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Date: Mon, 15 Jan 2001 12:18:21 GMT\r\n" +
+ "Server: Apache/1.3.14 (Unix)\r\n" +
+ "Connection: close\r\n" +
+ "Content-Type: text/html; charset=iso-8859-1\r\n" +
+ "Content-Length: 10\r\n\r\n" +
+ "123456789",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 9\r\n" +
+ "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 9\r\n" +
+ "Content-Type: text/html; charset=UTF-8\r\n\r\n" + // more than one SP after ':'
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length:\t10\r\n" +
+ "Content-Type:\ttext/html; charset=UTF-8\r\n\r\n" + // HT separator
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length:\t\t10\r\n" +
+ "Content-Type:\t\ttext/html; charset=UTF-8\r\n\r\n" + // more than one HT after ':'
+ "XXXXX",
+
+ "HTTP/1.1 407 Proxy Authorization Required\r\n" +
+ "Proxy-Authenticate: Basic realm=\"a fake realm\"\r\n\r\n",
+
+ "HTTP/1.1 401 Unauthorized\r\n" +
+ "WWW-Authenticate: Digest realm=\"wally land\" domain=/ " +
+ "nonce=\"2B7F3A2B\" qop=\"auth\"\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n\r\n", // no value
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n\r\n" + // no value, with response body
+ "Some Response Body",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n" + // no value, followed by another header
+ "Content-Length: 10\r\n\r\n" +
+ "Some Response Body",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n" + // no value, followed by another header, with response body
+ "Content-Length: 10\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo: chegar\r\n" +
+ "X-Foo: dfuchs\r\n" + // same header appears multiple times
+ "Content-Length: 0\r\n" +
+ "X-Foo: michaelm\r\n" +
+ "X-Foo: prappo\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "X-Foo:\r\n" + // no value, same header appears multiple times
+ "X-Foo: dfuchs\r\n" +
+ "Content-Length: 0\r\n" +
+ "X-Foo: michaelm\r\n" +
+ "X-Foo: prappo\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Accept-Ranges: bytes\r\n" +
+ "Cache-control: max-age=0, no-cache=\"set-cookie\"\r\n" +
+ "Content-Length: 132868\r\n" +
+ "Content-Type: text/html; charset=UTF-8\r\n" +
+ "Date: Sun, 05 Nov 2017 22:24:03 GMT\r\n" +
+ "Server: Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips Communique/4.2.2\r\n" +
+ "Set-Cookie: AWSELB=AF7927F5100F4202119876ED2436B5005EE;PATH=/;MAX-AGE=900\r\n" +
+ "Vary: Host,Accept-Encoding,User-Agent\r\n" +
+ "X-Mod-Pagespeed: 1.12.34.2-0\r\n" +
+ "Connection: keep-alive\r\n\r\n"
+ };
+ Arrays.stream(basic).forEach(responses::add);
+
+ String[] foldingTemplate =
+ { "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 9\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r'
+ " charset=UTF-8\r\n" + // one preceding SP
+ "Connection: close\r\n\r\n" +
+ "XXYYZZAABBCCDDEE",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 19\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ " charset=UTF-8\r\n" + // more than one preceding SP
+ "Connection: keep-alive\r\n\r\n" +
+ "XXYYZZAABBCCDDEEFFGG",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 999\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ "\tcharset=UTF-8\r\n" + // one preceding HT
+ "Connection: close\r\n\r\n" +
+ "XXYYZZAABBCCDDEE",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 54\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ "\t\t\tcharset=UTF-8\r\n" + // more than one preceding HT
+ "Connection: keep-alive\r\n\r\n" +
+ "XXYYZZAABBCCDDEEFFGG",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: -1\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ "\t \t \tcharset=UTF-8\r\n" + // mix of preceding HT and SP
+ "Connection: keep-alive\r\n\r\n" +
+ "XXYYZZAABBCCDDEEFFGGHH",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 65\r\n" +
+ "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
+ " \t \t charset=UTF-8\r\n" + // mix of preceding SP and HT
+ "Connection: keep-alive\r\n\r\n" +
+ "XXYYZZAABBCCDDEEFFGGHHII",
+
+ "HTTP/1.1 401 Unauthorized\r\n" +
+ "WWW-Authenticate: Digest realm=\"wally land\","
+ +"$NEWLINE domain=/,"
+ +"$NEWLINE nonce=\"2B7F3A2B\","
+ +"$NEWLINE\tqop=\"auth\"\r\n\r\n",
+
+ };
+ for (String newLineChar : new String[] { "\n", "\r", "\r\n" }) {
+ for (String template : foldingTemplate)
+ responses.add(template.replace("$NEWLINE", newLineChar));
+ }
+
+ String[] bad = // much of this is to retain parity with legacy MessageHeaders
+ { "HTTP/1.1 200 OK\r\n" +
+ "Connection:\r\n\r\n", // empty value, no body
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Connection:\r\n\r\n" + // empty value, with body
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ ": no header\r\n\r\n", // no/empty header-name, no body, no following header
+
+ "HTTP/1.1 200 OK\r\n" +
+ ": no; header\r\n" + // no/empty header-name, no body, following header
+ "Content-Length: 65\r\n\r\n",
+
+ "HTTP/1.1 200 OK\r\n" +
+ ": no header\r\n" + // no/empty header-name
+ "Content-Length: 65\r\n\r\n" +
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ ": no header\r\n\r\n" + // no/empty header-name, followed by header
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Conte\r" +
+ " nt-Length: 9\r\n" + // fold/bad header name ???
+ "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
+ "XXXXX",
+
+ "HTTP/1.1 200 OK\r\n" +
+ "Conte\r" +
+ "nt-Length: 9\r\n" + // fold/bad header name ??? without preceding space
+ "Content-Type: text/html; charset=UTF-8\r\n\r\n" +
+ "XXXXXYYZZ",
+
+ "HTTP/1.0 404 Not Found\r\n" +
+ "header-without-colon\r\n\r\n",
+
+ "HTTP/1.0 404 Not Found\r\n" +
+ "header-without-colon\r\n\r\n" +
+ "SOMEBODY",
+
+ };
+ Arrays.stream(bad).forEach(responses::add);
+
+ return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "responses")
+ public void verifyHeaders(String respString) throws Exception {
+ byte[] bytes = respString.getBytes(US_ASCII);
+ ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
+ MessageHeader m = new MessageHeader(bais);
+ Map<String,List<String>> messageHeaderMap = m.getHeaders();
+ int available = bais.available();
+
+ Http1HeaderParser decoder = new Http1HeaderParser();
+ ByteBuffer b = ByteBuffer.wrap(bytes);
+ decoder.parse(b);
+ Map<String,List<String>> decoderMap1 = decoder.headers().map();
+ assertEquals(available, b.remaining(),
+ "stream available not equal to remaining");
+
+ // assert status-line
+ String statusLine1 = messageHeaderMap.get(null).get(0);
+ String statusLine2 = decoder.statusLine();
+ if (statusLine1.startsWith("HTTP")) {// skip the case where MH's messes up the status-line
+ assertEquals(statusLine1, statusLine2, "Status-line not equal");
+ } else {
+ assertTrue(statusLine2.startsWith("HTTP/1."), "Status-line not HTTP/1.");
+ }
+
+ // remove the null'th entry with is the status-line
+ Map<String,List<String>> map = new HashMap<>();
+ for (Map.Entry<String,List<String>> e : messageHeaderMap.entrySet()) {
+ if (e.getKey() != null) {
+ map.put(e.getKey(), e.getValue());
+ }
+ }
+ messageHeaderMap = map;
+
+ assertHeadersEqual(messageHeaderMap, decoderMap1,
+ "messageHeaderMap not equal to decoderMap1");
+
+ // byte at a time
+ decoder = new Http1HeaderParser();
+ List<ByteBuffer> buffers = IntStream.range(0, bytes.length)
+ .mapToObj(i -> ByteBuffer.wrap(bytes, i, 1))
+ .collect(toList());
+ while (decoder.parse(buffers.remove(0)) != true);
+ Map<String,List<String>> decoderMap2 = decoder.headers().map();
+ assertEquals(available, buffers.size(),
+ "stream available not equals to remaining buffers");
+ assertEquals(decoderMap1, decoderMap2, "decoder maps not equal");
+ }
+
+ @DataProvider(name = "errors")
+ public Object[][] errors() {
+ List<String> responses = new ArrayList<>();
+
+ // These responses are parsed, somewhat, by MessageHeaders but give
+ // nonsensible results. They, correctly, fail with the Http1HeaderParser.
+ String[] bad =
+ {// "HTTP/1.1 402 Payment Required\r\n" +
+ // "Content-Length: 65\r\n\r", // missing trailing LF //TODO: incomplete
+
+ "HTTP/1.1 402 Payment Required\r\n" +
+ "Content-Length: 65\r\n\rT\r\n\r\nGGGGGG",
+
+ "HTTP/1.1 200OK\r\n\rT",
+
+ "HTTP/1.1 200OK\rT",
+ };
+ Arrays.stream(bad).forEach(responses::add);
+
+ return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
+ }
+
+ @Test(dataProvider = "errors", expectedExceptions = ProtocolException.class)
+ public void errors(String respString) throws ProtocolException {
+ byte[] bytes = respString.getBytes(US_ASCII);
+ Http1HeaderParser decoder = new Http1HeaderParser();
+ ByteBuffer b = ByteBuffer.wrap(bytes);
+ decoder.parse(b);
+ }
+
+ void assertHeadersEqual(Map<String,List<String>> expected,
+ Map<String,List<String>> actual,
+ String msg) {
+
+ if (expected.equals(actual))
+ return;
+
+ assertEquals(expected.size(), actual.size(),
+ format("%s. Expected size %d, actual size %s. %nexpected= %s,%n actual=%s.",
+ msg, expected.size(), actual.size(), mapToString(expected), mapToString(actual)));
+
+ for (Map.Entry<String,List<String>> e : expected.entrySet()) {
+ String key = e.getKey();
+ List<String> values = e.getValue();
+
+ boolean found = false;
+ for (Map.Entry<String,List<String>> other: actual.entrySet()) {
+ if (key.equalsIgnoreCase(other.getKey())) {
+ found = true;
+ List<String> otherValues = other.getValue();
+ assertEquals(values.size(), otherValues.size(),
+ format("%s. Expected list size %d, actual size %s",
+ msg, values.size(), otherValues.size()));
+ if (!(values.containsAll(otherValues) && otherValues.containsAll(values)))
+ assertTrue(false, format("Lists are unequal [%s] [%s]", values, otherValues));
+ break;
+ }
+ }
+ assertTrue(found, format("header name, %s, not found in %s", key, actual));
+ }
+ }
+
+ static String mapToString(Map<String,List<String>> map) {
+ StringBuilder sb = new StringBuilder();
+ List<String> sortedKeys = new ArrayList(map.keySet());
+ Collections.sort(sortedKeys);
+ for (String key : sortedKeys) {
+ List<String> values = map.get(key);
+ sb.append("\n\t" + key + " | " + values);
+ }
+ return sb.toString();
+ }
+
+ // ---
+
+ /* Main entry point for standalone testing of the main functional test. */
+ public static void main(String... args) throws Exception {
+ Http1HeaderParserTest test = new Http1HeaderParserTest();
+ int count = 0;
+ for (Object[] objs : test.responses()) {
+ out.println("Testing " + count++ + ", " + objs[0]);
+ test.verifyHeaders((String) objs[0]);
+ }
+ for (Object[] objs : test.errors()) {
+ out.println("Testing " + count++ + ", " + objs[0]);
+ try {
+ test.errors((String) objs[0]);
+ throw new RuntimeException("Expected ProtocolException for " + objs[0]);
+ } catch (ProtocolException expected) { /* Ok */ }
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/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)];
+ }
+}
--- /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
+ * }
+ * <p>
+ * 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<? super List<ByteBuffer>> 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<ByteBuffer> 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
+ * }
+ * <p>
+ * 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<Flow.Publisher<List<ByteBuffer>>> futurePublisher
+ = new CompletableFuture<>();
+ final ConcurrentLinkedQueue<Consumer<Flow.Subscriber<? super List<ByteBuffer>>>> queue
+ = new ConcurrentLinkedQueue<>();
+ AtomicReference<Flow.Subscriber<? super List<ByteBuffer>>> subscriberRef = new AtomicReference<>();
+ SequentialScheduler scheduler = SequentialScheduler.synchronizedScheduler(this::loop);
+ AtomicReference<Throwable> errorRef = new AtomicReference<>();
+ private volatile boolean finished;
+ private volatile boolean completed;
+
+
+ public void start(Flow.Publisher<List<ByteBuffer>> publisher,
+ Flow.Subscriber<? super List<ByteBuffer>> subscriber) {
+ subscriberRef.set(subscriber);
+ futurePublisher.complete(publisher);
+ scheduler.runOrSchedule();
+ }
+
+ @Override
+ public boolean isFinished() {
+ return finished;
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> 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<ByteBuffer> 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<? super List<ByteBuffer>> subscriber = subscriberRef.get();
+ if (subscriber == null) return;
+ try {
+ Consumer<Flow.Subscriber<? super List<ByteBuffer>>> 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<Object> 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<? super List<ByteBuffer>> subscriber;
+
+ private EchoTube(int maxBufferSize) {
+ if (maxBufferSize < 1)
+ throw new IllegalArgumentException();
+ this.maxQueueSize = maxBufferSize;
+ }
+
+ @Override
+ public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> 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<ByteBuffer> 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<ByteBuffer> bytes = (List<ByteBuffer>) 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;
+ }
+ }
+ };
+ }
+ }
+ }
--- /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<ByteBuffer> buffer;
+ private final Socket clientSock;
+ private final SSLSocket serverSock;
+ private final Thread thread1, thread2, thread3;
+ private volatile Flow.Subscription clientSubscription;
+ private final SubmissionPublisher<List<ByteBuffer>> 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<List<ByteBuffer>> returnSubscriber) {
+ publisher.subscribe(returnSubscriber);
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ clientSubscription = subscription;
+ clientSubscription.request(5);
+ }
+
+ @Override
+ public void onNext(List<ByteBuffer> 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<? super List<ByteBuffer>> subscriber) {
+ publisher.subscribe(subscriber);
+ }
+ }
+
+}
--- /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);
+ }
+ }
+}
--- /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<List<ByteBuffer>> publisher;
+ final SubscriberWrapper sub1, sub2, sub3;
+ final ExecutorService executor = Executors.newCachedThreadPool();
+ volatile int hipricount = 0;
+
+ void errorHandler(Flow.Subscriber<? super List<ByteBuffer>> 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<Void> 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<ByteBuffer> list, boolean complete) {
+ List<ByteBuffer> 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<List<ByteBuffer>> downstreamSubscriber)
+ {
+ super();
+ subscribe(downstreamSubscriber);
+ }
+
+ // Inserts up to NUM_HI_PRI hi priority buffers into flow
+ protected void incoming(List<ByteBuffer> 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<List<ByteBuffer>> {
+ volatile Flow.Subscription subscription;
+ volatile int hipriCounter=0;
+ volatile int lopriCounter=0;
+ final CompletableFuture<Void> cf;
+
+ LastSubscriber(CompletableFuture<Void> 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<ByteBuffer> 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<ByteBuffer> getBuffer(int c) {
+ ByteBuffer buf = ByteBuffer.allocate(BUFSIZE+4);
+ buf.putInt(LO_PRI);
+ for (int i=0; i<BUFSIZE_INT; i++) {
+ buf.putInt(c++);
+ }
+ buf.flip();
+ return List.of(buf);
+ }
+
+ boolean errorTest = false;
+
+ @Test
+ public void run() throws InterruptedException {
+ try {
+ CompletableFuture<Void> completion = sub3.completion();
+ publisher.subscribe(sub3);
+ // now submit a load of data
+ int counter = 0;
+ for (int i = 0; i < TOTAL; i++) {
+ List<ByteBuffer> 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();
+ }
+*/
+}
--- /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<Throwable> 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);
+ }
+}
--- /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<Object> 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<Object> 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<Object> mf = new MinimalFuture<>();
+ mf.completeExceptionally(new Throwable());
+
+ MinimalFuture<Object> 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()));
+ }
+}
--- /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<ByteBuffer> data1 = List.of(ByteBuffer.wrap("XXXX".getBytes(UTF_8)));
+ DataFrame dataFrame1 = new DataFrame(1, 0, data1);
+ List<ByteBuffer> data2 = List.of(ByteBuffer.wrap("YYYY".getBytes(UTF_8)));
+ DataFrame dataFrame2 = new DataFrame(1, 0, data2);
+
+ List<ByteBuffer> 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<ByteBuffer> 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<ByteBuffer> data1 = List.of(ByteBuffer.wrap("XXXX".getBytes(UTF_8)));
+ DataFrame dataFrame1 = new DataFrame(1, 0, data1);
+
+ List<ByteBuffer> 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<ByteBuffer> 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);
+ }
+}