1 /* 2 * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 import java.io.IOException; 25 import java.net.ProxySelector; 26 import java.net.URI; 27 import java.net.http.HttpClient; 28 import java.net.http.HttpClient.Version; 29 import java.net.http.HttpRequest; 30 import java.net.http.HttpResponse; 31 import java.net.http.HttpResponse.BodyHandlers; 32 import java.nio.charset.StandardCharsets; 33 import java.util.Arrays; 34 import java.util.Base64; 35 import java.util.List; 36 import java.util.stream.Collectors; 37 import java.util.stream.Stream; 38 import javax.net.ssl.SSLContext; 39 import jdk.test.lib.net.SimpleSSLContext; 40 41 import static java.lang.System.out; 42 43 /** 44 * @test 45 * @bug 8262027 46 * @summary Verify that it's possible to handle proxy authentication manually 47 * even when using an HTTPS tunnel. This test uses an authenticating 48 * proxy (basic auth) serving an authenticated server (basic auth). 49 * The test also helps verifying the fix for 8262027. 50 * @library /test/lib http2/server 51 * @build jdk.test.lib.net.SimpleSSLContext HttpServerAdapters ProxyServer HttpsTunnelAuthTest 52 * @modules java.net.http/jdk.internal.net.http.common 53 * java.net.http/jdk.internal.net.http.frame 54 * java.net.http/jdk.internal.net.http.hpack 55 * java.logging 56 * java.base/sun.net.www.http 57 * java.base/sun.net.www 58 * java.base/sun.net 59 * @run main/othervm -Djdk.httpclient.HttpClient.log=requests,headers,errors 60 * -Djdk.http.auth.tunneling.disabledSchemes 61 * -Djdk.httpclient.allowRestrictedHeaders=connection 62 * -Djdk.internal.httpclient.debug=true 63 * HttpsTunnelAuthTest 64 * 65 */ 66 //-Djdk.internal.httpclient.debug=true -Dtest.debug=true 67 public class HttpsTunnelAuthTest implements HttpServerAdapters, AutoCloseable { 68 69 static final String data[] = { 70 "Lorem ipsum", 71 "dolor sit amet", 72 "consectetur adipiscing elit, sed do eiusmod tempor", 73 "quis nostrud exercitation ullamco", 74 "laboris nisi", 75 "ut", 76 "aliquip ex ea commodo consequat." + 77 "Duis aute irure dolor in reprehenderit in voluptate velit esse" + 78 "cillum dolore eu fugiat nulla pariatur.", 79 "Excepteur sint occaecat cupidatat non proident." 80 }; 81 82 static final SSLContext context; 83 static { 84 try { 85 context = new SimpleSSLContext().get(); 86 SSLContext.setDefault(context); 87 } catch (Exception x) { 88 throw new ExceptionInInitializerError(x); 89 } 90 } 91 92 final String realm = "earth"; 93 final String sUserName = "arthur"; 94 final String pUserName = "porpoise"; 95 final String sPassword = "dent"; 96 final String pPassword = "fish"; 97 final String proxyAuth = "Basic " + Base64.getEncoder().withoutPadding() 98 .encodeToString((pUserName+":"+pPassword).getBytes(StandardCharsets.US_ASCII)); 99 final String serverAuth = "Basic " + Base64.getEncoder().withoutPadding() 100 .encodeToString((sUserName+":"+sPassword).getBytes(StandardCharsets.UTF_8)); 101 final DigestEchoServer.HttpTestAuthenticator testAuth = 102 new DigestEchoServer.HttpTestAuthenticator(realm, sUserName); 103 104 DigestEchoServer http1Server; 105 DigestEchoServer https1Server; 106 DigestEchoServer https2Server; 107 ProxyServer proxy; 108 ProxySelector proxySelector; 109 HttpClient client; 110 HttpsTunnelAuthTest()111 HttpsTunnelAuthTest() { 112 } 113 setUp()114 void setUp() throws IOException { 115 // Creates an HTTP/1.1 Server that will authenticate for 116 // arthur with password dent 117 http1Server = DigestEchoServer.createServer(Version.HTTP_1_1, 118 "http", 119 DigestEchoServer.HttpAuthType.SERVER, 120 testAuth, 121 DigestEchoServer.HttpAuthSchemeType.BASICSERVER, 122 new HttpTestEchoHandler(), 123 "/"); 124 125 // Creates a TLS HTTP/1.1 Server that will authenticate for 126 // arthur with password dent 127 https1Server = DigestEchoServer.createServer(Version.HTTP_1_1, 128 "https", 129 DigestEchoServer.HttpAuthType.SERVER, 130 testAuth, 131 DigestEchoServer.HttpAuthSchemeType.BASICSERVER, 132 new HttpTestEchoHandler(), 133 "/"); 134 135 // Creates a TLS HTTP/2 Server that will authenticate for 136 // arthur with password dent 137 https2Server = DigestEchoServer.createServer(Version.HTTP_2, 138 "https", 139 DigestEchoServer.HttpAuthType.SERVER, 140 testAuth, 141 DigestEchoServer.HttpAuthSchemeType.BASICSERVER, 142 new HttpTestEchoHandler(), "/"); 143 144 // Creates a proxy server that will authenticate for 145 // porpoise with password fish. 146 proxy = new ProxyServer(0, true, pUserName, pPassword); 147 148 // Creates a proxy selector that unconditionally select the 149 // above proxy. 150 var ps = proxySelector = ProxySelector.of(proxy.getProxyAddress()); 151 152 // Creates a client that uses the above proxy selector 153 client = newHttpClient(ps); 154 } 155 156 @Override close()157 public void close() throws Exception { 158 if (proxy != null) close(proxy::stop); 159 if (http1Server != null) close(http1Server::stop); 160 if (https1Server != null) close(https1Server::stop); 161 if (https2Server != null) close(https2Server::stop); 162 } 163 close(AutoCloseable closeable)164 private void close(AutoCloseable closeable) { 165 try { 166 closeable.close(); 167 } catch (Exception x) { 168 // OK. 169 } 170 } 171 newHttpClient(ProxySelector ps)172 public HttpClient newHttpClient(ProxySelector ps) { 173 HttpClient.Builder builder = HttpClient 174 .newBuilder() 175 .sslContext(context) 176 .proxy(ps); 177 return builder.build(); 178 } 179 main(String[] args)180 public static void main(String[] args) throws Exception { 181 try (HttpsTunnelAuthTest test = new HttpsTunnelAuthTest()) { 182 test.setUp(); 183 184 // tests proxy and server authentication through: 185 // - plain proxy connection to plain HTTP/1.1 server, 186 test.test(Version.HTTP_1_1, "http", "/foo/http1"); 187 188 // can't really test plain proxy connection to plain HTTP/2 server: 189 // this is not supported: we downgrade to HTTP/1.1 in that case 190 // so that is actually somewhat equivalent to the first case: 191 // therefore we will use a new client to force re-authentication 192 // of the proxy connection. 193 test.client = test.newHttpClient(test.proxySelector); 194 test.test(Version.HTTP_2, "http", "/foo/http2"); 195 196 // - proxy tunnel SSL connection to HTTP/1.1 server 197 test.test(Version.HTTP_1_1, "https", "/foo/https1"); 198 199 // - proxy tunnel SSl connection to HTTP/2 server 200 test.test(Version.HTTP_2, "https", "/foo/https2"); 201 } 202 } 203 server(String scheme, Version version)204 DigestEchoServer server(String scheme, Version version) { 205 return switch (scheme) { 206 case "https" -> secure(version); 207 case "http" -> unsecure(version); 208 default -> throw new IllegalArgumentException(scheme); 209 }; 210 } 211 unsecure(Version version)212 DigestEchoServer unsecure(Version version) { 213 return switch (version) { 214 // when accessing HTTP/2 through a proxy we downgrade to HTTP/1.1 215 case HTTP_1_1, HTTP_2 -> http1Server; 216 default -> throw new IllegalArgumentException(String.valueOf(version)); 217 }; 218 } 219 220 DigestEchoServer secure(Version version) { 221 return switch (version) { 222 case HTTP_1_1 -> https1Server; 223 case HTTP_2 -> https2Server; 224 default -> throw new IllegalArgumentException(String.valueOf(version)); 225 }; 226 } 227 228 Version expectedVersion(String scheme, Version version) { 229 // when trying to send a plain HTTP/2 request through a proxy 230 // it should be downgraded to HTTP/1 231 return "http".equals(scheme) ? Version.HTTP_1_1 : version; 232 } 233 234 public void test(Version version, String scheme, String path) throws Exception { 235 System.out.printf("%nTesting %s, %s, %s%n", version, scheme, path); 236 DigestEchoServer server = server(scheme, version); 237 try { 238 239 URI uri = jdk.test.lib.net.URIBuilder.newBuilder() 240 .scheme(scheme) 241 .host("localhost") 242 .port(server.getServerAddress().getPort()) 243 .path(path).build(); 244 245 out.println("Proxy is: " + proxySelector.select(uri)); 246 247 List<String> lines = List.of(Arrays.copyOfRange(data, 0, data.length)); 248 assert lines.size() == data.length; 249 String body = lines.stream().collect(Collectors.joining("\r\n")); 250 HttpRequest.BodyPublisher reqBody = HttpRequest.BodyPublishers.ofString(body); 251 252 // Build first request, with no authorization header 253 HttpRequest.Builder req1Builder = HttpRequest 254 .newBuilder(uri) 255 .version(Version.HTTP_2) 256 .POST(reqBody); 257 HttpRequest req1 = req1Builder.build(); 258 out.printf("%nPosting to %s server at: %s%n", expectedVersion(scheme, version), req1); 259 260 // send first request, with no authorization: we expect 407 261 HttpResponse<Stream<String>> response = client.send(req1, BodyHandlers.ofLines()); 262 out.println("Checking response: " + response); 263 if (response.body() != null) response.body().sequential().forEach(out::println); 264 265 // check that we got 407, and check that we got the expected 266 // Proxy-Authenticate header 267 if (response.statusCode() != 407) { 268 throw new RuntimeException("Unexpected status code: " + response); 269 } 270 var pAuthenticate = response.headers().firstValue("proxy-authenticate").get(); 271 if (!pAuthenticate.equals("Basic realm=\"proxy realm\"")) { 272 throw new RuntimeException("Unexpected proxy-authenticate: " + pAuthenticate); 273 } 274 275 // Second request will have Proxy-Authorization, no Authorization. 276 // We should get 401 from the server this time. 277 out.printf("%nPosting with Proxy-Authorization to %s server at: %s%n", expectedVersion(scheme, version), req1); 278 HttpRequest authReq1 = HttpRequest.newBuilder(req1, (k, v)-> true) 279 .header("proxy-authorization", proxyAuth).build(); 280 response = client.send(authReq1, BodyHandlers.ofLines()); 281 out.println("Checking response: " + response); 282 if (response.body() != null) response.body().sequential().forEach(out::println); 283 284 // Check that we have 401, and that we got the expected 285 // WWW-Authenticate header 286 if (response.statusCode() != 401) { 287 throw new RuntimeException("Unexpected status code: " + response); 288 } 289 var sAuthenticate = response.headers().firstValue("www-authenticate").get(); 290 if (!sAuthenticate.startsWith("Basic realm=\"earth\"")) { 291 throw new RuntimeException("Unexpected authenticate: " + sAuthenticate); 292 } 293 294 // Third request has both Proxy-Authorization and Authorization, 295 // so we now expect 200 296 out.printf("%nPosting with Authorization to %s server at: %s%n", expectedVersion(scheme, version), req1); 297 HttpRequest authReq2 = HttpRequest.newBuilder(authReq1, (k, v)-> true) 298 .header("authorization", serverAuth).build(); 299 response = client.send(authReq2, BodyHandlers.ofLines()); 300 out.println("Checking response: " + response); 301 302 // Check that we have 200 and the expected body echoed back. 303 // Check that the response version is as expected too. 304 if (response.statusCode() != 200) { 305 throw new RuntimeException("Unexpected status code: " + response); 306 } 307 308 if (response.version() != expectedVersion(scheme, version)) { 309 throw new RuntimeException("Unexpected protocol version: " 310 + response.version()); 311 } 312 List<String> respLines = response.body().collect(Collectors.toList()); 313 if (!lines.equals(respLines)) { 314 throw new RuntimeException("Unexpected response 1: " + respLines); 315 } 316 } catch(Throwable t) { 317 out.println("Unexpected exception: exiting: " + t); 318 t.printStackTrace(out); 319 throw t; 320 } 321 } 322 323 } 324