src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java
branchhttp-client-branch
changeset 56132 c8a1eccbc719
parent 56092 fd85b2bf2b0d
child 56138 4f92b988600e
equal deleted inserted replaced
56131:99f144742013 56132:c8a1eccbc719
    24  */
    24  */
    25 
    25 
    26 package jdk.internal.net.http;
    26 package jdk.internal.net.http;
    27 
    27 
    28 import java.io.IOException;
    28 import java.io.IOException;
       
    29 import java.io.UncheckedIOException;
    29 import java.net.URI;
    30 import java.net.URI;
    30 import java.nio.file.OpenOption;
    31 import java.nio.file.OpenOption;
    31 import java.nio.file.Path;
    32 import java.nio.file.Path;
    32 import java.nio.file.Paths;
    33 import java.nio.file.Paths;
    33 import java.security.AccessControlContext;
    34 import java.security.AccessControlContext;
       
    35 import java.util.List;
    34 import java.util.concurrent.CompletableFuture;
    36 import java.util.concurrent.CompletableFuture;
    35 import java.util.concurrent.ConcurrentMap;
    37 import java.util.concurrent.ConcurrentMap;
    36 import java.util.function.Function;
    38 import java.util.function.Function;
    37 import java.net.http.HttpHeaders;
    39 import java.net.http.HttpHeaders;
    38 import java.net.http.HttpRequest;
    40 import java.net.http.HttpRequest;
    39 import java.net.http.HttpResponse;
    41 import java.net.http.HttpResponse;
    40 import java.net.http.HttpResponse.BodyHandler;
    42 import java.net.http.HttpResponse.BodyHandler;
    41 import java.net.http.HttpResponse.BodySubscriber;
    43 import java.net.http.HttpResponse.BodySubscriber;
       
    44 import java.util.regex.Matcher;
       
    45 import java.util.regex.Pattern;
    42 import jdk.internal.net.http.ResponseSubscribers.PathSubscriber;
    46 import jdk.internal.net.http.ResponseSubscribers.PathSubscriber;
    43 import static jdk.internal.net.http.common.Utils.unchecked;
    47 import static java.util.regex.Pattern.CASE_INSENSITIVE;
    44 
    48 
    45 public final class ResponseBodyHandlers {
    49 public final class ResponseBodyHandlers {
    46 
    50 
    47     private ResponseBodyHandlers() { }
    51     private ResponseBodyHandlers() { }
    48 
    52 
   122     }
   126     }
   123 
   127 
   124     // Similar to Path body handler, but for file download. Supports setting ACC.
   128     // Similar to Path body handler, but for file download. Supports setting ACC.
   125     public static class FileDownloadBodyHandler implements UntrustedBodyHandler<Path> {
   129     public static class FileDownloadBodyHandler implements UntrustedBodyHandler<Path> {
   126         private final Path directory;
   130         private final Path directory;
   127         private final OpenOption[]openOptions;
   131         private final OpenOption[] openOptions;
   128         private volatile AccessControlContext acc;
   132         private volatile AccessControlContext acc;
   129 
   133 
   130         public FileDownloadBodyHandler(Path directory, OpenOption... openOptions) {
   134         public FileDownloadBodyHandler(Path directory, OpenOption... openOptions) {
   131             this.directory = directory;
   135             this.directory = directory;
   132             this.openOptions = openOptions;
   136             this.openOptions = openOptions;
   135         @Override
   139         @Override
   136         public void setAccessControlContext(AccessControlContext acc) {
   140         public void setAccessControlContext(AccessControlContext acc) {
   137             this.acc = acc;
   141             this.acc = acc;
   138         }
   142         }
   139 
   143 
       
   144         /** The "attachment" disposition-type and separator. */
       
   145         static final String DISPOSITION_TYPE = "attachment;";
       
   146 
       
   147         /** The "filename" parameter. */
       
   148         static final Pattern FILENAME = Pattern.compile("filename\\s*=", CASE_INSENSITIVE);
       
   149 
       
   150         static final List<String> PROHIBITED = List.of(".", "..", "", "~" , "|");
       
   151 
       
   152         static final UncheckedIOException unchecked(int code,
       
   153                                                     HttpHeaders headers,
       
   154                                                     String msg) {
       
   155             String s = String.format("%s in response [%d, %s]", msg, code, headers);
       
   156             return new UncheckedIOException(new IOException(s));
       
   157         }
       
   158 
   140         @Override
   159         @Override
   141         public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
   160         public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
   142             String dispoHeader = headers.firstValue("Content-Disposition")
   161             String dispoHeader = headers.firstValue("Content-Disposition")
   143                     .orElseThrow(() -> unchecked(new IOException("No Content-Disposition")));
   162                     .orElseThrow(() -> unchecked(statusCode, headers,
   144             if (!dispoHeader.startsWith("attachment;")) {
   163                             "No Content-Disposition header"));
   145                 throw unchecked(new IOException("Unknown Content-Disposition type"));
   164 
   146             }
   165             if (!dispoHeader.regionMatches(true, // ignoreCase
   147             int n = dispoHeader.indexOf("filename=");
   166                                            0, DISPOSITION_TYPE,
   148             if (n == -1) {
   167                                            0, DISPOSITION_TYPE.length())) {
   149                 throw unchecked(new IOException("Bad Content-Disposition type"));
   168                 throw unchecked(statusCode, headers, "Unknown Content-Disposition type");
   150             }
   169             }
   151             int lastsemi = dispoHeader.lastIndexOf(';');
   170 
   152             String disposition;
   171             Matcher matcher = FILENAME.matcher(dispoHeader);
   153             if (lastsemi < n) {
   172             if (!matcher.find()) {
   154                 disposition = dispoHeader.substring(n + 9);
   173                 throw unchecked(statusCode, headers,
       
   174                           "Bad Content-Disposition filename parameter");
       
   175             }
       
   176             int n = matcher.end();
       
   177 
       
   178             int semi = dispoHeader.substring(n).indexOf(";");
       
   179             String filenameParam;
       
   180             if (semi < 0) {
       
   181                 filenameParam = dispoHeader.substring(n);
   155             } else {
   182             } else {
   156                 disposition = dispoHeader.substring(n + 9, lastsemi);
   183                 filenameParam = dispoHeader.substring(n, n + semi);
   157             }
   184             }
   158             Path file = Paths.get(directory.toString(), disposition);
   185 
       
   186             // strip all but the last path segment
       
   187             int x = filenameParam.lastIndexOf("/");
       
   188             if (x != -1) {
       
   189                 filenameParam = filenameParam.substring(x+1);
       
   190             }
       
   191             x = filenameParam.lastIndexOf("\\");
       
   192             if (x != -1) {
       
   193                 filenameParam = filenameParam.substring(x+1);
       
   194             }
       
   195 
       
   196             filenameParam = filenameParam.trim();
       
   197 
       
   198             if (filenameParam.startsWith("\"")) {  // quoted-string
       
   199                 if (!filenameParam.endsWith("\"") || filenameParam.length() == 1) {
       
   200                     throw unchecked(statusCode, headers,
       
   201                             "Badly quoted Content-Disposition filename parameter");
       
   202                 }
       
   203                 filenameParam = filenameParam.substring(1, filenameParam.length() -1 );
       
   204             } else {  // token,
       
   205                 if (filenameParam.contains(" ")) {  // space disallowed
       
   206                     throw unchecked(statusCode, headers,
       
   207                             "unquoted space in Content-Disposition filename parameter");
       
   208                 }
       
   209             }
       
   210 
       
   211             if (PROHIBITED.contains(filenameParam)) {
       
   212                 throw unchecked(statusCode, headers,
       
   213                         "Prohibited Content-Disposition filename parameter:"
       
   214                                 + filenameParam);
       
   215             }
       
   216 
       
   217             Path file = Paths.get(directory.toString(), filenameParam);
       
   218 
       
   219             if (!file.startsWith(directory)) {
       
   220                 throw unchecked(statusCode, headers,
       
   221                         "Resulting file, " + file.toString() + ", outside of given directory");
       
   222             }
   159 
   223 
   160             PathSubscriber bs = (PathSubscriber)asFileImpl(file, openOptions);
   224             PathSubscriber bs = (PathSubscriber)asFileImpl(file, openOptions);
   161             bs.setAccessControlContext(acc);
   225             bs.setAccessControlContext(acc);
   162             return bs;
   226             return bs;
   163         }
   227         }