/* * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsServer; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; import javax.net.ssl.SSLContext; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.util.FileUtils; import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.common.TestServerConfigurator; import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2Handler; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static java.lang.System.out; import static java.net.http.HttpResponse.BodyHandlers.ofFileDownload; import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.file.StandardOpenOption.*; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; /* * @test * @summary Basic test for ofFileDownload * @bug 8196965 8302475 * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext * jdk.test.lib.Platform jdk.test.lib.util.FileUtils * jdk.httpclient.test.lib.common.TestServerConfigurator * @run testng/othervm AsFileDownloadTest */ public class AsFileDownloadTest { SSLContext sslContext; HttpServer httpTestServer; // HTTP/1.1 [ 4 servers ] HttpsServer httpsTestServer; // HTTPS/1.1 Http2TestServer http2TestServer; // HTTP/2 ( h2c ) Http2TestServer https2TestServer; // HTTP/2 ( h2 ) String httpURI; String httpsURI; String http2URI; String https2URI; final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; Path tempDir; static final String[][] contentDispositionValues = new String[][] { // URI query Content-Type header value Expected filename { "001", "Attachment; filename=example001.html", "example001.html" }, { "002", "attachment; filename=example002.html", "example002.html" }, { "003", "ATTACHMENT; filename=example003.html", "example003.html" }, { "004", "attAChment; filename=example004.html", "example004.html" }, { "005", "attachmeNt; filename=example005.html", "example005.html" }, { "006", "attachment; Filename=example006.html", "example006.html" }, { "007", "attachment; FILENAME=example007.html", "example007.html" }, { "008", "attachment; fileName=example008.html", "example008.html" }, { "009", "attachment; fIlEnAMe=example009.html", "example009.html" }, { "010", "attachment; filename=Example010.html", "Example010.html" }, { "011", "attachment; filename=EXAMPLE011.html", "EXAMPLE011.html" }, { "012", "attachment; filename=eXample012.html", "eXample012.html" }, { "013", "attachment; filename=example013.HTML", "example013.HTML" }, { "014", "attachment; filename =eXaMpLe014.HtMl", "eXaMpLe014.HtMl"}, { "015", "attachment; filename=a", "a" }, { "016", "attachment; filename= b", "b" }, { "017", "attachment; filename= c", "c" }, { "018", "attachment; filename= d", "d" }, { "019", "attachment; filename=e ; filename*=utf-8''eee.txt", "e"}, { "020", "attachment; filename*=utf-8''fff.txt; filename=f", "f"}, { "021", "attachment; filename=g", "g" }, { "022", "attachment; filename= h", "h" }, { "023", "attachment; filename=\"space name\"", "space name" }, { "024", "attachment; filename=me.txt; filename*=utf-8''you.txt", "me.txt" }, { "025", "attachment; filename=\"m y.txt\"; filename*=utf-8''you.txt", "m y.txt" }, { "030", "attachment; filename=\"foo/file1.txt\"", "file1.txt" }, { "031", "attachment; filename=\"foo/bar/file2.txt\"", "file2.txt" }, { "032", "attachment; filename=\"baz\\\\file3.txt\"", "file3.txt" }, { "033", "attachment; filename=\"baz\\\\bar\\\\file4.txt\"", "file4.txt" }, { "034", "attachment; filename=\"x/y\\\\file5.txt\"", "file5.txt" }, { "035", "attachment; filename=\"x/y\\\\file6.txt\"", "file6.txt" }, { "036", "attachment; filename=\"x/y\\\\z/file7.txt\"", "file7.txt" }, { "037", "attachment; filename=\"x/y\\\\z/\\\\x/file8.txt\"", "file8.txt" }, { "038", "attachment; filename=\"/root/file9.txt\"", "file9.txt" }, { "039", "attachment; filename=\"../file10.txt\"", "file10.txt" }, { "040", "attachment; filename=\"..\\\\file11.txt\"", "file11.txt" }, { "041", "attachment; filename=\"foo/../../file12.txt\"", "file12.txt" }, }; @DataProvider(name = "positive") public Object[][] positive() { List list = new ArrayList<>(); Arrays.asList(contentDispositionValues).stream() .map(e -> new Object[] {httpURI + "?" + e[0], e[1], e[2]}) .forEach(list::add); Arrays.asList(contentDispositionValues).stream() .map(e -> new Object[] {httpsURI + "?" + e[0], e[1], e[2]}) .forEach(list::add); Arrays.asList(contentDispositionValues).stream() .map(e -> new Object[] {http2URI + "?" + e[0], e[1], e[2]}) .forEach(list::add); Arrays.asList(contentDispositionValues).stream() .map(e -> new Object[] {https2URI + "?" + e[0], e[1], e[2]}) .forEach(list::add); return list.stream().toArray(Object[][]::new); } @Test(dataProvider = "positive") void test(String uriString, String contentDispositionValue, String expectedFilename) throws Exception { out.printf("test(%s, %s, %s): starting", uriString, contentDispositionValue, expectedFilename); HttpClient client = HttpClient.newBuilder().sslContext(sslContext).build(); TRACKER.track(client); ReferenceQueue queue = new ReferenceQueue<>(); WeakReference ref = new WeakReference<>(client, queue); try { URI uri = URI.create(uriString); HttpRequest request = HttpRequest.newBuilder(uri) .POST(BodyPublishers.ofString("May the luck of the Irish be with you!")) .build(); BodyHandler bh = ofFileDownload(tempDir.resolve(uri.getPath().substring(1)), CREATE, TRUNCATE_EXISTING, WRITE); HttpResponse response = client.send(request, bh); Path body = response.body(); out.println("Got response: " + response); out.println("Got body Path: " + body); String fileContents = new String(Files.readAllBytes(response.body()), UTF_8); out.println("Got body: " + fileContents); assertEquals(response.statusCode(), 200); assertEquals(body.getFileName().toString(), expectedFilename); assertTrue(response.headers().firstValue("Content-Disposition").isPresent()); assertEquals(response.headers().firstValue("Content-Disposition").get(), contentDispositionValue); assertEquals(fileContents, "May the luck of the Irish be with you!"); if (!body.toAbsolutePath().startsWith(tempDir.toAbsolutePath())) { System.out.println("Tempdir = " + tempDir.toAbsolutePath()); System.out.println("body = " + body.toAbsolutePath()); throw new AssertionError("body in wrong location"); } // additional checks unrelated to file download caseInsensitivityOfHeaders(request.headers()); caseInsensitivityOfHeaders(response.headers()); } finally { client = null; System.gc(); while (!ref.refersTo(null)) { System.gc(); if (queue.remove(100) == ref) break; } AssertionError failed = TRACKER.checkShutdown(1000); if (failed != null) throw failed; } } // --- Negative static final String[][] contentDispositionBADValues = new String[][] { // URI query Content-Type header value { "100", "" }, // empty { "101", "filename=example.html" }, // no attachment { "102", "attachment; filename=space name" }, // unquoted with space { "103", "attachment; filename=" }, // empty filename param { "104", "attachment; filename=\"" }, // single quote { "105", "attachment; filename=\"\"" }, // empty quoted { "106", "attachment; filename=." }, // dot { "107", "attachment; filename=.." }, // dot dot { "108", "attachment; filename=\".." }, // badly quoted dot dot { "109", "attachment; filename=\"..\"" }, // quoted dot dot { "110", "attachment; filename=\"bad" }, // badly quoted { "111", "attachment; filename=\"bad;" }, // badly quoted with ';' { "112", "attachment; filename=\"bad ;" }, // badly quoted with ' ;' { "113", "attachment; filename*=utf-8''xx.txt "}, // no "filename" param { "120", "<>" }, // header not present }; @DataProvider(name = "negative") public Object[][] negative() { List list = new ArrayList<>(); Arrays.asList(contentDispositionBADValues).stream() .map(e -> new Object[] {httpURI + "?" + e[0], e[1]}) .forEach(list::add); Arrays.asList(contentDispositionBADValues).stream() .map(e -> new Object[] {httpsURI + "?" + e[0], e[1]}) .forEach(list::add); Arrays.asList(contentDispositionBADValues).stream() .map(e -> new Object[] {http2URI + "?" + e[0], e[1]}) .forEach(list::add); Arrays.asList(contentDispositionBADValues).stream() .map(e -> new Object[] {https2URI + "?" + e[0], e[1]}) .forEach(list::add); return list.stream().toArray(Object[][]::new); } @Test(dataProvider = "negative") void negativeTest(String uriString, String contentDispositionValue) throws Exception { out.printf("negativeTest(%s, %s): starting", uriString, contentDispositionValue); HttpClient client = HttpClient.newBuilder().sslContext(sslContext).build(); TRACKER.track(client); ReferenceQueue queue = new ReferenceQueue<>(); WeakReference ref = new WeakReference<>(client, queue); try { URI uri = URI.create(uriString); HttpRequest request = HttpRequest.newBuilder(uri) .POST(BodyPublishers.ofString("Does not matter")) .build(); BodyHandler bh = ofFileDownload(tempDir, CREATE, TRUNCATE_EXISTING, WRITE); try { HttpResponse response = client.send(request, bh); fail("UNEXPECTED response: " + response + ", path:" + response.body()); } catch (UncheckedIOException | IOException ioe) { System.out.println("Caught expected: " + ioe); } } finally { client = null; System.gc(); while (!ref.refersTo(null)) { System.gc(); if (queue.remove(100) == ref) break; } AssertionError failed = TRACKER.checkShutdown(1000); if (failed != null) throw failed; } } // -- Infrastructure static String serverAuthority(HttpServer server) { final String hostIP = InetAddress.getLoopbackAddress().getHostAddress(); // escape for ipv6 final String h = hostIP.contains(":") ? "[" + hostIP + "]" : hostIP; return h + ":" + server.getAddress().getPort(); } @BeforeTest public void setup() throws Exception { tempDir = Paths.get("asFileDownloadTest.tmp.dir"); if (Files.exists(tempDir)) throw new AssertionError("Unexpected test work dir existence: " + tempDir.toString()); Files.createDirectory(tempDir); // Unique dirs per test run, based on the URI path Files.createDirectories(tempDir.resolve("http1/afdt/")); Files.createDirectories(tempDir.resolve("https1/afdt/")); Files.createDirectories(tempDir.resolve("http2/afdt/")); Files.createDirectories(tempDir.resolve("https2/afdt/")); // HTTP/1.1 server logging in case of security exceptions ( uncomment if needed ) //Logger logger = Logger.getLogger("com.sun.net.httpserver"); //ConsoleHandler ch = new ConsoleHandler(); //logger.setLevel(Level.ALL); //ch.setLevel(Level.ALL); //logger.addHandler(ch); sslContext = new SimpleSSLContext().get(); if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); httpTestServer = HttpServer.create(sa, 0); httpTestServer.createContext("/http1/afdt", new Http1FileDispoHandler()); httpURI = "http://" + serverAuthority(httpTestServer) + "/http1/afdt"; httpsTestServer = HttpsServer.create(sa, 0); httpsTestServer.setHttpsConfigurator(new TestServerConfigurator(sa.getAddress(), sslContext)); httpsTestServer.createContext("/https1/afdt", new Http1FileDispoHandler()); httpsURI = "https://" + serverAuthority(httpsTestServer) + "/https1/afdt"; http2TestServer = new Http2TestServer("localhost", false, 0); http2TestServer.addHandler(new Http2FileDispoHandler(), "/http2/afdt"); http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/afdt"; https2TestServer = new Http2TestServer("localhost", true, sslContext); https2TestServer.addHandler(new Http2FileDispoHandler(), "/https2/afdt"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/afdt"; httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); } @AfterTest public void teardown() throws Exception { httpTestServer.stop(0); httpsTestServer.stop(0); http2TestServer.stop(); https2TestServer.stop(); if (Files.exists(tempDir)) { // clean up FileUtils.deleteFileTreeWithRetry(tempDir); } } static String contentDispositionValueFromURI(URI uri) { String queryIndex = uri.getQuery(); String[][] values; if (queryIndex.startsWith("0")) // positive tests start with '0' values = contentDispositionValues; else if (queryIndex.startsWith("1")) // negative tests start with '1' values = contentDispositionBADValues; else throw new AssertionError("SERVER: UNEXPECTED query:" + queryIndex); return Arrays.asList(values).stream() .filter(e -> e[0].equals(queryIndex)) .map(e -> e[1]) .findFirst() .orElseThrow(); } static class Http1FileDispoHandler implements HttpHandler { @Override public void handle(HttpExchange t) throws IOException { try (InputStream is = t.getRequestBody(); OutputStream os = t.getResponseBody()) { byte[] bytes = is.readAllBytes(); String value = contentDispositionValueFromURI(t.getRequestURI()); if (!value.equals("<>")) t.getResponseHeaders().set("Content-Disposition", value); t.sendResponseHeaders(200, bytes.length); os.write(bytes); } } } static class Http2FileDispoHandler implements Http2Handler { @Override public void handle(Http2TestExchange t) throws IOException { try (InputStream is = t.getRequestBody(); OutputStream os = t.getResponseBody()) { byte[] bytes = is.readAllBytes(); String value = contentDispositionValueFromURI(t.getRequestURI()); if (!value.equals("<>")) t.getResponseHeaders().addHeader("Content-Disposition", value); t.sendResponseHeaders(200, bytes.length); os.write(bytes); } } } // --- // Asserts case-insensitivity of headers (nothing to do with file // download, just convenient as we have a couple of header instances. ) static void caseInsensitivityOfHeaders(HttpHeaders headers) { try { for (Map.Entry> entry : headers.map().entrySet()) { String headerName = entry.getKey(); List headerValue = entry.getValue(); for (String name : List.of(headerName.toUpperCase(Locale.ROOT), headerName.toLowerCase(Locale.ROOT))) { assertTrue(headers.firstValue(name).isPresent()); assertEquals(headers.firstValue(name).get(), headerValue.get(0)); assertEquals(headers.allValues(name).size(), headerValue.size()); assertEquals(headers.allValues(name), headerValue); assertEquals(headers.map().get(name).size(), headerValue.size()); assertEquals(headers.map().get(name), headerValue); } } } catch (Throwable t) { System.out.println("failure in caseInsensitivityOfHeaders with:" + headers); throw t; } } }