--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLTunnelConnection.java Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLTunnelConnection.java Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -45,10 +45,11 @@
AsyncSSLTunnelConnection(InetSocketAddress addr,
HttpClientImpl client,
String[] alpn,
- InetSocketAddress proxy)
+ InetSocketAddress proxy,
+ HttpHeaders proxyHeaders)
{
super(addr, client, Utils.getServerName(addr), alpn);
- this.plainConnection = new PlainTunnelingConnection(addr, proxy, client);
+ this.plainConnection = new PlainTunnelingConnection(addr, proxy, client, proxyHeaders);
this.writePublisher = new PlainHttpPublisher();
}
@@ -70,6 +71,9 @@
}
@Override
+ boolean isTunnel() { return true; }
+
+ @Override
boolean connected() {
return plainConnection.connected(); // && sslDelegate.connected();
}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AuthenticationFilter.java Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AuthenticationFilter.java Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -26,10 +26,12 @@
package jdk.incubator.http;
import java.io.IOException;
+import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.net.InetSocketAddress;
import java.net.URISyntaxException;
+import java.net.URL;
import java.util.Base64;
import java.util.LinkedList;
import java.util.Objects;
@@ -72,6 +74,7 @@
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(),
@@ -80,11 +83,21 @@
uri.getScheme(),
realm,
authscheme,
- uri.toURL(),
+ 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) {
@@ -221,6 +234,9 @@
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");
@@ -242,6 +258,10 @@
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) {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -37,6 +37,8 @@
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
+import java.util.function.Function;
+
import jdk.incubator.http.internal.common.MinimalFuture;
import jdk.incubator.http.internal.common.Utils;
import jdk.incubator.http.internal.common.Log;
@@ -65,6 +67,8 @@
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;
@@ -119,6 +123,12 @@
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());
@@ -131,6 +141,7 @@
* other cases.
*/
public CompletableFuture<Void> ignoreBody() {
+ if (bodyIgnored != null) return bodyIgnored;
return exchImpl.ignoreBody();
}
@@ -287,45 +298,92 @@
}
}
+ // 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);
+ 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");
- return establishExchange(connection)
- .thenCompose((ex) -> ex.sendHeadersAsync())
- .thenCompose(v -> exchImpl.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);
- }
- });
+ // wait for 100-Continue before sending body
+ after407Check = this::expectContinue;
} else {
- CompletableFuture<Response> cf = establishExchange(connection)
- .thenCompose((ex) -> ex.sendHeadersAsync())
- .thenCompose(ExchangeImpl::sendBodyAsync)
- .thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
- cf = wrapForUpgrade(cf);
- cf = wrapForLog(cf);
- return cf;
+ // 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) {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Exchange.java Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Exchange.java Fri Jan 26 16:46:52 2018 +0000
@@ -82,9 +82,9 @@
private final Http1Publisher writePublisher = new Http1Publisher();
/** Completed when the header have been published, or there is an error */
- private volatile CompletableFuture<ExchangeImpl<T>> headersSentCF = new MinimalFuture<>();
+ private final CompletableFuture<ExchangeImpl<T>> headersSentCF = new MinimalFuture<>();
/** Completed when the body has been published, or there is an error */
- private volatile CompletableFuture<ExchangeImpl<T>> bodySentCF = new MinimalFuture<>();
+ private final CompletableFuture<ExchangeImpl<T>> bodySentCF = new MinimalFuture<>();
/** The subscriber to the request's body published. Maybe null. */
private volatile Http1BodySubscriber bodySubscriber;
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -35,6 +35,7 @@
import java.net.InetSocketAddress;
import java.util.Objects;
import java.util.concurrent.Flow;
+import java.util.function.Predicate;
import jdk.incubator.http.Http1Exchange.Http1BodySubscriber;
import jdk.incubator.http.internal.common.HttpHeadersImpl;
@@ -82,14 +83,26 @@
}
private void collectHeaders0(StringBuilder sb) {
- collectHeaders1(sb, systemHeaders);
- collectHeaders1(sb, userHeaders);
+ Predicate<String> filter = connection.isTunnel()
+ ? Utils.NO_PROXY_HEADER : Utils.ALL_HEADERS;
+
+ // 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) {
+ private void collectHeaders1(StringBuilder sb, HttpHeaders headers, Predicate<String> filter) {
for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) {
String key = entry.getKey();
+ if (!filter.test(key)) continue;
List<String> values = entry.getValue();
for (String value : values) {
sb.append(key).append(": ").append(value).append("\r\n");
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -34,6 +34,7 @@
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;
@@ -186,7 +187,7 @@
if (version == HTTP_2) {
alpn = new String[] { "h2", "http/1.1" };
}
- return getSSLConnection(addr, proxy, alpn, client);
+ return getSSLConnection(addr, proxy, alpn, request, client);
}
}
}
@@ -194,13 +195,26 @@
private static HttpConnection getSSLConnection(InetSocketAddress addr,
InetSocketAddress proxy,
String[] alpn,
+ HttpRequestImpl request,
HttpClientImpl client) {
if (proxy != null)
- return new AsyncSSLTunnelConnection(addr, client, alpn, proxy);
+ return new AsyncSSLTunnelConnection(addr, client, alpn, proxy,
+ proxyHeaders(request));
else
return new AsyncSSLConnection(addr, client, alpn);
}
+ // Composes a new immutable HttpHeaders that combines the
+ // user and system header but only keeps those headers that
+ // start with "proxy-"
+ private static HttpHeaders proxyHeaders(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-*
+ return ImmutableHeaders.of(combined, Utils.IS_PROXY_HEADER);
+ }
+
/* Returns either a plain HTTP connection or a plain tunnelling connection
* for proxied WebSocket */
private static HttpConnection getPlainConnection(InetSocketAddress addr,
@@ -208,7 +222,8 @@
HttpRequestImpl request,
HttpClientImpl client) {
if (request.isWebSocket() && proxy != null)
- return new PlainTunnelingConnection(addr, proxy, client);
+ return new PlainTunnelingConnection(addr, proxy, client,
+ proxyHeaders(request));
if (proxy == null)
return new PlainHttpConnection(addr, client);
@@ -243,6 +258,9 @@
}
}
+ /* Tells whether or not this connection is a tunnel through a proxy */
+ boolean isTunnel() { return false; }
+
abstract SocketChannel channel();
final InetSocketAddress address() {
@@ -251,11 +269,6 @@
abstract ConnectionPool.CacheKey cacheKey();
-// // overridden in SSL only
-// SSLParameters sslParameters() {
-// return null;
-// }
-
/**
* Closes this connection, by returning the socket to its connection pool.
*/
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpHeaders.java Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpHeaders.java Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -156,7 +156,7 @@
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
- sb.append(super.toString()).append(" ");
+ sb.append(super.toString()).append(" { ");
sb.append(map());
sb.append(" }");
return sb.toString();
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -140,13 +140,14 @@
}
/* used for creating CONNECT requests */
- HttpRequestImpl(String method, InetSocketAddress authority) {
+ 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.empty();
+ this.userHeaders = ImmutableHeaders.of(headers);
this.uri = URI.create("socket://" + authority.getHostString() + ":"
+ Integer.toString(authority.getPort()) + "/");
this.proxy = null;
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponseImpl.java Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponseImpl.java Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -66,31 +66,18 @@
this.sslParameters = exch.client().sslParameters();
this.uri = response.request().uri();
this.version = response.version();
- this.connection = exch.exchImpl.connection();
+ this.connection = connection(exch);
this.stream = null;
this.body = body;
}
-// // A response to a PUSH_PROMISE
-// public HttpResponseImpl(Response response,
-// HttpRequestImpl pushRequest,
-// ImmutableHeaders headers,
-// Stream<T> stream,
-// SSLParameters sslParameters,
-// T body) {
-// this.responseCode = response.statusCode();
-// this.exchange = null;
-// this.initialRequest = null; // ## fix this
-// this.finalRequest = pushRequest;
-// this.headers = headers;
-// //this.trailers = null;
-// this.sslParameters = sslParameters;
-// this.uri = finalRequest.uri(); // TODO: take from headers
-// this.version = HttpClient.Version.HTTP_2;
-// this.connection = stream.connection();
-// this.stream = stream;
-// 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;
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ImmutableHeaders.java Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ImmutableHeaders.java Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -47,6 +47,12 @@
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");
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -31,8 +31,11 @@
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+
import jdk.incubator.http.internal.common.FlowTube;
import jdk.incubator.http.internal.common.MinimalFuture;
+
import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
/**
@@ -43,14 +46,17 @@
final class PlainTunnelingConnection extends HttpConnection {
final PlainHttpConnection delegate;
+ final HttpHeaders proxyHeaders;
protected final InetSocketAddress proxyAddr;
private volatile boolean connected;
protected PlainTunnelingConnection(InetSocketAddress addr,
InetSocketAddress proxy,
- HttpClientImpl client) {
+ HttpClientImpl client,
+ HttpHeaders proxyHeaders) {
super(addr, client);
this.proxyAddr = proxy;
+ this.proxyHeaders = proxyHeaders;
delegate = new PlainHttpConnection(proxy, client);
}
@@ -62,8 +68,9 @@
debug.log(Level.DEBUG, "sending HTTP/1.1 CONNECT");
HttpClientImpl client = client();
assert client != null;
- HttpRequestImpl req = new HttpRequestImpl("CONNECT", address);
- MultiExchange<Void> mulEx = new MultiExchange<>(null, req, client, discard(null), null, null);
+ HttpRequestImpl req = new HttpRequestImpl("CONNECT", address, proxyHeaders);
+ MultiExchange<Void> mulEx = new MultiExchange<>(null, req,
+ client, discard(null), null, null);
Exchange<Void> connectExchange = new Exchange<>(req, mulEx);
return connectExchange
@@ -71,7 +78,18 @@
.thenCompose((Response resp) -> {
CompletableFuture<Void> cf = new MinimalFuture<>();
debug.log(Level.DEBUG, "got response: %d", resp.statusCode());
- if (resp.statusCode() != 200) {
+ 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 {
@@ -88,6 +106,9 @@
}
@Override
+ boolean isTunnel() { return true; }
+
+ @Override
HttpPublisher publisher() { return delegate.publisher(); }
@Override
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ProxyAuthenticationRequired.java Fri Jan 26 16:46:52 2018 +0000
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http;
+import java.io.IOException;
+
+/**
+ * Signals that a proxy has refused a CONNECT request with a
+ * 407 error code.
+ */
+final class ProxyAuthenticationRequired extends IOException {
+ private static final long serialVersionUID = 0;
+ final transient Response proxyResponse;
+
+ /**
+ * Constructs a {@code ConnectionExpiredException} with the specified detail
+ * message and cause.
+ *
+ * @param proxyResponse the response from the proxy
+ */
+ public ProxyAuthenticationRequired(Response proxyResponse) {
+ super("Proxy Authentication Required");
+ assert proxyResponse.statusCode() == 407;
+ this.proxyResponse = proxyResponse;
+ }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Stream.java Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Stream.java Fri Jan 26 16:46:52 2018 +0000
@@ -481,7 +481,9 @@
h.setHeader("content-length", Long.toString(contentLength));
}
setPseudoHeaderFields();
- OutgoingHeaders<Stream<T>> f = new OutgoingHeaders<>(h, request.getUserHeaders(), this);
+ 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;
@@ -489,6 +491,20 @@
return f;
}
+ private HttpHeaders filter(HttpHeaders headers) {
+ if (connection().isTunnel()) {
+ boolean needsFiltering = headers
+ .firstValue("proxy-authorization")
+ .isPresent();
+ // don't send proxy-* headers to the target server.
+ if (needsFiltering) {
+ return ImmutableHeaders.of(headers.map(),
+ Utils.NO_PROXY_HEADER);
+ }
+ }
+ return headers;
+ }
+
private void setPseudoHeaderFields() {
HttpHeadersImpl hdrs = requestPseudoHeaders;
String method = request.method();
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Utils.java Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Utils.java Fri Jan 26 16:46:52 2018 +0000
@@ -102,12 +102,19 @@
TreeSet<String> treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
treeSet.addAll(Set.of("connection", "content-length",
"date", "expect", "from", "host", "origin",
- "proxy-authorization", "referer", "upgrade",
+ "referer", "upgrade",
"via", "warning"));
DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet);
}
+
public static final Predicate<String>
- ALLOWED_HEADERS = header -> !Utils.DISALLOWED_HEADERS_SET.contains(header);
+ ALLOWED_HEADERS = header -> !DISALLOWED_HEADERS_SET.contains(header);
+
+ public static final Predicate<String> IS_PROXY_HEADER = (k) ->
+ k != null && k.length() > 6 && "proxy-".equalsIgnoreCase(k.substring(0,6));
+ public static final Predicate<String> NO_PROXY_HEADER =
+ IS_PROXY_HEADER.negate();
+ public static final Predicate<String> ALL_HEADERS = (s) -> true;
public static ByteBuffer getBuffer() {
return ByteBuffer.allocate(BUFSIZE);
--- a/test/jdk/java/net/httpclient/DigestEchoClient.java Fri Jan 26 11:08:42 2018 +0000
+++ b/test/jdk/java/net/httpclient/DigestEchoClient.java Fri Jan 26 16:46:52 2018 +0000
@@ -26,7 +26,6 @@
import java.math.BigInteger;
import java.net.ProxySelector;
import java.net.URI;
-import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@@ -35,7 +34,6 @@
import java.util.List;
import java.util.Optional;
import java.util.Random;
-import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
@@ -55,7 +53,7 @@
import sun.net.www.HeaderParser;
import static java.lang.System.out;
import static java.lang.String.format;
-import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
+import static jdk.incubator.http.HttpResponse.BodyHandler.asLines;
/**
* @test
@@ -151,6 +149,7 @@
throw new ExceptionInInitializerError(x);
}
}
+ static final List<Boolean> BOOLEANS = List.of(true, false);
final ServerSocketFactory factory;
final boolean useSSL;
@@ -228,7 +227,14 @@
authScheme,
authType);
for (Version version : HttpClient.Version.values()) {
- dec.testBasic(version, true);
+ for (boolean expectContinue : BOOLEANS) {
+ for (boolean async : BOOLEANS) {
+ for (boolean preemptive : BOOLEANS) {
+ dec.testBasic(version, async,
+ expectContinue, preemptive);
+ }
+ }
+ }
}
}
EnumSet<DigestEchoServer.HttpAuthSchemeType> digests =
@@ -238,7 +244,11 @@
authScheme,
authType);
for (Version version : HttpClient.Version.values()) {
- dec.testDigest(version, true);
+ for (boolean expectContinue : BOOLEANS) {
+ for (boolean async : BOOLEANS) {
+ dec.testDigest(version, async, expectContinue);
+ }
+ }
}
}
}
@@ -259,11 +269,18 @@
final static AtomicLong basics = new AtomicLong();
final static AtomicLong basicCount = new AtomicLong();
// @Test
- void testBasic(HttpClient.Version version, boolean async)
+ void testBasic(HttpClient.Version version, boolean async,
+ boolean expectContinue, boolean preemptive)
throws Exception
{
- out.println(format("*** testBasic: version: %s, async: %s, useSSL: %s, authScheme: %s, authType: %s ***",
- version, async, useSSL, authScheme, authType));
+ final boolean addHeaders = authScheme == DigestEchoServer.HttpAuthSchemeType.BASICSERVER;
+ // !preemptive has no meaning if we don't handle the authorization
+ // headers ourselves
+ if (!preemptive && !addHeaders) return;
+
+ out.println(format("*** testBasic: version: %s, async: %s, useSSL: %s, " +
+ "authScheme: %s, authType: %s, expectContinue: %s preemptive: %s***",
+ version, async, useSSL, authScheme, authType, expectContinue, preemptive));
DigestEchoServer server = EchoServers.of(useSSL ? "https" : "http", authType, authScheme);
URI uri = DigestEchoServer.uri(useSSL ? "https" : "http", server.getServerAddress(), "/foo/");
@@ -273,7 +290,6 @@
CompletableFuture<HttpResponse<String>> cf1;
String auth = null;
-
try {
for (int i=0; i<data.length; i++) {
out.println(DigestEchoServer.now() + " ----- iteration " + i + " -----");
@@ -282,22 +298,26 @@
String body = lines.stream().collect(Collectors.joining("\r\n"));
HttpRequest.BodyPublisher reqBody = HttpRequest.BodyPublisher.fromString(body);
HttpRequest.Builder builder = HttpRequest.newBuilder(uri).version(version)
- .POST(reqBody);
- final boolean addHeaders = authScheme == DigestEchoServer.HttpAuthSchemeType.BASICSERVER;
+ .POST(reqBody).expectContinue(expectContinue);
+ boolean isTunnel = isProxy(authType) && useSSL;
if (addHeaders) {
// handle authentication ourselves
assert !client.authenticator().isPresent();
if (auth == null) auth = "Basic " + getBasicAuth("arthur");
try {
- builder = builder.header(authorizationKey(authType), auth);
- if (isProxy(authType)) {
- throw new RuntimeException("Setting " + authorizationKey(authType)
- + " should have failed");
+ if ((i > 0 || preemptive) && (!isTunnel || i == 0)) {
+ // In case of a SSL tunnel through proxy then only the
+ // first request should require proxy authorization
+ // Though this might be invalidated if the server decides
+ // to close the connection...
+ out.println(String.format("%s adding %s: %s",
+ DigestEchoServer.now(),
+ authorizationKey(authType),
+ auth));
+ builder = builder.header(authorizationKey(authType), auth);
}
} catch (IllegalArgumentException x) {
- if (isProxy(authType)) {
- System.out.println("Got expected " + x);
- } else throw x;
+ throw x;
}
} else {
// let the stack do the authentication
@@ -308,9 +328,9 @@
HttpResponse<Stream<String>> resp;
try {
if (async) {
- resp = client.sendAsync(request, HttpResponse.BodyHandler.asLines()).join();
+ resp = client.sendAsync(request, asLines()).join();
} else {
- resp = client.send(request, HttpResponse.BodyHandler.asLines());
+ resp = client.send(request, asLines());
}
} catch (Throwable t) {
long stop = System.nanoTime();
@@ -323,25 +343,19 @@
assert t.getCause() != null;
t = t.getCause();
}
- // If we let the stack manage the authorisation, or if we
- // were not authenticating with a Proxy, then there should
- // have been no exception.
- if (!addHeaders || !isProxy(authType) || client.authenticator().isPresent()) {
- throw new RuntimeException("Unexpected exception: " + t, t);
- }
- // In the case of Proxy authentication with Basic, we should get
- // an IOException complaining that we haven't set an authenticator.
- if (t instanceof IOException && t.getMessage().contains("No authenticator set")) {
- System.out.println("Got expected exception: " + t);
- continue;
- }
throw new RuntimeException("Unexpected exception: " + t, t);
}
- if (isProxy(authType) && addHeaders) {
- assert resp.statusCode() == 407;
- continue;
+
+ if (addHeaders && !preemptive && i==0) {
+ assert resp.statusCode() == 401 || resp.statusCode() == 407;
+ request = HttpRequest.newBuilder(uri).version(version)
+ .POST(reqBody).header(authorizationKey(authType), auth).build();
+ if (async) {
+ resp = client.sendAsync(request, asLines()).join();
+ } else {
+ resp = client.send(request, asLines());
+ }
}
-
assert resp.statusCode() == 200;
List<String> respLines = resp.body().collect(Collectors.toList());
long stop = System.nanoTime();
@@ -370,11 +384,12 @@
final static AtomicLong digests = new AtomicLong();
final static AtomicLong digestCount = new AtomicLong();
// @Test
- void testDigest(HttpClient.Version version, boolean async)
+ void testDigest(HttpClient.Version version, boolean async, boolean expectContinue)
throws Exception
{
- out.println(format("*** testDigest: version: %s, async: %s, useSSL: %s, authScheme: %s, authType: %s ***",
- version, async, useSSL, authScheme, authType));
+ out.println(format("*** testDigest: version: %s, async: %s, useSSL: %s, " +
+ "authScheme: %s, authType: %s, expectContinue: %s ***",
+ version, async, useSSL, authScheme, authType, expectContinue));
DigestEchoServer server = EchoServers.of(useSSL ? "https" : "http", authType, authScheme);
URI uri = DigestEchoServer.uri(useSSL ? "https" : "http", server.getServerAddress(), "/foo/");
@@ -393,33 +408,43 @@
assert lines.size() == i + 1;
String body = lines.stream().collect(Collectors.joining("\r\n"));
HttpRequest.BodyPublisher reqBody = HttpRequest.BodyPublisher.fromString(body);
- HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(uri).version(version).POST(reqBody);
- if (challenge != null) {
+ HttpRequest.Builder reqBuilder = HttpRequest
+ .newBuilder(uri).version(version).POST(reqBody)
+ .expectContinue(expectContinue);
+
+ boolean isTunnel = isProxy(authType) && useSSL;
+ String digestMethod = isTunnel ? "CONNECT" : "POST";
+
+ // In case of a tunnel connection only the first request
+ // which establishes the tunnel needs to authenticate with
+ // the proxy.
+ if (challenge != null && !isTunnel) {
assert cnonceStr != null;
- String auth = digestResponse(uri, "POST", challenge, cnonceStr);
+ String auth = digestResponse(uri, digestMethod, challenge, cnonceStr);
try {
reqBuilder = reqBuilder.header(authorizationKey(authType), auth);
- if (isProxy(authType)) {
- throw new RuntimeException("Setting " + authorizationKey(authType)
- + " should have failed");
- }
} catch (IllegalArgumentException x) {
- if (isProxy(authType)) {
- System.out.println("Got expected " + x);
- } else throw x;
+ throw x;
}
}
+
long start = System.nanoTime();
HttpRequest request = reqBuilder.build();
HttpResponse<Stream<String>> resp;
if (async) {
- resp = client.sendAsync(request, HttpResponse.BodyHandler.asLines()).join();
+ resp = client.sendAsync(request, asLines()).join();
} else {
- resp = client.send(request, HttpResponse.BodyHandler.asLines());
+ resp = client.send(request, asLines());
}
System.out.println(resp);
assert challenge != null || resp.statusCode() == 401 || resp.statusCode() == 407;
if (resp.statusCode() == 401 || resp.statusCode() == 407) {
+ // This assert may need to be relaxed if our server happened to
+ // decide to close the tunnel connection, in which case we would
+ // receive 407 again...
+ assert challenge == null || !isTunnel
+ : "No proxy auth should be required after establishing an SSL tunnel";
+
System.out.println("Received " + resp.statusCode() + " answering challenge...");
random.nextBytes(cnonce);
cnonceStr = new BigInteger(1, cnonce).toString(16);
@@ -434,27 +459,20 @@
if (qop == null && nonce == null) {
throw new RuntimeException("QOP and NONCE not found");
}
- challenge =
- DigestEchoServer.DigestResponse.create(authenticate.substring("Digest ".length()));
- String auth = digestResponse(uri, "POST", challenge, cnonceStr);
+ challenge = DigestEchoServer.DigestResponse
+ .create(authenticate.substring("Digest ".length()));
+ String auth = digestResponse(uri, digestMethod, challenge, cnonceStr);
try {
request = HttpRequest.newBuilder(uri).version(version)
.POST(reqBody).header(authorizationKey(authType), auth).build();
- if (isProxy(authType)) {
- throw new RuntimeException("Setting " + authorizationKey(authType)
- + " should have failed");
- }
} catch (IllegalArgumentException x) {
- if (isProxy(authType)) {
- System.out.println("Got expected " + x);
- continue;
- } else throw x;
+ throw x;
}
if (async) {
- resp = client.sendAsync(request, HttpResponse.BodyHandler.asLines()).join();
+ resp = client.sendAsync(request, asLines()).join();
} else {
- resp = client.send(request, HttpResponse.BodyHandler.asLines());
+ resp = client.send(request, asLines());
}
System.out.println(resp);
}
--- a/test/jdk/java/net/httpclient/DigestEchoServer.java Fri Jan 26 11:08:42 2018 +0000
+++ b/test/jdk/java/net/httpclient/DigestEchoServer.java Fri Jan 26 16:46:52 2018 +0000
@@ -52,6 +52,7 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
+import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
@@ -59,12 +60,15 @@
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
+import java.util.Locale;
import java.util.Objects;
+import java.util.Optional;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import javax.net.ssl.SSLContext;
import sun.net.www.HeaderParser;
@@ -79,7 +83,10 @@
public static final boolean DEBUG =
Boolean.parseBoolean(System.getProperty("test.debug", "false"));
- public enum HttpAuthType { SERVER, PROXY, SERVER307, PROXY305 };
+ public enum HttpAuthType {
+ SERVER, PROXY, SERVER307, PROXY305
+ /* add PROXY_AND_SERVER and SERVER_PROXY_NONE */
+ };
public enum HttpAuthSchemeType { NONE, BASICSERVER, BASIC, DIGEST };
public static final HttpAuthType DEFAULT_HTTP_AUTH_TYPE = HttpAuthType.SERVER;
public static final String DEFAULT_PROTOCOL_TYPE = "https";
@@ -398,7 +405,7 @@
HttpServer impl = createHttpServer(protocol);
final DigestEchoServer server = new DigestEchoServer(impl, null, delegate);
- final HttpHandler hh = server.createHandler(schemeType, auth, authType);
+ final HttpHandler hh = server.createHandler(schemeType, auth, authType, false);
HttpContext ctxt = impl.createContext(path, hh);
server.configureAuthentication(ctxt, schemeType, auth, authType);
impl.start();
@@ -419,7 +426,10 @@
final DigestEchoServer server = "https".equalsIgnoreCase(protocol)
? new HttpsProxyTunnel(impl, null, delegate)
: new DigestEchoServer(impl, null, delegate);
- final HttpHandler hh = server.createHandler(schemeType, auth, authType);
+
+ final HttpHandler hh = server.createHandler(HttpAuthSchemeType.NONE,
+ null, HttpAuthType.SERVER,
+ server instanceof HttpsProxyTunnel);
HttpContext ctxt = impl.createContext(path, hh);
server.configureAuthentication(ctxt, schemeType, auth, authType);
impl.start();
@@ -492,11 +502,12 @@
private HttpHandler createHandler(HttpAuthSchemeType schemeType,
HttpTestAuthenticator auth,
- HttpAuthType authType) {
- return new HttpNoAuthHandler(authType);
+ HttpAuthType authType,
+ boolean tunelled) {
+ return new HttpNoAuthHandler(authType, tunelled);
}
- private void configureAuthentication(HttpContext ctxt,
+ void configureAuthentication(HttpContext ctxt,
HttpAuthSchemeType schemeType,
HttpTestAuthenticator auth,
HttpAuthType authType) {
@@ -994,13 +1005,35 @@
private class HttpNoAuthHandler extends AbstractHttpHandler {
- public HttpNoAuthHandler(HttpAuthType authType) {
+ // true if this server is behind a proxy tunnel.
+ final boolean tunnelled;
+ public HttpNoAuthHandler(HttpAuthType authType, boolean tunnelled) {
super(authType, authType == HttpAuthType.SERVER
? "NoAuth Server" : "NoAuth Proxy");
+ this.tunnelled = tunnelled;
}
@Override
protected void sendResponse(HttpExchange he) throws IOException {
+ if (DEBUG) {
+ System.out.println(type + ": headers are: "
+ + DigestEchoServer.toString(he.getRequestHeaders()));
+ }
+ if (authType == HttpAuthType.SERVER && tunnelled) {
+ // Verify that the client doesn't send us proxy-* headers
+ // used to establish the proxy tunnel
+ Optional<String> proxyAuth = he.getRequestHeaders()
+ .keySet().stream()
+ .filter("proxy-authorization"::equalsIgnoreCase)
+ .findAny();
+ if (proxyAuth.isPresent()) {
+ System.out.println(type + " found "
+ + proxyAuth.get() + ": failing!");
+ throw new IOException(proxyAuth.get()
+ + " found by " + type + " for "
+ + he.getRequestURI());
+ }
+ }
DigestEchoServer.this.writeResponse(he);
}
@@ -1057,6 +1090,172 @@
long nan = now % 1000_000;
return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);
}
+
+ static class ProxyAuthorization {
+ final HttpAuthSchemeType schemeType;
+ final HttpTestAuthenticator authenticator;
+ private final byte[] nonce;
+ private final String ns;
+
+ ProxyAuthorization(HttpAuthSchemeType schemeType, HttpTestAuthenticator auth) {
+ this.schemeType = schemeType;
+ this.authenticator = auth;
+ nonce = new byte[16];
+ new Random(Instant.now().toEpochMilli()).nextBytes(nonce);
+ ns = new BigInteger(1, nonce).toString(16);
+ }
+
+ String doBasic(Optional<String> authorization) {
+ String offset = "proxy-authorization: basic ";
+ String authstring = authorization.orElse("");
+ if (!authstring.toLowerCase(Locale.US).startsWith(offset)) {
+ return "Proxy-Authenticate: BASIC " + "realm=\""
+ + authenticator.getRealm() +"\"";
+ }
+ authstring = authstring
+ .substring(offset.length())
+ .trim();
+ byte[] base64 = Base64.getDecoder().decode(authstring);
+ String up = new String(base64, StandardCharsets.UTF_8);
+ int colon = up.indexOf(':');
+ if (colon < 1) {
+ return "Proxy-Authenticate: BASIC " + "realm=\""
+ + authenticator.getRealm() +"\"";
+ }
+ String u = up.substring(0, colon);
+ String p = up.substring(colon+1);
+ char[] pw = authenticator.getPassword(u);
+ if (!p.equals(new String(pw))) {
+ return "Proxy-Authenticate: BASIC " + "realm=\""
+ + authenticator.getRealm() +"\"";
+ }
+ System.out.println(now() + " Proxy basic authentication success");
+ return null;
+ }
+
+ String doDigest(Optional<String> authorization) {
+ String offset = "proxy-authorization: digest ";
+ String authstring = authorization.orElse("");
+ if (!authstring.toLowerCase(Locale.US).startsWith(offset)) {
+ return "Proxy-Authenticate: " +
+ "Digest realm=\"" + authenticator.getRealm() + "\","
+ + "\r\n qop=\"auth\","
+ + "\r\n nonce=\"" + ns +"\"";
+ }
+ authstring = authstring
+ .substring(offset.length())
+ .trim();
+ boolean validated = false;
+ try {
+ DigestResponse dgr = DigestResponse.create(authstring);
+ validated = validate("CONNECT", dgr);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ if (!validated) {
+ return "Proxy-Authenticate: " +
+ "Digest realm=\"" + authenticator.getRealm() + "\","
+ + "\r\n qop=\"auth\","
+ + "\r\n nonce=\"" + ns +"\"";
+ }
+ return null;
+ }
+
+
+
+
+ boolean validate(String reqMethod, DigestResponse dg) {
+ String type = now() + this.getClass().getSimpleName();
+ if (!"MD5".equalsIgnoreCase(dg.getAlgorithm("MD5"))) {
+ System.out.println(type + ": Unsupported algorithm "
+ + dg.algorithm);
+ return false;
+ }
+ if (!"auth".equalsIgnoreCase(dg.getQoP("auth"))) {
+ System.out.println(type + ": Unsupported qop "
+ + dg.qop);
+ return false;
+ }
+ try {
+ if (!dg.nonce.equals(ns)) {
+ System.out.println(type + ": bad nonce returned by client: "
+ + nonce + " expected " + ns);
+ return false;
+ }
+ if (dg.response == null) {
+ System.out.println(type + ": missing digest response.");
+ return false;
+ }
+ char[] pa = authenticator.getPassword(dg.username);
+ return verify(type, reqMethod, dg, pa);
+ } catch(IllegalArgumentException | SecurityException
+ | NoSuchAlgorithmException e) {
+ System.out.println(type + ": " + e.getMessage());
+ return false;
+ }
+ }
+
+
+ boolean verify(String type, String reqMethod, DigestResponse dg, char[] pw)
+ throws NoSuchAlgorithmException {
+ String response = DigestResponse.computeDigest(true, reqMethod, pw, dg);
+ if (!dg.response.equals(response)) {
+ System.out.println(type + ": bad response returned by client: "
+ + dg.response + " expected " + response);
+ return false;
+ } else {
+ // A real server would also verify the uri=<request-uri>
+ // parameter - but this is just a test...
+ System.out.println(type + ": verified response " + response);
+ }
+ return true;
+ }
+
+ public boolean authorize(StringBuilder response, String requestLine, String headers) {
+ String message = "<html><body><p>Authorization Failed%s</p></body></html>\r\n";
+ if (authenticator == null && schemeType != HttpAuthSchemeType.NONE) {
+ message = String.format(message, " No Authenticator Set");
+ response.append("HTTP/1.1 407 Proxy Authentication Failed\r\n");
+ response.append("Content-Length: ")
+ .append(message.getBytes(StandardCharsets.UTF_8).length)
+ .append("\r\n\r\n");
+ response.append(message);
+ return false;
+ }
+ Optional<String> authorization = Stream.of(headers.split("\r\n"))
+ .filter((k) -> k.toLowerCase(Locale.US).startsWith("proxy-authorization:"))
+ .findFirst();
+ String authenticate = null;
+ switch(schemeType) {
+ case BASIC:
+ case BASICSERVER:
+ authenticate = doBasic(authorization);
+ break;
+ case DIGEST:
+ authenticate = doDigest(authorization);
+ break;
+ case NONE:
+ response.append("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
+ return true;
+ default:
+ throw new InternalError("Unknown scheme type: " + schemeType);
+ }
+ if (authenticate != null) {
+ message = String.format(message, "");
+ response.append("HTTP/1.1 407 Proxy Authentication Required\r\n");
+ response.append("Content-Length: ")
+ .append(message.getBytes(StandardCharsets.UTF_8).length)
+ .append("\r\n")
+ .append(authenticate)
+ .append("\r\n\r\n");
+ response.append(message);
+ return false;
+ }
+ response.append("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
+ return true;
+ }
+ }
+
// This is a bit hacky: HttpsProxyTunnel is an HTTPTestServer hidden
// behind a fake proxy that only understands CONNECT requests.
// The fake proxy is just a server socket that intercept the
@@ -1067,6 +1266,7 @@
final ServerSocket ss;
final CopyOnWriteArrayList<CompletableFuture<Void>> connectionCFs
= new CopyOnWriteArrayList<>();
+ volatile ProxyAuthorization authorization;
volatile boolean stopped;
public HttpsProxyTunnel(HttpServer server, DigestEchoServer target,
HttpHandler delegate)
@@ -1095,6 +1295,27 @@
}
}
+
+ @Override
+ void configureAuthentication(HttpContext ctxt,
+ HttpAuthSchemeType schemeType,
+ HttpTestAuthenticator auth,
+ HttpAuthType authType) {
+ if (authType == HttpAuthType.PROXY || authType == HttpAuthType.PROXY305) {
+ authorization = new ProxyAuthorization(schemeType, auth);
+ } else {
+ super.configureAuthentication(ctxt, schemeType, auth, authType);
+ }
+ }
+
+ boolean authorize(StringBuilder response, String requestLine, String headers) {
+ if (authorization != null) {
+ return authorization.authorize(response, requestLine, headers);
+ }
+ response.append("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
+ return true;
+ }
+
// Pipe the input stream to the output stream.
private synchronized Thread pipe(InputStream is, OutputStream os, char tag, CompletableFuture<Void> end) {
return new Thread("TunnelPipe("+tag+")") {
@@ -1168,6 +1389,7 @@
break;
}
System.out.println(now() + "Tunnel: Client accepted");
+ StringBuilder headers = new StringBuilder();
Socket targetConnection = null;
InputStream ccis = clientConnection.getInputStream();
OutputStream ccos = clientConnection.getOutputStream();
@@ -1184,19 +1406,32 @@
// Read all headers until we find the empty line that
// signals the end of all headers.
- while(!requestLine.equals("")) {
+ String line = requestLine;
+ while(!line.equals("")) {
System.out.println(now() + "Tunnel: Reading header: "
- + (requestLine = readLine(ccis)));
+ + (line = readLine(ccis)));
+ headers.append(line).append("\r\n");
}
+ StringBuilder response = new StringBuilder();
+ final boolean authorize = authorize(response, requestLine, headers.toString());
+ if (!authorize) {
+ System.out.println(now() + "Tunnel: Sending "
+ + response);
+ // send the 407 response
+ pw.print(response.toString());
+ pw.flush();
+ toClose.close();
+ continue;
+ }
targetConnection = new Socket(
serverImpl.getAddress().getAddress(),
serverImpl.getAddress().getPort());
// Then send the 200 OK response to the client
System.out.println(now() + "Tunnel: Sending "
- + "HTTP/1.1 200 OK\r\n\r\n");
- pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
+ + response);
+ pw.print(response);
pw.flush();
} else {
// This should not happen. If it does let our serverImpl
--- a/test/jdk/java/net/httpclient/MultiAuthTest.java Fri Jan 26 11:08:42 2018 +0000
+++ b/test/jdk/java/net/httpclient/MultiAuthTest.java Fri Jan 26 16:46:52 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
@@ -176,6 +176,9 @@
resp = client.send(req, asString());
ok = resp.statusCode() == 200 &&
resp.body().equals(RESPONSE);
+ if (resp.statusCode() == 401 || resp.statusCode() == 407) {
+ throw new IOException(String.valueOf(resp));
+ }
if (expectFailure != null) {
throw new RuntimeException("Expected " + expectFailure.getName()
+" not raised");
--- a/test/jdk/java/net/httpclient/RequestBuilderTest.java Fri Jan 26 11:08:42 2018 +0000
+++ b/test/jdk/java/net/httpclient/RequestBuilderTest.java Fri Jan 26 16:46:52 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
@@ -315,15 +315,12 @@
private static final Set<String> RESTRICTED = Set.of("connection", "content-length",
"date", "expect", "from", "host", "origin",
"referer", "upgrade", "via", "warning",
- "proxy-authorization",
"Connection", "Content-Length",
"DATE", "eXpect", "frOm", "hosT", "origIN",
"ReFerer", "upgradE", "vIa", "Warning",
- "Proxy-Authorization",
"CONNection", "CONTENT-LENGTH",
"Date", "EXPECT", "From", "Host", "Origin",
- "Referer", "Upgrade", "Via", "WARNING",
- "PROXY-AUTHORIZATION");
+ "Referer", "Upgrade", "Via", "WARNING");
interface WithHeader {
HttpRequest.Builder withHeader(HttpRequest.Builder builder, String name, String value);