1 /*
2  * JBoss, Home of Professional Open Source.
3  * Copyright 2014 Red Hat, Inc., and individual contributors
4  * as indicated by the @author tags.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  */

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 /**
57  * @author Stuart Douglas
58  */

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             //we need to delegate to a thread pool
196             //as we parse with blocking operations
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             //we have to dispatch this, as it may result in file IO
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) { // ignore
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