Path: blob/master/src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java
41171 views
/*1* Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package jdk.internal.net.http;2627import java.io.File;28import java.io.FilePermission;29import java.io.IOException;30import java.io.UncheckedIOException;31import java.net.URI;32import java.nio.file.Files;33import java.nio.file.OpenOption;34import java.nio.file.Path;35import java.nio.file.Paths;36import java.security.AccessControlContext;37import java.security.AccessController;38import java.util.List;39import java.util.concurrent.CompletableFuture;40import java.util.concurrent.ConcurrentMap;41import java.util.function.Function;42import java.net.http.HttpRequest;43import java.net.http.HttpResponse;44import java.net.http.HttpResponse.BodyHandler;45import java.net.http.HttpResponse.ResponseInfo;46import java.net.http.HttpResponse.BodySubscriber;47import java.util.regex.Matcher;48import java.util.regex.Pattern;49import jdk.internal.net.http.ResponseSubscribers.PathSubscriber;50import static java.util.regex.Pattern.CASE_INSENSITIVE;5152public final class ResponseBodyHandlers {5354private ResponseBodyHandlers() { }5556private static final String pathForSecurityCheck(Path path) {57return path.toFile().getPath();58}5960/**61* A Path body handler.62*/63public static class PathBodyHandler implements BodyHandler<Path>{64private final Path file;65private final List<OpenOption> openOptions; // immutable list66@SuppressWarnings("removal")67private final AccessControlContext acc;68private final FilePermission filePermission;6970/**71* Factory for creating PathBodyHandler.72*73* Permission checks are performed here before construction of the74* PathBodyHandler. Permission checking and construction are75* deliberately and tightly co-located.76*/77public static PathBodyHandler create(Path file,78List<OpenOption> openOptions) {79FilePermission filePermission = null;80@SuppressWarnings("removal")81SecurityManager sm = System.getSecurityManager();82if (sm != null) {83try {84String fn = pathForSecurityCheck(file);85FilePermission writePermission = new FilePermission(fn, "write");86sm.checkPermission(writePermission);87filePermission = writePermission;88} catch (UnsupportedOperationException ignored) {89// path not associated with the default file system provider90}91}9293assert filePermission == null || filePermission.getActions().equals("write");94@SuppressWarnings("removal")95var acc = sm != null ? AccessController.getContext() : null;96return new PathBodyHandler(file, openOptions, acc, filePermission);97}9899private PathBodyHandler(Path file,100List<OpenOption> openOptions,101@SuppressWarnings("removal") AccessControlContext acc,102FilePermission filePermission) {103this.file = file;104this.openOptions = openOptions;105this.acc = acc;106this.filePermission = filePermission;107}108109@Override110public BodySubscriber<Path> apply(ResponseInfo responseInfo) {111return new PathSubscriber(file, openOptions, acc, filePermission);112}113}114115/** With push promise Map implementation */116public static class PushPromisesHandlerWithMap<T>117implements HttpResponse.PushPromiseHandler<T>118{119private final ConcurrentMap<HttpRequest,CompletableFuture<HttpResponse<T>>> pushPromisesMap;120private final Function<HttpRequest,BodyHandler<T>> pushPromiseHandler;121122public PushPromisesHandlerWithMap(Function<HttpRequest,BodyHandler<T>> pushPromiseHandler,123ConcurrentMap<HttpRequest,CompletableFuture<HttpResponse<T>>> pushPromisesMap) {124this.pushPromiseHandler = pushPromiseHandler;125this.pushPromisesMap = pushPromisesMap;126}127128@Override129public void applyPushPromise(130HttpRequest initiatingRequest, HttpRequest pushRequest,131Function<BodyHandler<T>,CompletableFuture<HttpResponse<T>>> acceptor)132{133URI initiatingURI = initiatingRequest.uri();134URI pushRequestURI = pushRequest.uri();135if (!initiatingURI.getHost().equalsIgnoreCase(pushRequestURI.getHost()))136return;137138int initiatingPort = initiatingURI.getPort();139if (initiatingPort == -1 ) {140if ("https".equalsIgnoreCase(initiatingURI.getScheme()))141initiatingPort = 443;142else143initiatingPort = 80;144}145int pushPort = pushRequestURI.getPort();146if (pushPort == -1 ) {147if ("https".equalsIgnoreCase(pushRequestURI.getScheme()))148pushPort = 443;149else150pushPort = 80;151}152if (initiatingPort != pushPort)153return;154155CompletableFuture<HttpResponse<T>> cf =156acceptor.apply(pushPromiseHandler.apply(pushRequest));157pushPromisesMap.put(pushRequest, cf);158}159}160161// Similar to Path body handler, but for file download.162public static class FileDownloadBodyHandler implements BodyHandler<Path> {163private final Path directory;164private final List<OpenOption> openOptions;165@SuppressWarnings("removal")166private final AccessControlContext acc;167private final FilePermission[] filePermissions; // may be null168169/**170* Factory for creating FileDownloadBodyHandler.171*172* Permission checks are performed here before construction of the173* FileDownloadBodyHandler. Permission checking and construction are174* deliberately and tightly co-located.175*/176public static FileDownloadBodyHandler create(Path directory,177List<OpenOption> openOptions) {178String fn;179try {180fn = pathForSecurityCheck(directory);181} catch (UnsupportedOperationException uoe) {182// directory not associated with the default file system provider183throw new IllegalArgumentException("invalid path: " + directory, uoe);184}185186FilePermission filePermissions[] = null;187@SuppressWarnings("removal")188SecurityManager sm = System.getSecurityManager();189if (sm != null) {190FilePermission writePermission = new FilePermission(fn, "write");191String writePathPerm = fn + File.separatorChar + "*";192FilePermission writeInDirPermission = new FilePermission(writePathPerm, "write");193sm.checkPermission(writeInDirPermission);194FilePermission readPermission = new FilePermission(fn, "read");195sm.checkPermission(readPermission);196197// read permission is only needed before determine the below checks198// only write permission is required when downloading to the file199filePermissions = new FilePermission[] { writePermission, writeInDirPermission };200}201202// existence, etc, checks must be after permission checks203if (Files.notExists(directory))204throw new IllegalArgumentException("non-existent directory: " + directory);205if (!Files.isDirectory(directory))206throw new IllegalArgumentException("not a directory: " + directory);207if (!Files.isWritable(directory))208throw new IllegalArgumentException("non-writable directory: " + directory);209210assert filePermissions == null || (filePermissions[0].getActions().equals("write")211&& filePermissions[1].getActions().equals("write"));212@SuppressWarnings("removal")213var acc = sm != null ? AccessController.getContext() : null;214return new FileDownloadBodyHandler(directory, openOptions, acc, filePermissions);215}216217private FileDownloadBodyHandler(Path directory,218List<OpenOption> openOptions,219@SuppressWarnings("removal") AccessControlContext acc,220FilePermission... filePermissions) {221this.directory = directory;222this.openOptions = openOptions;223this.acc = acc;224this.filePermissions = filePermissions;225}226227/** The "attachment" disposition-type and separator. */228static final String DISPOSITION_TYPE = "attachment;";229230/** The "filename" parameter. */231static final Pattern FILENAME = Pattern.compile("filename\\s*=", CASE_INSENSITIVE);232233static final List<String> PROHIBITED = List.of(".", "..", "", "~" , "|");234235static final UncheckedIOException unchecked(ResponseInfo rinfo,236String msg) {237String s = String.format("%s in response [%d, %s]", msg, rinfo.statusCode(), rinfo.headers());238return new UncheckedIOException(new IOException(s));239}240241@Override242public BodySubscriber<Path> apply(ResponseInfo responseInfo) {243String dispoHeader = responseInfo.headers().firstValue("Content-Disposition")244.orElseThrow(() -> unchecked(responseInfo, "No Content-Disposition header"));245246if (!dispoHeader.regionMatches(true, // ignoreCase2470, DISPOSITION_TYPE,2480, DISPOSITION_TYPE.length())) {249throw unchecked(responseInfo, "Unknown Content-Disposition type");250}251252Matcher matcher = FILENAME.matcher(dispoHeader);253if (!matcher.find()) {254throw unchecked(responseInfo, "Bad Content-Disposition filename parameter");255}256int n = matcher.end();257258int semi = dispoHeader.substring(n).indexOf(";");259String filenameParam;260if (semi < 0) {261filenameParam = dispoHeader.substring(n);262} else {263filenameParam = dispoHeader.substring(n, n + semi);264}265266// strip all but the last path segment267int x = filenameParam.lastIndexOf("/");268if (x != -1) {269filenameParam = filenameParam.substring(x+1);270}271x = filenameParam.lastIndexOf("\\");272if (x != -1) {273filenameParam = filenameParam.substring(x+1);274}275276filenameParam = filenameParam.trim();277278if (filenameParam.startsWith("\"")) { // quoted-string279if (!filenameParam.endsWith("\"") || filenameParam.length() == 1) {280throw unchecked(responseInfo,281"Badly quoted Content-Disposition filename parameter");282}283filenameParam = filenameParam.substring(1, filenameParam.length() -1 );284} else { // token,285if (filenameParam.contains(" ")) { // space disallowed286throw unchecked(responseInfo,287"unquoted space in Content-Disposition filename parameter");288}289}290291if (PROHIBITED.contains(filenameParam)) {292throw unchecked(responseInfo,293"Prohibited Content-Disposition filename parameter:"294+ filenameParam);295}296297Path file = Paths.get(directory.toString(), filenameParam);298299if (!file.startsWith(directory)) {300throw unchecked(responseInfo,301"Resulting file, " + file.toString() + ", outside of given directory");302}303304return new PathSubscriber(file, openOptions, acc, filePermissions);305}306}307}308309310