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