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 } |