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