-
Notifications
You must be signed in to change notification settings - Fork 1.9k
/
ServletMultiPartFormData.java
262 lines (229 loc) · 9.96 KB
/
ServletMultiPartFormData.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.Part;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.InputStreamContentSource;
import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.util.StringUtil;
/**
* <p>Servlet specific class for multipart content support.</p>
* <p>Use {@link #from(ServletRequest)} to
* parse multipart request content into a {@link Parts} object that can
* be used to access Servlet {@link Part} objects.</p>
*
* @see Parts
*/
public class ServletMultiPartFormData
{
/**
* Get future {@link ServletMultiPartFormData.Parts} from a servlet request.
* @param servletRequest A servlet request
* @return A future {@link ServletMultiPartFormData.Parts}, which may have already been created and/or completed.
* @see #from(ServletRequest, String)
*/
public static CompletableFuture<Parts> from(ServletRequest servletRequest)
{
return from(servletRequest, servletRequest.getContentType());
}
/**
* Get future {@link ServletMultiPartFormData.Parts} from a servlet request.
* @param servletRequest A servlet request
* @param contentType The contentType, passed as an optimization as it has likely already been retrieved.
* @return A future {@link ServletMultiPartFormData.Parts}, which may have already been created and/or completed.
*/
public static CompletableFuture<Parts> from(ServletRequest servletRequest, String contentType)
{
// Look for an existing future (we use the future here rather than the parts as it can remember any failure).
@SuppressWarnings("unchecked")
CompletableFuture<Parts> futureServletParts = (CompletableFuture<Parts>)servletRequest.getAttribute(ServletMultiPartFormData.class.getName());
if (futureServletParts == null)
{
// No existing parts, so we need to try to read them ourselves
// Is this servlet a valid target for Multipart?
MultipartConfigElement config = (MultipartConfigElement)servletRequest.getAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT);
if (config == null)
return CompletableFuture.failedFuture(new IllegalStateException("No multipart configuration element"));
// Are we the right content type to produce our own parts?
if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpField.getValueParameters(contentType, null)))
return CompletableFuture.failedFuture(new IllegalStateException("Not multipart Content-Type"));
// Do we have a boundary?
String boundary = MultiPart.extractBoundary(servletRequest.getContentType());
if (boundary == null)
return CompletableFuture.failedFuture(new IllegalStateException("No multipart boundary parameter in Content-Type"));
// Can we access the core request, needed for components (eg buffer pools, temp directory, etc.) as well
// as IO optimization
ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(servletRequest);
if (servletContextRequest == null)
return CompletableFuture.failedFuture(new IllegalStateException("No core request"));
// Get a temporary directory for larger parts.
File filesDirectory = StringUtil.isBlank(config.getLocation())
? servletContextRequest.getContext().getTempDirectory()
: new File(config.getLocation());
// Look for an existing future MultiPartFormData.Parts
CompletableFuture<MultiPartFormData.Parts> futureFormData = MultiPartFormData.from(servletContextRequest, boundary, parser ->
{
try
{
// No existing core parts, so we need to configure the parser.
ServletContextHandler contextHandler = servletContextRequest.getServletContext().getServletContextHandler();
ByteBufferPool byteBufferPool = servletContextRequest.getComponents().getByteBufferPool();
ConnectionMetaData connectionMetaData = servletContextRequest.getConnectionMetaData();
Connection connection = connectionMetaData.getConnection();
Content.Source source;
if (servletRequest instanceof ServletApiRequest servletApiRequest)
{
source = servletApiRequest.getRequest();
}
else
{
int bufferSize = connection instanceof AbstractConnection c ? c.getInputBufferSize() : 2048;
InputStreamContentSource iscs = new InputStreamContentSource(servletRequest.getInputStream(), byteBufferPool);
iscs.setBufferSize(bufferSize);
source = iscs;
}
parser.setMaxParts(contextHandler.getMaxFormKeys());
parser.setFilesDirectory(filesDirectory.toPath());
parser.setMaxMemoryFileSize(config.getFileSizeThreshold());
parser.setMaxFileSize(config.getMaxFileSize());
parser.setMaxLength(config.getMaxRequestSize());
parser.setPartHeadersMaxLength(connectionMetaData.getHttpConfiguration().getRequestHeaderSize());
// parse the core parts.
return parser.parse(source);
}
catch (Throwable failure)
{
return CompletableFuture.failedFuture(failure);
}
});
// When available, convert the core parts to servlet parts
futureServletParts = futureFormData.thenApply(formDataParts -> new Parts(filesDirectory.toPath(), formDataParts));
// cache the result in attributes.
servletRequest.setAttribute(ServletMultiPartFormData.class.getName(), futureServletParts);
}
return futureServletParts;
}
/**
* <p>An ordered list of {@link Part}s that can be accessed by name.</p>
*/
public static class Parts
{
private final List<Part> parts = new ArrayList<>();
public Parts(Path directory, MultiPartFormData.Parts parts)
{
parts.forEach(part -> this.parts.add(new ServletPart(directory, part)));
}
public Part getPart(String name)
{
return parts.stream()
.filter(part -> part.getName().equals(name))
.findFirst()
.orElse(null);
}
public Collection<Part> getParts()
{
return List.copyOf(parts);
}
}
private static class ServletPart implements Part
{
private final Path _directory;
private final MultiPart.Part _part;
private ServletPart(Path directory, MultiPart.Part part)
{
_directory = directory;
_part = part;
}
@Override
public InputStream getInputStream() throws IOException
{
return Content.Source.asInputStream(_part.newContentSource());
}
@Override
public String getContentType()
{
return _part.getHeaders().get(HttpHeader.CONTENT_TYPE);
}
@Override
public String getName()
{
return _part.getName();
}
@Override
public String getSubmittedFileName()
{
return _part.getFileName();
}
@Override
public long getSize()
{
return _part.getLength();
}
@Override
public void write(String fileName) throws IOException
{
Path filePath = Path.of(fileName);
if (!filePath.isAbsolute() && Files.isDirectory(_directory))
filePath = _directory.resolve(filePath).normalize();
_part.writeTo(filePath);
}
@Override
public void delete() throws IOException
{
_part.delete();
}
@Override
public String getHeader(String name)
{
return _part.getHeaders().get(name);
}
@Override
public Collection<String> getHeaders(String name)
{
return _part.getHeaders().getValuesList(name);
}
@Override
public Collection<String> getHeaderNames()
{
return _part.getHeaders().getFieldNamesCollection();
}
@Override
public String toString()
{
return "%s@%x[part=%s]".formatted(
getClass().getSimpleName(),
hashCode(),
_part
);
}
}
}