# HG changeset patch # User dfuchs # Date 1516985212 0 # Node ID b4b5e09ef3cc0547493c040c57e7591685c8f7a8 # Parent f8eabb9a5c0f53926b01548353deb7f9c562fcf6 http-client-branch: make it possible to supply proxy-authorization headers diff -r f8eabb9a5c0f -r b4b5e09ef3cc src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLTunnelConnection.java --- 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(); } diff -r f8eabb9a5c0f -r b4b5e09ef3cc src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AuthenticationFilter.java --- 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) { diff -r f8eabb9a5c0f -r b4b5e09ef3cc src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java --- 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 exchImpl; volatile CompletableFuture> exchangeCF; + volatile CompletableFuture bodyIgnored; + // used to record possible cancellation raised before the exchImpl // has been established. private volatile IOException failed; @@ -119,6 +123,12 @@ public CompletableFuture readBodyAsync(HttpResponse.BodyHandler handler) { + // If we received a 407 while establishing the exchange + // there will be no body to read: bodyIgnored will be true, + // and exchImpl will be null (if we were trying to establish + // an HTTP/2 tunnel through an HTTP/1.1 proxy) + if (bodyIgnored != null) return MinimalFuture.completedFuture(null); + // The connection will not be returned to the pool in the case of WebSocket return exchImpl.readBodyAsync(handler, !request.isWebSocket(), parentExecutor) .whenComplete((r,t) -> exchImpl.completed()); @@ -131,6 +141,7 @@ * other cases. */ public CompletableFuture 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 checkFor407(ExchangeImpl ex, Throwable t, + Function,CompletableFuture> andThen) { + t = Utils.getCompletionCause(t); + if (t instanceof ProxyAuthenticationRequired) { + bodyIgnored = MinimalFuture.completedFuture(null); + Response proxyResponse = ((ProxyAuthenticationRequired)t).proxyResponse; + Response syntheticResponse = new Response(request, this, + proxyResponse.headers, proxyResponse.statusCode, proxyResponse.version); + return MinimalFuture.completedFuture(syntheticResponse); + } else if (t != null) { + return MinimalFuture.failedFuture(t); + } else { + return andThen.apply(ex); + } + } + + // After sending the request headers, if no ProxyAuthorizationRequired + // was raised and the expectContinue flag is on, we need to wait + // for the 100-Continue response + private CompletableFuture expectContinue(ExchangeImpl ex) { + assert request.expectContinue(); + return ex.getResponseAsync(parentExecutor) + .thenCompose((Response r1) -> { + Log.logResponse(r1::toString); + int rcode = r1.statusCode(); + if (rcode == 100) { + Log.logTrace("Received 100-Continue: sending body"); + CompletableFuture cf = + exchImpl.sendBodyAsync() + .thenCompose(exIm -> exIm.getResponseAsync(parentExecutor)); + cf = wrapForUpgrade(cf); + cf = wrapForLog(cf); + return cf; + } else { + Log.logTrace("Expectation failed: Received {0}", + rcode); + if (upgrading && rcode == 101) { + IOException failed = new IOException( + "Unable to handle 101 while waiting for 100"); + return MinimalFuture.failedFuture(failed); + } + return exchImpl.readBodyAsync(this::ignoreBody, false, parentExecutor) + .thenApply(v -> r1); + } + }); + } + + // After sending the request headers, if no ProxyAuthorizationRequired + // was raised and the expectContinue flag is off, we can immediately + // send the request body and proceed. + private CompletableFuture sendRequestBody(ExchangeImpl ex) { + assert !request.expectContinue(); + CompletableFuture cf = ex.sendBodyAsync() + .thenCompose(exIm -> exIm.getResponseAsync(parentExecutor)); + cf = wrapForUpgrade(cf); + cf = wrapForLog(cf); + return cf; + } + CompletableFuture responseAsyncImpl0(HttpConnection connection) { + Function, CompletableFuture> after407Check; + bodyIgnored = null; if (request.expectContinue()) { request.addSystemHeader("Expect", "100-Continue"); Log.logTrace("Sending Expect: 100-Continue"); - 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 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 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, CompletableFuture> afterExch407Check = + (ex) -> ex.sendHeadersAsync() + .handle((r,t) -> this.checkFor407(r, t, after407Check)) + .thenCompose(Function.identity()); + return establishExchange(connection) + .handle((r,t) -> this.checkFor407(r,t, afterExch407Check)) + .thenCompose(Function.identity()); } private CompletableFuture wrapForUpgrade(CompletableFuture cf) { diff -r f8eabb9a5c0f -r b4b5e09ef3cc src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Exchange.java --- 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> headersSentCF = new MinimalFuture<>(); + private final CompletableFuture> headersSentCF = new MinimalFuture<>(); /** Completed when the body has been published, or there is an error */ - private volatile CompletableFuture> bodySentCF = new MinimalFuture<>(); + private final CompletableFuture> bodySentCF = new MinimalFuture<>(); /** The subscriber to the request's body published. Maybe null. */ private volatile Http1BodySubscriber bodySubscriber; diff -r f8eabb9a5c0f -r b4b5e09ef3cc src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java --- 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 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 filter) { for (Map.Entry> entry : headers.map().entrySet()) { String key = entry.getKey(); + if (!filter.test(key)) continue; List values = entry.getValue(); for (String value : values) { sb.append(key).append(": ").append(value).append("\r\n"); diff -r f8eabb9a5c0f -r b4b5e09ef3cc src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java --- 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> 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. */ diff -r f8eabb9a5c0f -r b4b5e09ef3cc src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpHeaders.java --- 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(); diff -r f8eabb9a5c0f -r b4b5e09ef3cc src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java --- 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; diff -r f8eabb9a5c0f -r b4b5e09ef3cc src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponseImpl.java --- 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 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; diff -r f8eabb9a5c0f -r b4b5e09ef3cc src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ImmutableHeaders.java --- 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> src, Predicate keyAllowed) { requireNonNull(src, "src"); diff -r f8eabb9a5c0f -r b4b5e09ef3cc src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java --- 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 mulEx = new MultiExchange<>(null, req, client, discard(null), null, null); + HttpRequestImpl req = new HttpRequestImpl("CONNECT", address, proxyHeaders); + MultiExchange mulEx = new MultiExchange<>(null, req, + client, discard(null), null, null); Exchange connectExchange = new Exchange<>(req, mulEx); return connectExchange @@ -71,7 +78,18 @@ .thenCompose((Response resp) -> { CompletableFuture 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 diff -r f8eabb9a5c0f -r b4b5e09ef3cc src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ProxyAuthenticationRequired.java --- /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; + } +} diff -r f8eabb9a5c0f -r b4b5e09ef3cc src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Stream.java --- 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> f = new OutgoingHeaders<>(h, request.getUserHeaders(), this); + HttpHeaders sysh = filter(h); + HttpHeaders userh = filter(request.getUserHeaders()); + OutgoingHeaders> f = new OutgoingHeaders<>(sysh, userh, this); if (contentLength == 0) { f.setFlag(HeadersFrame.END_STREAM); endStreamSent = true; @@ -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(); diff -r f8eabb9a5c0f -r b4b5e09ef3cc src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Utils.java --- 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 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 - ALLOWED_HEADERS = header -> !Utils.DISALLOWED_HEADERS_SET.contains(header); + ALLOWED_HEADERS = header -> !DISALLOWED_HEADERS_SET.contains(header); + + public static final Predicate IS_PROXY_HEADER = (k) -> + k != null && k.length() > 6 && "proxy-".equalsIgnoreCase(k.substring(0,6)); + public static final Predicate NO_PROXY_HEADER = + IS_PROXY_HEADER.negate(); + public static final Predicate ALL_HEADERS = (s) -> true; public static ByteBuffer getBuffer() { return ByteBuffer.allocate(BUFSIZE); diff -r f8eabb9a5c0f -r b4b5e09ef3cc test/jdk/java/net/httpclient/DigestEchoClient.java --- 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 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 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> cf1; String auth = null; - try { for (int i=0; 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> 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 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> 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); } diff -r f8eabb9a5c0f -r b4b5e09ef3cc test/jdk/java/net/httpclient/DigestEchoServer.java --- 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 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 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 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= + // 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 = "

Authorization Failed%s

\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 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> 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 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 diff -r f8eabb9a5c0f -r b4b5e09ef3cc test/jdk/java/net/httpclient/MultiAuthTest.java --- 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"); diff -r f8eabb9a5c0f -r b4b5e09ef3cc test/jdk/java/net/httpclient/RequestBuilderTest.java --- 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 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);