Path: blob/master/test/jdk/java/net/httpclient/CookieHeaderTest.java
41149 views
/*1* Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation.7*8* This code is distributed in the hope that it will be useful, but WITHOUT9* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or10* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License11* version 2 for more details (a copy is included in the LICENSE file that12* accompanied this code).13*14* You should have received a copy of the GNU General Public License version15* 2 along with this work; if not, write to the Free Software Foundation,16* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.17*18* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA19* or visit www.oracle.com if you need additional information or have any20* questions.21*/2223/*24* @test25* @bug 819985126* @summary Test for multiple vs single cookie header for HTTP/2 vs HTTP/1.127* @modules java.base/sun.net.www.http28* java.net.http/jdk.internal.net.http.common29* java.net.http/jdk.internal.net.http.frame30* java.net.http/jdk.internal.net.http.hpack31* java.logging32* jdk.httpserver33* @library /test/lib http2/server34* @build Http2TestServer35* @build jdk.test.lib.net.SimpleSSLContext36* @run testng/othervm37* -Djdk.tls.acknowledgeCloseNotify=true38* -Djdk.httpclient.HttpClient.log=trace,headers,requests39* CookieHeaderTest40*/4142import com.sun.net.httpserver.HttpServer;43import com.sun.net.httpserver.HttpsConfigurator;44import com.sun.net.httpserver.HttpsServer;45import jdk.test.lib.net.SimpleSSLContext;46import org.testng.annotations.AfterTest;47import org.testng.annotations.BeforeTest;48import org.testng.annotations.DataProvider;49import org.testng.annotations.Test;5051import javax.net.ServerSocketFactory;52import javax.net.ssl.SSLContext;53import java.io.IOException;54import java.io.InputStream;55import java.io.OutputStream;56import java.io.OutputStreamWriter;57import java.io.PrintWriter;58import java.io.Writer;59import java.net.CookieHandler;60import java.net.CookieManager;61import java.net.InetAddress;62import java.net.InetSocketAddress;63import java.net.ServerSocket;64import java.net.Socket;65import java.net.URI;66import java.net.http.HttpClient;67import java.net.http.HttpClient.Redirect;68import java.net.http.HttpRequest;69import java.net.http.HttpResponse;70import java.net.http.HttpResponse.BodyHandlers;71import java.util.ArrayList;72import java.util.Arrays;73import java.util.Collections;74import java.util.HashMap;75import java.util.List;76import java.util.Locale;77import java.util.Map;78import java.util.StringTokenizer;79import java.util.concurrent.ConcurrentHashMap;80import java.util.concurrent.ConcurrentLinkedQueue;81import java.util.concurrent.atomic.AtomicLong;82import java.util.stream.Collectors;83import java.util.stream.Stream;8485import static java.lang.System.out;86import static java.nio.charset.StandardCharsets.UTF_8;87import static org.testng.Assert.assertEquals;88import static org.testng.Assert.assertTrue;8990public class CookieHeaderTest implements HttpServerAdapters {9192SSLContext sslContext;93HttpTestServer httpTestServer; // HTTP/1.1 [ 6 servers ]94HttpTestServer httpsTestServer; // HTTPS/1.195HttpTestServer http2TestServer; // HTTP/2 ( h2c )96HttpTestServer https2TestServer; // HTTP/2 ( h2 )97DummyServer httpDummyServer;98DummyServer httpsDummyServer;99String httpURI;100String httpsURI;101String http2URI;102String https2URI;103String httpDummy;104String httpsDummy;105106static final String MESSAGE = "Basic CookieHeaderTest message body";107static final int ITERATIONS = 3;108static final long start = System.nanoTime();109public static String now() {110long now = System.nanoTime() - start;111long secs = now / 1000_000_000;112long mill = (now % 1000_000_000) / 1000_000;113long nan = now % 1000_000;114return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);115}116117@DataProvider(name = "positive")118public Object[][] positive() {119return new Object[][] {120{ httpURI, HttpClient.Version.HTTP_1_1 },121{ httpsURI, HttpClient.Version.HTTP_1_1 },122{ httpDummy, HttpClient.Version.HTTP_1_1 },123{ httpsDummy, HttpClient.Version.HTTP_1_1 },124{ httpURI, HttpClient.Version.HTTP_2 },125{ httpsURI, HttpClient.Version.HTTP_2 },126{ httpDummy, HttpClient.Version.HTTP_2 },127{ httpsDummy, HttpClient.Version.HTTP_2 },128{ http2URI, null },129{ https2URI, null },130};131}132133static final AtomicLong requestCounter = new AtomicLong();134135@Test(dataProvider = "positive")136void test(String uriString, HttpClient.Version version) throws Exception {137out.printf("%n---- starting (%s) ----%n", uriString);138ConcurrentHashMap<String, List<String>> cookieHeaders139= new ConcurrentHashMap<>();140CookieHandler cookieManager = new TestCookieHandler(cookieHeaders);141HttpClient client = HttpClient.newBuilder()142.followRedirects(Redirect.ALWAYS)143.cookieHandler(cookieManager)144.sslContext(sslContext)145.build();146assert client.cookieHandler().isPresent();147148URI uri = URI.create(uriString);149List<String> cookies = new ArrayList<>();150cookies.add("CUSTOMER=ARTHUR_DENT");151cookies.add("LOCATION=TR\u0100IN_STATION");152cookies.add("LOC\u0100TION=TRAIN_STATION");153cookies.add("ORDER=BISCUITS");154cookieHeaders.put("Cookie", cookies);155156HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(uri)157.header("X-uuid", "uuid-" + requestCounter.incrementAndGet());158if (version != null) {159requestBuilder.version(version);160}161HttpRequest request = requestBuilder.build();162out.println("Initial request: " + request.uri());163164for (int i=0; i< ITERATIONS; i++) {165out.println("iteration: " + i);166HttpResponse<String> response = client.send(request, BodyHandlers.ofString());167168out.println(" Got response: " + response);169out.println(" Got body Path: " + response.body());170171assertEquals(response.statusCode(), 200);172assertEquals(response.body(), MESSAGE);173assertEquals(response.headers().allValues("X-Request-Cookie"),174cookies.stream()175.filter(s -> !s.startsWith("LOC"))176.collect(Collectors.toList()));177requestBuilder = HttpRequest.newBuilder(uri)178.header("X-uuid", "uuid-" + requestCounter.incrementAndGet());179if (version != null) {180requestBuilder.version(version);181}182request = requestBuilder.build();183}184}185186// -- Infrastructure187188@BeforeTest189public void setup() throws Exception {190sslContext = new SimpleSSLContext().get();191if (sslContext == null)192throw new AssertionError("Unexpected null sslContext");193194InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);195196httpTestServer = HttpTestServer.of(HttpServer.create(sa, 0));197httpTestServer.addHandler(new CookieValidationHandler(), "/http1/cookie/");198httpURI = "http://" + httpTestServer.serverAuthority() + "/http1/cookie/retry";199HttpsServer httpsServer = HttpsServer.create(sa, 0);200httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));201httpsTestServer = HttpTestServer.of(httpsServer);202httpsTestServer.addHandler(new CookieValidationHandler(),"/https1/cookie/");203httpsURI = "https://" + httpsTestServer.serverAuthority() + "/https1/cookie/retry";204205http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));206http2TestServer.addHandler(new CookieValidationHandler(), "/http2/cookie/");207http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/cookie/retry";208https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));209https2TestServer.addHandler(new CookieValidationHandler(), "/https2/cookie/");210https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/retry";211212213// DummyServer214httpDummyServer = DummyServer.create(sa);215httpsDummyServer = DummyServer.create(sa, sslContext);216httpDummy = "http://" + httpDummyServer.serverAuthority() + "/http1/dummy/x";217httpsDummy = "https://" + httpsDummyServer.serverAuthority() + "/https1/dummy/x";218219httpTestServer.start();220httpsTestServer.start();221http2TestServer.start();222https2TestServer.start();223httpDummyServer.start();224httpsDummyServer.start();225}226227@AfterTest228public void teardown() throws Exception {229httpTestServer.stop();230httpsTestServer.stop();231http2TestServer.stop();232https2TestServer.stop();233httpsDummyServer.stopServer();234httpsDummyServer.stopServer();235}236237static class TestCookieHandler extends CookieHandler {238239final ConcurrentHashMap<String, List<String>> cookies;240TestCookieHandler(ConcurrentHashMap<String, List<String>> map) {241this.cookies = map;242}243244@Override245public Map<String, List<String>> get(URI uri, Map<String, List<String>> requestHeaders)246throws IOException247{248return cookies;249}250251@Override252public void put(URI uri, Map<String, List<String>> responseHeaders)253throws IOException254{255// do nothing256}257}258259static class CookieValidationHandler implements HttpTestHandler {260ConcurrentHashMap<String,String> closedRequests = new ConcurrentHashMap<>();261262@Override263public void handle(HttpTestExchange t) throws IOException {264System.out.println("CookieValidationHandler for: " + t.getRequestURI());265266List<String> uuids = t.getRequestHeaders().get("X-uuid");267if (uuids == null || uuids.size() != 1) {268readAllRequestData(t);269try (OutputStream os = t.getResponseBody()) {270String msg = "Incorrect uuid header values:[" + uuids + "]";271(new RuntimeException(msg)).printStackTrace();272t.sendResponseHeaders(500, -1);273os.write(msg.getBytes(UTF_8));274}275return;276}277278String uuid = uuids.get(0);279// retrying280if (closedRequests.putIfAbsent(uuid, t.getRequestURI().toString()) == null) {281if (t.getExchangeVersion() == HttpClient.Version.HTTP_1_1) {282// Throwing an exception here only causes a retry283// with HTTP_1_1 - where it forces the server to close284// the connection.285// For HTTP/2 then throwing an IOE would cause the server286// to close the stream, and throwing anything else would287// cause it to close the connection, but neither would288// cause the client to retry.289// So we simply do not try to retry with HTTP/2 and just verify290// we have received the expected cookie291throw new IOException("Closing on first request");292}293}294295// Check whether this request was upgraded.296// An upgraded request will have a version of HTTP_2 and297// an Upgrade: h2c header298HttpClient.Version version = t.getExchangeVersion();299List<String> upgrade = t.getRequestHeaders().get("Upgrade");300if (upgrade == null) upgrade = List.of();301boolean upgraded = version == HttpClient.Version.HTTP_2302&& upgrade.stream().anyMatch("h2c"::equalsIgnoreCase);303304// not retrying305readAllRequestData(t);306try (OutputStream os = t.getResponseBody()) {307List<String> cookie = t.getRequestHeaders().get("Cookie");308if (cookie != null) {309if (version == HttpClient.Version.HTTP_1_1 || upgraded) {310if (cookie.size() == 1) {311cookie = List.of(cookie.get(0).split("; "));312} else if (cookie.size() > 1) {313String msg = "Found multiple 'Cookie:' lines for version=%s (upgraded=%s): %s";314msg = String.format(msg, version, upgraded, cookie);315(new RuntimeException(msg)).printStackTrace();316t.sendResponseHeaders(500, -1);317os.write(msg.getBytes(UTF_8));318return;319}320}321Collections.sort(cookie = new ArrayList<String>(cookie));322}323if (cookie == null || cookie.size() == 0) {324String msg = "No cookie header present";325(new RuntimeException(msg)).printStackTrace();326t.sendResponseHeaders(500, -1);327os.write(msg.getBytes(UTF_8));328} else if (!cookie.get(0).equals("CUSTOMER=ARTHUR_DENT")) {329String msg = "Incorrect cookie header value:[" + cookie.get(0) + "]";330(new RuntimeException(msg)).printStackTrace();331t.sendResponseHeaders(500, -1);332os.write(msg.getBytes(UTF_8));333} else if (cookie.size() == 2 && !cookie.get(1).equals("ORDER=BISCUITS")) {334String msg = "Incorrect cookie header value:[" + cookie.get(0) + "]";335(new RuntimeException(msg)).printStackTrace();336t.sendResponseHeaders(500, -1);337os.write(msg.getBytes(UTF_8));338} else if (cookie.size() != 2) {339String msg = "Incorrect cookie header values:[" + cookie + "]";340(new RuntimeException(msg)).printStackTrace();341t.sendResponseHeaders(500, -1);342os.write(msg.getBytes(UTF_8));343} else {344assert cookie.get(0).equals("CUSTOMER=ARTHUR_DENT");345byte[] bytes = MESSAGE.getBytes(UTF_8);346for (String value : cookie) {347t.getResponseHeaders().addHeader("X-Request-Cookie", value);348}349t.sendResponseHeaders(200, bytes.length);350os.write(bytes);351}352} finally {353closedRequests.remove(uuid);354}355356}357}358359static void readAllRequestData(HttpTestExchange t) throws IOException {360try (InputStream is = t.getRequestBody()) {361is.readAllBytes();362}363}364365static class DummyServer extends Thread {366final ServerSocket ss;367final boolean secure;368ConcurrentLinkedQueue<Socket> connections = new ConcurrentLinkedQueue<>();369volatile boolean stopped;370DummyServer(ServerSocket ss, boolean secure) {371super("DummyServer[" + ss.getLocalPort()+"]");372this.secure = secure;373this.ss = ss;374}375376// This is a bit shaky. It doesn't handle continuation377// lines, but our client shouldn't send any.378// Read a line from the input stream, swallowing the final379// \r\n sequence. Stops at the first \n, doesn't complain380// if it wasn't preceded by '\r'.381//382String readLine(InputStream r) throws IOException {383StringBuilder b = new StringBuilder();384int c;385while ((c = r.read()) != -1) {386if (c == '\n') break;387b.appendCodePoint(c);388}389if (b.codePointAt(b.length() -1) == '\r') {390b.delete(b.length() -1, b.length());391}392return b.toString();393}394395@Override396public void run() {397try {398while(!stopped) {399Socket clientConnection = ss.accept();400connections.add(clientConnection);401System.out.println(now() + getName() + ": Client accepted");402StringBuilder headers = new StringBuilder();403Socket targetConnection = null;404InputStream ccis = clientConnection.getInputStream();405OutputStream ccos = clientConnection.getOutputStream();406Writer w = new OutputStreamWriter(407clientConnection.getOutputStream(), "UTF-8");408PrintWriter pw = new PrintWriter(w);409System.out.println(now() + getName() + ": Reading request line");410String requestLine = readLine(ccis);411System.out.println(now() + getName() + ": Request line: " + requestLine);412413StringTokenizer tokenizer = new StringTokenizer(requestLine);414String method = tokenizer.nextToken();415assert method.equalsIgnoreCase("POST") || method.equalsIgnoreCase("GET");416String path = tokenizer.nextToken();417URI uri;418try {419String hostport = serverAuthority();420uri = new URI((secure ? "https" : "http") +"://" + hostport + path);421} catch (Throwable x) {422System.err.printf("Bad target address: \"%s\" in \"%s\"%n",423path, requestLine);424clientConnection.close();425continue;426}427428// Read all headers until we find the empty line that429// signals the end of all headers.430String line = requestLine;431String cookies = null;432while (!line.equals("")) {433System.out.println(now() + getName() + ": Reading header: "434+ (line = readLine(ccis)));435if (line.startsWith("Cookie:")) {436if (cookies == null) cookies = line;437else cookies = cookies + "\n" + line;438}439headers.append(line).append("\r\n");440}441442StringBuilder response = new StringBuilder();443StringBuilder xheaders = new StringBuilder();444445int index = headers.toString()446.toLowerCase(Locale.US)447.indexOf("content-length: ");448if (index >= 0) {449index = index + "content-length: ".length();450String cl = headers.toString().substring(index);451StringTokenizer tk = new StringTokenizer(cl);452int len = Integer.parseInt(tk.nextToken());453System.out.println(now() + getName()454+ ": received body: "455+ new String(ccis.readNBytes(len), UTF_8));456}457String resp = MESSAGE;458String status = "200 OK";459if (cookies == null) {460resp = "No cookies found in headers";461status = "500 Internal Server Error";462} else if (cookies.contains("\n")) {463resp = "More than one 'Cookie:' line found: "464+ Arrays.asList(cookies.split("\n"));465status = "500 Internal Server Error";466} else {467List<String> values =468Stream.of(cookies.substring("Cookie:".length()).trim().split("; "))469.map(String::trim)470.collect(Collectors.toList());471Collections.sort(values);472if (values.size() != 2) {473resp = "Bad cookie list: " + values;474status = "500 Internal Server Error";475} else if (!values.get(0).equals("CUSTOMER=ARTHUR_DENT")) {476resp = "Unexpected cookie: " + values.get(0) + " in " + values;477status = "500 Internal Server Error";478} else if (!values.get(1).equals("ORDER=BISCUITS")) {479resp = "Unexpected cookie: " + values.get(1) + " in " + values;480status = "500 Internal Server Error";481} else {482for (String cookie : values) {483xheaders.append("X-Request-Cookie: ")484.append(cookie)485.append("\r\n");486}487}488}489byte[] b = resp.getBytes(UTF_8);490System.out.println(now()491+ getName() + ": sending back " + uri);492493response.append("HTTP/1.1 ")494.append(status)495.append("\r\nContent-Length: ")496.append(b.length)497.append("\r\n")498.append(xheaders)499.append("\r\n");500501// Then send the 200 OK response to the client502System.out.println(now() + getName() + ": Sending "503+ response);504pw.print(response);505pw.flush();506ccos.write(b);507ccos.flush();508ccos.close();509connections.remove(clientConnection);510clientConnection.close();511}512} catch (Throwable t) {513if (!stopped) {514System.out.println(now() + getName() + ": failed: " + t);515t.printStackTrace();516try {517stopServer();518} catch (Throwable e) {519520}521}522} finally {523System.out.println(now() + getName() + ": exiting");524}525}526527void close(Socket s) {528try {529s.close();530} catch(Throwable t) {531532}533}534public void stopServer() throws IOException {535stopped = true;536ss.close();537connections.forEach(this::close);538}539540public String serverAuthority() {541return InetAddress.getLoopbackAddress().getHostName() + ":"542+ ss.getLocalPort();543}544545static DummyServer create(InetSocketAddress sa) throws IOException {546ServerSocket ss = ServerSocketFactory.getDefault()547.createServerSocket(sa.getPort(), -1, sa.getAddress());548return new DummyServer(ss, false);549}550551static DummyServer create(InetSocketAddress sa, SSLContext sslContext) throws IOException {552ServerSocket ss = sslContext.getServerSocketFactory()553.createServerSocket(sa.getPort(), -1, sa.getAddress());554return new DummyServer(ss, true);555}556557558}559}560561562