1 /* 2 * Copyright (c) 2018, 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 /* 25 * @test 26 * @summary Basic test for ofFileDownload 27 * @bug 8196965 28 * @modules java.base/sun.net.www.http 29 * java.net.http/jdk.internal.net.http.common 30 * java.net.http/jdk.internal.net.http.frame 31 * java.net.http/jdk.internal.net.http.hpack 32 * java.logging 33 * jdk.httpserver 34 * @library /test/lib http2/server 35 * @build Http2TestServer 36 * @build jdk.test.lib.net.SimpleSSLContext 37 * @build jdk.test.lib.Platform 38 * @build jdk.test.lib.util.FileUtils 39 * @run testng/othervm AsFileDownloadTest 40 * @run testng/othervm/java.security.policy=AsFileDownloadTest.policy AsFileDownloadTest 41 */ 42 43 import com.sun.net.httpserver.HttpExchange; 44 import com.sun.net.httpserver.HttpHandler; 45 import com.sun.net.httpserver.HttpServer; 46 import com.sun.net.httpserver.HttpsConfigurator; 47 import com.sun.net.httpserver.HttpsServer; 48 import java.io.IOException; 49 import java.io.InputStream; 50 import java.io.OutputStream; 51 import java.io.UncheckedIOException; 52 import java.net.InetAddress; 53 import java.net.InetSocketAddress; 54 import java.net.URI; 55 import java.net.http.HttpClient; 56 import java.net.http.HttpHeaders; 57 import java.net.http.HttpRequest; 58 import java.net.http.HttpRequest.BodyPublishers; 59 import java.net.http.HttpResponse; 60 import java.net.http.HttpResponse.BodyHandler; 61 import java.nio.file.Files; 62 import java.nio.file.Path; 63 import java.nio.file.Paths; 64 import java.util.ArrayList; 65 import java.util.Arrays; 66 import java.util.List; 67 import java.util.Locale; 68 import java.util.Map; 69 import javax.net.ssl.SSLContext; 70 import jdk.test.lib.net.SimpleSSLContext; 71 import jdk.test.lib.util.FileUtils; 72 import org.testng.annotations.AfterTest; 73 import org.testng.annotations.BeforeTest; 74 import org.testng.annotations.DataProvider; 75 import org.testng.annotations.Test; 76 import static java.lang.System.out; 77 import static java.net.http.HttpResponse.BodyHandlers.ofFileDownload; 78 import static java.nio.charset.StandardCharsets.UTF_8; 79 import static java.nio.file.StandardOpenOption.*; 80 import static org.testng.Assert.assertEquals; 81 import static org.testng.Assert.assertTrue; 82 import static org.testng.Assert.fail; 83 84 public class AsFileDownloadTest { 85 86 SSLContext sslContext; 87 HttpServer httpTestServer; // HTTP/1.1 [ 4 servers ] 88 HttpsServer httpsTestServer; // HTTPS/1.1 89 Http2TestServer http2TestServer; // HTTP/2 ( h2c ) 90 Http2TestServer https2TestServer; // HTTP/2 ( h2 ) 91 String httpURI; 92 String httpsURI; 93 String http2URI; 94 String https2URI; 95 96 Path tempDir; 97 98 static final String[][] contentDispositionValues = new String[][] { 99 // URI query Content-Type header value Expected filename 100 { "001", "Attachment; filename=example001.html", "example001.html" }, 101 { "002", "attachment; filename=example002.html", "example002.html" }, 102 { "003", "ATTACHMENT; filename=example003.html", "example003.html" }, 103 { "004", "attAChment; filename=example004.html", "example004.html" }, 104 { "005", "attachmeNt; filename=example005.html", "example005.html" }, 105 106 { "006", "attachment; Filename=example006.html", "example006.html" }, 107 { "007", "attachment; FILENAME=example007.html", "example007.html" }, 108 { "008", "attachment; fileName=example008.html", "example008.html" }, 109 { "009", "attachment; fIlEnAMe=example009.html", "example009.html" }, 110 111 { "010", "attachment; filename=Example010.html", "Example010.html" }, 112 { "011", "attachment; filename=EXAMPLE011.html", "EXAMPLE011.html" }, 113 { "012", "attachment; filename=eXample012.html", "eXample012.html" }, 114 { "013", "attachment; filename=example013.HTML", "example013.HTML" }, 115 { "014", "attachment; filename =eXaMpLe014.HtMl", "eXaMpLe014.HtMl"}, 116 117 { "015", "attachment; filename=a", "a" }, 118 { "016", "attachment; filename= b", "b" }, 119 { "017", "attachment; filename= c", "c" }, 120 { "018", "attachment; filename= d", "d" }, 121 { "019", "attachment; filename=e ; filename*=utf-8''eee.txt", "e"}, 122 { "020", "attachment; filename*=utf-8''fff.txt; filename=f", "f"}, 123 { "021", "attachment; filename=g", "g" }, 124 { "022", "attachment; filename= h", "h" }, 125 126 { "023", "attachment; filename=\"space name\"", "space name" }, 127 { "024", "attachment; filename=me.txt; filename*=utf-8''you.txt", "me.txt" }, 128 { "025", "attachment; filename=\"m y.txt\"; filename*=utf-8''you.txt", "m y.txt" }, 129 130 { "030", "attachment; filename=foo/file1.txt", "file1.txt" }, 131 { "031", "attachment; filename=foo/bar/file2.txt", "file2.txt" }, 132 { "032", "attachment; filename=baz\\file3.txt", "file3.txt" }, 133 { "033", "attachment; filename=baz\\bar\\file4.txt", "file4.txt" }, 134 { "034", "attachment; filename=x/y\\file5.txt", "file5.txt" }, 135 { "035", "attachment; filename=x/y\\file6.txt", "file6.txt" }, 136 { "036", "attachment; filename=x/y\\z/file7.txt", "file7.txt" }, 137 { "037", "attachment; filename=x/y\\z/\\x/file8.txt", "file8.txt" }, 138 { "038", "attachment; filename=/root/file9.txt", "file9.txt" }, 139 { "039", "attachment; filename=../file10.txt", "file10.txt" }, 140 { "040", "attachment; filename=..\\file11.txt", "file11.txt" }, 141 { "041", "attachment; filename=foo/../../file12.txt", "file12.txt" }, 142 }; 143 144 @DataProvider(name = "positive") positive()145 public Object[][] positive() { 146 List<Object[]> list = new ArrayList<>(); 147 148 Arrays.asList(contentDispositionValues).stream() 149 .map(e -> new Object[] {httpURI + "?" + e[0], e[1], e[2]}) 150 .forEach(list::add); 151 Arrays.asList(contentDispositionValues).stream() 152 .map(e -> new Object[] {httpsURI + "?" + e[0], e[1], e[2]}) 153 .forEach(list::add); 154 Arrays.asList(contentDispositionValues).stream() 155 .map(e -> new Object[] {http2URI + "?" + e[0], e[1], e[2]}) 156 .forEach(list::add); 157 Arrays.asList(contentDispositionValues).stream() 158 .map(e -> new Object[] {https2URI + "?" + e[0], e[1], e[2]}) 159 .forEach(list::add); 160 161 return list.stream().toArray(Object[][]::new); 162 } 163 164 @Test(dataProvider = "positive") test(String uriString, String contentDispositionValue, String expectedFilename)165 void test(String uriString, String contentDispositionValue, String expectedFilename) 166 throws Exception 167 { 168 out.printf("test(%s, %s, %s): starting", uriString, contentDispositionValue, expectedFilename); 169 HttpClient client = HttpClient.newBuilder().sslContext(sslContext).build(); 170 171 URI uri = URI.create(uriString); 172 HttpRequest request = HttpRequest.newBuilder(uri) 173 .POST(BodyPublishers.ofString("May the luck of the Irish be with you!")) 174 .build(); 175 176 BodyHandler bh = ofFileDownload(tempDir.resolve(uri.getPath().substring(1)), 177 CREATE, TRUNCATE_EXISTING, WRITE); 178 HttpResponse<Path> response = client.send(request, bh); 179 180 out.println("Got response: " + response); 181 out.println("Got body Path: " + response.body()); 182 String fileContents = new String(Files.readAllBytes(response.body()), UTF_8); 183 out.println("Got body: " + fileContents); 184 185 assertEquals(response.statusCode(),200); 186 assertEquals(response.body().getFileName().toString(), expectedFilename); 187 assertTrue(response.headers().firstValue("Content-Disposition").isPresent()); 188 assertEquals(response.headers().firstValue("Content-Disposition").get(), 189 contentDispositionValue); 190 assertEquals(fileContents, "May the luck of the Irish be with you!"); 191 192 // additional checks unrelated to file download 193 caseInsensitivityOfHeaders(request.headers()); 194 caseInsensitivityOfHeaders(response.headers()); 195 } 196 197 // --- Negative 198 199 static final String[][] contentDispositionBADValues = new String[][] { 200 // URI query Content-Type header value 201 { "100", "" }, // empty 202 { "101", "filename=example.html" }, // no attachment 203 { "102", "attachment; filename=space name" }, // unquoted with space 204 { "103", "attachment; filename=" }, // empty filename param 205 { "104", "attachment; filename=\"" }, // single quote 206 { "105", "attachment; filename=\"\"" }, // empty quoted 207 { "106", "attachment; filename=." }, // dot 208 { "107", "attachment; filename=.." }, // dot dot 209 { "108", "attachment; filename=\".." }, // badly quoted dot dot 210 { "109", "attachment; filename=\"..\"" }, // quoted dot dot 211 { "110", "attachment; filename=\"bad" }, // badly quoted 212 { "111", "attachment; filename=\"bad;" }, // badly quoted with ';' 213 { "112", "attachment; filename=\"bad ;" }, // badly quoted with ' ;' 214 { "113", "attachment; filename*=utf-8''xx.txt "}, // no "filename" param 215 216 { "120", "<<NOT_PRESENT>>" }, // header not present 217 218 }; 219 220 @DataProvider(name = "negative") negative()221 public Object[][] negative() { 222 List<Object[]> list = new ArrayList<>(); 223 224 Arrays.asList(contentDispositionBADValues).stream() 225 .map(e -> new Object[] {httpURI + "?" + e[0], e[1]}) 226 .forEach(list::add); 227 Arrays.asList(contentDispositionBADValues).stream() 228 .map(e -> new Object[] {httpsURI + "?" + e[0], e[1]}) 229 .forEach(list::add); 230 Arrays.asList(contentDispositionBADValues).stream() 231 .map(e -> new Object[] {http2URI + "?" + e[0], e[1]}) 232 .forEach(list::add); 233 Arrays.asList(contentDispositionBADValues).stream() 234 .map(e -> new Object[] {https2URI + "?" + e[0], e[1]}) 235 .forEach(list::add); 236 237 return list.stream().toArray(Object[][]::new); 238 } 239 240 @Test(dataProvider = "negative") negativeTest(String uriString, String contentDispositionValue)241 void negativeTest(String uriString, String contentDispositionValue) 242 throws Exception 243 { 244 out.printf("negativeTest(%s, %s): starting", uriString, contentDispositionValue); 245 HttpClient client = HttpClient.newBuilder().sslContext(sslContext).build(); 246 247 URI uri = URI.create(uriString); 248 HttpRequest request = HttpRequest.newBuilder(uri) 249 .POST(BodyPublishers.ofString("Does not matter")) 250 .build(); 251 252 BodyHandler bh = ofFileDownload(tempDir, CREATE, TRUNCATE_EXISTING, WRITE); 253 try { 254 HttpResponse<Path> response = client.send(request, bh); 255 fail("UNEXPECTED response: " + response + ", path:" + response.body()); 256 } catch (UncheckedIOException | IOException ioe) { 257 System.out.println("Caught expected: " + ioe); 258 } 259 } 260 261 // -- Infrastructure 262 serverAuthority(HttpServer server)263 static String serverAuthority(HttpServer server) { 264 return InetAddress.getLoopbackAddress().getHostName() + ":" 265 + server.getAddress().getPort(); 266 } 267 268 @BeforeTest setup()269 public void setup() throws Exception { 270 tempDir = Paths.get("asFileDownloadTest.tmp.dir"); 271 if (Files.exists(tempDir)) 272 throw new AssertionError("Unexpected test work dir existence: " + tempDir.toString()); 273 274 Files.createDirectory(tempDir); 275 // Unique dirs per test run, based on the URI path 276 Files.createDirectories(tempDir.resolve("http1/afdt/")); 277 Files.createDirectories(tempDir.resolve("https1/afdt/")); 278 Files.createDirectories(tempDir.resolve("http2/afdt/")); 279 Files.createDirectories(tempDir.resolve("https2/afdt/")); 280 281 // HTTP/1.1 server logging in case of security exceptions ( uncomment if needed ) 282 //Logger logger = Logger.getLogger("com.sun.net.httpserver"); 283 //ConsoleHandler ch = new ConsoleHandler(); 284 //logger.setLevel(Level.ALL); 285 //ch.setLevel(Level.ALL); 286 //logger.addHandler(ch); 287 288 sslContext = new SimpleSSLContext().get(); 289 if (sslContext == null) 290 throw new AssertionError("Unexpected null sslContext"); 291 292 InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); 293 httpTestServer = HttpServer.create(sa, 0); 294 httpTestServer.createContext("/http1/afdt", new Http1FileDispoHandler()); 295 httpURI = "http://" + serverAuthority(httpTestServer) + "/http1/afdt"; 296 297 httpsTestServer = HttpsServer.create(sa, 0); 298 httpsTestServer.setHttpsConfigurator(new HttpsConfigurator(sslContext)); 299 httpsTestServer.createContext("/https1/afdt", new Http1FileDispoHandler()); 300 httpsURI = "https://" + serverAuthority(httpsTestServer) + "/https1/afdt"; 301 302 http2TestServer = new Http2TestServer("localhost", false, 0); 303 http2TestServer.addHandler(new Http2FileDispoHandler(), "/http2/afdt"); 304 http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/afdt"; 305 306 https2TestServer = new Http2TestServer("localhost", true, sslContext); 307 https2TestServer.addHandler(new Http2FileDispoHandler(), "/https2/afdt"); 308 https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/afdt"; 309 310 httpTestServer.start(); 311 httpsTestServer.start(); 312 http2TestServer.start(); 313 https2TestServer.start(); 314 } 315 316 @AfterTest teardown()317 public void teardown() throws Exception { 318 httpTestServer.stop(0); 319 httpsTestServer.stop(0); 320 http2TestServer.stop(); 321 https2TestServer.stop(); 322 323 if (System.getSecurityManager() == null && Files.exists(tempDir)) { 324 // clean up before next run with security manager 325 FileUtils.deleteFileTreeWithRetry(tempDir); 326 } 327 } 328 contentDispositionValueFromURI(URI uri)329 static String contentDispositionValueFromURI(URI uri) { 330 String queryIndex = uri.getQuery(); 331 String[][] values; 332 if (queryIndex.startsWith("0")) // positive tests start with '0' 333 values = contentDispositionValues; 334 else if (queryIndex.startsWith("1")) // negative tests start with '1' 335 values = contentDispositionBADValues; 336 else 337 throw new AssertionError("SERVER: UNEXPECTED query:" + queryIndex); 338 339 return Arrays.asList(values).stream() 340 .filter(e -> e[0].equals(queryIndex)) 341 .map(e -> e[1]) 342 .findFirst() 343 .orElseThrow(); 344 } 345 346 static class Http1FileDispoHandler implements HttpHandler { 347 @Override handle(HttpExchange t)348 public void handle(HttpExchange t) throws IOException { 349 try (InputStream is = t.getRequestBody(); 350 OutputStream os = t.getResponseBody()) { 351 byte[] bytes = is.readAllBytes(); 352 353 String value = contentDispositionValueFromURI(t.getRequestURI()); 354 if (!value.equals("<<NOT_PRESENT>>")) 355 t.getResponseHeaders().set("Content-Disposition", value); 356 357 t.sendResponseHeaders(200, bytes.length); 358 os.write(bytes); 359 } 360 } 361 } 362 363 static class Http2FileDispoHandler implements Http2Handler { 364 @Override handle(Http2TestExchange t)365 public void handle(Http2TestExchange t) throws IOException { 366 try (InputStream is = t.getRequestBody(); 367 OutputStream os = t.getResponseBody()) { 368 byte[] bytes = is.readAllBytes(); 369 370 String value = contentDispositionValueFromURI(t.getRequestURI()); 371 if (!value.equals("<<NOT_PRESENT>>")) 372 t.getResponseHeaders().addHeader("Content-Disposition", value); 373 374 t.sendResponseHeaders(200, bytes.length); 375 os.write(bytes); 376 } 377 } 378 } 379 380 // --- 381 382 // Asserts case-insensitivity of headers (nothing to do with file 383 // download, just convenient as we have a couple of header instances. ) caseInsensitivityOfHeaders(HttpHeaders headers)384 static void caseInsensitivityOfHeaders(HttpHeaders headers) { 385 try { 386 for (Map.Entry<String, List<String>> entry : headers.map().entrySet()) { 387 String headerName = entry.getKey(); 388 List<String> headerValue = entry.getValue(); 389 390 for (String name : List.of(headerName.toUpperCase(Locale.ROOT), 391 headerName.toLowerCase(Locale.ROOT))) { 392 assertTrue(headers.firstValue(name).isPresent()); 393 assertEquals(headers.firstValue(name).get(), headerValue.get(0)); 394 assertEquals(headers.allValues(name).size(), headerValue.size()); 395 assertEquals(headers.allValues(name), headerValue); 396 assertEquals(headers.map().get(name).size(), headerValue.size()); 397 assertEquals(headers.map().get(name), headerValue); 398 } 399 } 400 } catch (Throwable t) { 401 System.out.println("failure in caseInsensitivityOfHeaders with:" + headers); 402 throw t; 403 } 404 } 405 } 406