1
18
19 package io.undertow.server.handlers.form;
20
21 import io.undertow.UndertowLogger;
22 import io.undertow.UndertowMessages;
23 import io.undertow.UndertowOptions;
24 import io.undertow.server.ExchangeCompletionListener;
25 import io.undertow.server.HttpHandler;
26 import io.undertow.server.HttpServerExchange;
27 import io.undertow.util.HeaderMap;
28 import io.undertow.util.Headers;
29 import io.undertow.util.MalformedMessageException;
30 import io.undertow.util.MultipartParser;
31 import io.undertow.util.SameThreadExecutor;
32 import io.undertow.util.StatusCodes;
33 import org.xnio.ChannelListener;
34 import org.xnio.IoUtils;
35 import io.undertow.connector.PooledByteBuffer;
36 import org.xnio.channels.StreamSourceChannel;
37
38 import java.io.ByteArrayOutputStream;
39 import java.io.FileOutputStream;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.io.UnsupportedEncodingException;
43 import java.nio.ByteBuffer;
44 import java.nio.channels.FileChannel;
45 import java.nio.charset.StandardCharsets;
46 import java.nio.file.Files;
47 import java.nio.file.NoSuchFileException;
48 import java.nio.file.Path;
49 import java.nio.file.Paths;
50 import java.nio.file.StandardOpenOption;
51 import java.util.ArrayList;
52 import java.util.Arrays;
53 import java.util.List;
54 import java.util.concurrent.Executor;
55
56
59 public class MultiPartParserDefinition implements FormParserFactory.ParserDefinition<MultiPartParserDefinition> {
60
61 public static final String MULTIPART_FORM_DATA = "multipart/form-data";
62
63 private Executor executor;
64
65 private Path tempFileLocation;
66
67 private String defaultEncoding = StandardCharsets.ISO_8859_1.displayName();
68
69 private long maxIndividualFileSize = -1;
70
71 private long fileSizeThreshold;
72
73 public MultiPartParserDefinition() {
74 tempFileLocation = Paths.get(System.getProperty("java.io.tmpdir"));
75 }
76
77 public MultiPartParserDefinition(final Path tempDir) {
78 tempFileLocation = tempDir;
79 }
80
81 @Override
82 public FormDataParser create(final HttpServerExchange exchange) {
83 String mimeType = exchange.getRequestHeaders().getFirst(Headers.CONTENT_TYPE);
84 if (mimeType != null && mimeType.startsWith(MULTIPART_FORM_DATA)) {
85 String boundary = Headers.extractQuotedValueFromHeader(mimeType, "boundary");
86 if (boundary == null) {
87 UndertowLogger.REQUEST_LOGGER.debugf("Could not find boundary in multipart request with ContentType: %s, multipart data will not be available", mimeType);
88 return null;
89 }
90 final MultiPartUploadHandler parser = new MultiPartUploadHandler(exchange, boundary, maxIndividualFileSize, fileSizeThreshold, defaultEncoding);
91 exchange.addExchangeCompleteListener(new ExchangeCompletionListener() {
92 @Override
93 public void exchangeEvent(final HttpServerExchange exchange, final NextListener nextListener) {
94 IoUtils.safeClose(parser);
95 nextListener.proceed();
96 }
97 });
98 Long sizeLimit = exchange.getConnection().getUndertowOptions().get(UndertowOptions.MULTIPART_MAX_ENTITY_SIZE);
99 if(sizeLimit != null) {
100 exchange.setMaxEntitySize(sizeLimit);
101 }
102 UndertowLogger.REQUEST_LOGGER.tracef("Created multipart parser for %s", exchange);
103
104 return parser;
105
106 }
107 return null;
108 }
109
110 public Executor getExecutor() {
111 return executor;
112 }
113
114 public MultiPartParserDefinition setExecutor(final Executor executor) {
115 this.executor = executor;
116 return this;
117 }
118
119 public Path getTempFileLocation() {
120 return tempFileLocation;
121 }
122
123 public MultiPartParserDefinition setTempFileLocation(Path tempFileLocation) {
124 this.tempFileLocation = tempFileLocation;
125 return this;
126 }
127
128 public String getDefaultEncoding() {
129 return defaultEncoding;
130 }
131
132 public MultiPartParserDefinition setDefaultEncoding(final String defaultEncoding) {
133 this.defaultEncoding = defaultEncoding;
134 return this;
135 }
136
137 public long getMaxIndividualFileSize() {
138 return maxIndividualFileSize;
139 }
140
141 public void setMaxIndividualFileSize(final long maxIndividualFileSize) {
142 this.maxIndividualFileSize = maxIndividualFileSize;
143 }
144
145 public void setFileSizeThreshold(long fileSizeThreshold) {
146 this.fileSizeThreshold = fileSizeThreshold;
147 }
148
149 private final class MultiPartUploadHandler implements FormDataParser, MultipartParser.PartHandler {
150
151 private final HttpServerExchange exchange;
152 private final FormData data;
153 private final List<Path> createdFiles = new ArrayList<>();
154 private final long maxIndividualFileSize;
155 private final long fileSizeThreshold;
156 private String defaultEncoding;
157
158 private final ByteArrayOutputStream contentBytes = new ByteArrayOutputStream();
159 private String currentName;
160 private String fileName;
161 private Path file;
162 private FileChannel fileChannel;
163 private HeaderMap headers;
164 private HttpHandler handler;
165 private long currentFileSize;
166 private final MultipartParser.ParseState parser;
167
168
169 private MultiPartUploadHandler(final HttpServerExchange exchange, final String boundary, final long maxIndividualFileSize, final long fileSizeThreshold, final String defaultEncoding) {
170 this.exchange = exchange;
171 this.maxIndividualFileSize = maxIndividualFileSize;
172 this.defaultEncoding = defaultEncoding;
173 this.fileSizeThreshold = fileSizeThreshold;
174 this.data = new FormData(exchange.getConnection().getUndertowOptions().get(UndertowOptions.MAX_PARAMETERS, 1000));
175 String charset = defaultEncoding;
176 String contentType = exchange.getRequestHeaders().getFirst(Headers.CONTENT_TYPE);
177 if (contentType != null) {
178 String value = Headers.extractQuotedValueFromHeader(contentType, "charset");
179 if (value != null) {
180 charset = value;
181 }
182 }
183 this.parser = MultipartParser.beginParse(exchange.getConnection().getByteBufferPool(), this, boundary.getBytes(StandardCharsets.US_ASCII), charset);
184
185 }
186
187
188 @Override
189 public void parse(final HttpHandler handler) throws Exception {
190 if (exchange.getAttachment(FORM_DATA) != null) {
191 handler.handleRequest(exchange);
192 return;
193 }
194 this.handler = handler;
195
196
197
198 StreamSourceChannel requestChannel = exchange.getRequestChannel();
199 if (requestChannel == null) {
200 throw new IOException(UndertowMessages.MESSAGES.requestChannelAlreadyProvided());
201 }
202 if (executor == null) {
203 exchange.dispatch(new NonBlockingParseTask(exchange.getConnection().getWorker(), requestChannel));
204 } else {
205 exchange.dispatch(executor, new NonBlockingParseTask(executor, requestChannel));
206 }
207 }
208
209 @Override
210 public FormData parseBlocking() throws IOException {
211 final FormData existing = exchange.getAttachment(FORM_DATA);
212 if (existing != null) {
213 return existing;
214 }
215 InputStream inputStream = exchange.getInputStream();
216 if (inputStream == null) {
217 throw new IOException(UndertowMessages.MESSAGES.requestChannelAlreadyProvided());
218 }
219 try (PooledByteBuffer pooled = exchange.getConnection().getByteBufferPool().getArrayBackedPool().allocate()){
220 ByteBuffer buf = pooled.getBuffer();
221 while (true) {
222 buf.clear();
223 int c = inputStream.read(buf.array(), buf.arrayOffset(), buf.remaining());
224 if (c == -1) {
225 if (parser.isComplete()) {
226 break;
227 } else {
228 throw UndertowMessages.MESSAGES.connectionTerminatedReadingMultiPartData();
229 }
230 } else if (c != 0) {
231 buf.limit(c);
232 parser.parse(buf);
233 }
234 }
235 exchange.putAttachment(FORM_DATA, data);
236 } catch (MalformedMessageException e) {
237 throw new IOException(e);
238 }
239 return exchange.getAttachment(FORM_DATA);
240 }
241
242 @Override
243 public void beginPart(final HeaderMap headers) {
244 this.currentFileSize = 0;
245 this.headers = headers;
246 final String disposition = headers.getFirst(Headers.CONTENT_DISPOSITION);
247 if (disposition != null) {
248 if (disposition.startsWith("form-data")) {
249 currentName = Headers.extractQuotedValueFromHeader(disposition, "name");
250 fileName = Headers.extractQuotedValueFromHeaderWithEncoding(disposition, "filename");
251 if (fileName != null && fileSizeThreshold == 0) {
252 try {
253 if (tempFileLocation != null) {
254 file = Files.createTempFile(tempFileLocation, "undertow", "upload");
255 } else {
256 file = Files.createTempFile("undertow", "upload");
257 }
258 createdFiles.add(file);
259 fileChannel = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE);
260 } catch (IOException e) {
261 throw new RuntimeException(e);
262 }
263 }
264 }
265 }
266 }
267
268 @Override
269 public void data(final ByteBuffer buffer) throws IOException {
270 this.currentFileSize += buffer.remaining();
271 if (this.maxIndividualFileSize > 0 && this.currentFileSize > this.maxIndividualFileSize) {
272 throw UndertowMessages.MESSAGES.maxFileSizeExceeded(this.maxIndividualFileSize);
273 }
274 if (file == null && fileName != null && fileSizeThreshold < this.currentFileSize) {
275 try {
276 if (tempFileLocation != null) {
277 file = Files.createTempFile(tempFileLocation, "undertow", "upload");
278 } else {
279 file = Files.createTempFile("undertow", "upload");
280 }
281 createdFiles.add(file);
282
283 FileOutputStream fileOutputStream = new FileOutputStream(file.toFile());
284 contentBytes.writeTo(fileOutputStream);
285
286 fileChannel = fileOutputStream.getChannel();
287 } catch (IOException e) {
288 throw new RuntimeException(e);
289 }
290 }
291
292 if (file == null) {
293 while (buffer.hasRemaining()) {
294 contentBytes.write(buffer.get());
295 }
296 } else {
297 fileChannel.write(buffer);
298 }
299 }
300
301 @Override
302 public void endPart() {
303 if (file != null) {
304 data.add(currentName, file, fileName, headers);
305 file = null;
306 contentBytes.reset();
307 try {
308 fileChannel.close();
309 fileChannel = null;
310 } catch (IOException e) {
311 throw new RuntimeException(e);
312 }
313 } else if (fileName != null) {
314 data.add(currentName, Arrays.copyOf(contentBytes.toByteArray(), contentBytes.size()), fileName, headers);
315 contentBytes.reset();
316 } else {
317
318
319 try {
320 String charset = defaultEncoding;
321 String contentType = headers.getFirst(Headers.CONTENT_TYPE);
322 if (contentType != null) {
323 String cs = Headers.extractQuotedValueFromHeader(contentType, "charset");
324 if (cs != null) {
325 charset = cs;
326 }
327 }
328
329 data.add(currentName, new String(contentBytes.toByteArray(), charset), headers);
330 } catch (UnsupportedEncodingException e) {
331 throw new RuntimeException(e);
332 }
333 contentBytes.reset();
334 }
335 }
336
337
338 public List<Path> getCreatedFiles() {
339 return createdFiles;
340 }
341
342 @Override
343 public void close() throws IOException {
344 IoUtils.safeClose(fileChannel);
345
346 final List<Path> files = new ArrayList<>(getCreatedFiles());
347 exchange.getConnection().getWorker().execute(new Runnable() {
348 @Override
349 public void run() {
350 for (final Path file : files) {
351 if (Files.exists(file)) {
352 try {
353 Files.delete(file);
354 } catch (NoSuchFileException e) {
355 } catch (IOException e) {
356 UndertowLogger.REQUEST_LOGGER.cannotRemoveUploadedFile(file);
357 }
358 }
359 }
360 }
361 });
362
363 }
364
365 @Override
366 public void setCharacterEncoding(final String encoding) {
367 this.defaultEncoding = encoding;
368 parser.setCharacterEncoding(encoding);
369 }
370
371 private final class NonBlockingParseTask implements Runnable {
372
373 private final Executor executor;
374 private final StreamSourceChannel requestChannel;
375
376 private NonBlockingParseTask(Executor executor, StreamSourceChannel requestChannel) {
377 this.executor = executor;
378 this.requestChannel = requestChannel;
379 }
380
381 @Override
382 public void run() {
383 try {
384 final FormData existing = exchange.getAttachment(FORM_DATA);
385 if (existing != null) {
386 exchange.dispatch(SameThreadExecutor.INSTANCE, handler);
387 return;
388 }
389 PooledByteBuffer pooled = exchange.getConnection().getByteBufferPool().allocate();
390 try {
391 while (true) {
392 int c = requestChannel.read(pooled.getBuffer());
393 if(c == 0) {
394 requestChannel.getReadSetter().set(new ChannelListener<StreamSourceChannel>() {
395 @Override
396 public void handleEvent(StreamSourceChannel channel) {
397 channel.suspendReads();
398 executor.execute(NonBlockingParseTask.this);
399 }
400 });
401 requestChannel.resumeReads();
402 return;
403 } else if (c == -1) {
404 if (parser.isComplete()) {
405 exchange.putAttachment(FORM_DATA, data);
406 exchange.dispatch(SameThreadExecutor.INSTANCE, handler);
407 } else {
408 UndertowLogger.REQUEST_IO_LOGGER.ioException(UndertowMessages.MESSAGES.connectionTerminatedReadingMultiPartData());
409 exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
410 exchange.endExchange();
411 }
412 return;
413 } else {
414 pooled.getBuffer().flip();
415 parser.parse(pooled.getBuffer());
416 pooled.getBuffer().compact();
417 }
418 }
419 } catch (MalformedMessageException e) {
420 UndertowLogger.REQUEST_IO_LOGGER.ioException(e);
421 exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
422 exchange.endExchange();
423 } finally {
424 pooled.close();
425 }
426
427 } catch (Throwable e) {
428 UndertowLogger.REQUEST_IO_LOGGER.debug("Exception parsing data", e);
429 exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
430 exchange.endExchange();
431 }
432 }
433 }
434 }
435
436
437 public static class FileTooLargeException extends IOException {
438
439 public FileTooLargeException() {
440 super();
441 }
442
443 public FileTooLargeException(String message) {
444 super(message);
445 }
446
447 public FileTooLargeException(String message, Throwable cause) {
448 super(message, cause);
449 }
450
451 public FileTooLargeException(Throwable cause) {
452 super(cause);
453 }
454 }
455
456 }
457