violations)
+ {
+ return new MultiPartCompliance("CUSTOM" + __custom.getAndIncrement(), violations);
+ }
+
+ /**
+ * Create compliance set from string.
+ *
+ * Format: {@code [,[-]]...}
+ *
+ * BASE is one of:
+ *
+ * - 0
- No {@link MultiPartCompliance.Violation}s
+ * - *
- All {@link MultiPartCompliance.Violation}s
+ * - <name>
- The name of a static instance of MultiPartCompliance (e.g. {@link MultiPartCompliance#LEGACY}).
+ *
+ *
+ * The remainder of the list can contain then names of {@link MultiPartCompliance.Violation}s to include them in the mode, or prefixed
+ * with a '-' to exclude them from the mode. Examples are:
+ *
+ *
+ * - {@code 0,CONTENT_TRANSFER_ENCODING}
- Only allow {@link MultiPartCompliance.Violation#CONTENT_TRANSFER_ENCODING}
+ * - {@code *,-BASE64_TRANSFER_ENCODING}
- Only all except {@link MultiPartCompliance.Violation#BASE64_TRANSFER_ENCODING}
+ * - {@code LEGACY,BASE64_TRANSFER_ENCODING}
- Same as LEGACY plus {@link MultiPartCompliance.Violation#BASE64_TRANSFER_ENCODING}
+ *
+ *
+ * @param spec A string describing the compliance
+ * @return the MultiPartCompliance instance derived from the string description
+ */
+ public static MultiPartCompliance from(String spec)
+ {
+ MultiPartCompliance compliance = valueOf(spec);
+ if (compliance == null)
+ {
+ String[] elements = spec.split("\\s*,\\s*");
+
+ Set violations = switch (elements[0])
+ {
+ case "0" -> noneOf(MultiPartCompliance.Violation.class);
+ case "*" -> allOf(MultiPartCompliance.Violation.class);
+ default ->
+ {
+ MultiPartCompliance mode = MultiPartCompliance.valueOf(elements[0]);
+ yield (mode == null) ? noneOf(MultiPartCompliance.Violation.class) : copyOf(mode.getAllowed());
+ }
+ };
+
+ for (int i = 1; i < elements.length; i++)
+ {
+ String element = elements[i];
+ boolean exclude = element.startsWith("-");
+ if (exclude)
+ element = element.substring(1);
+
+ MultiPartCompliance.Violation section = MultiPartCompliance.Violation.valueOf(element);
+ if (exclude)
+ violations.remove(section);
+ else
+ violations.add(section);
+ }
+
+ compliance = new MultiPartCompliance("CUSTOM" + __custom.getAndIncrement(), violations);
+ }
+ return compliance;
+ }
+
+ private static Set copyOf(Set violations)
+ {
+ if (violations == null || violations.isEmpty())
+ return EnumSet.noneOf(MultiPartCompliance.Violation.class);
+ return EnumSet.copyOf(violations);
+ }
+
private final String name;
private final Set violations;
@@ -92,13 +198,13 @@ public boolean allows(ComplianceViolation violation)
}
@Override
- public Set extends ComplianceViolation> getKnown()
+ public Set getKnown()
{
return EnumSet.allOf(Violation.class);
}
@Override
- public Set extends ComplianceViolation> getAllowed()
+ public Set getAllowed()
{
return violations;
}
@@ -106,6 +212,8 @@ public Set extends ComplianceViolation> getAllowed()
@Override
public String toString()
{
- return String.format("%s@%x%s", name, hashCode(), violations);
+ if (this == RFC7578 || this == LEGACY)
+ return name;
+ return String.format("%s@%x(v=%s)", name, hashCode(), violations);
}
}
diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java
index 7e3b43efd664..e1cae2531fa7 100644
--- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java
+++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java
@@ -32,6 +32,7 @@
import org.eclipse.jetty.io.content.ContentSourceCompletableFuture;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -78,21 +79,30 @@ private MultiPartFormData()
}
/**
- * @deprecated use {@link #from(Attributes, ComplianceViolation.Listener, String, Function)} instead. This method will be removed in Jetty 12.1.0
+ * Returns {@code multipart/form-data} parts using {@link MultiPartCompliance#RFC7578}.
*/
- @Deprecated(since = "12.0.6", forRemoval = true)
public static CompletableFuture from(Attributes attributes, String boundary, Function> parse)
{
- return from(attributes, ComplianceViolation.Listener.NOOP, boundary, parse);
+ return from(attributes, MultiPartCompliance.RFC7578, ComplianceViolation.Listener.NOOP, boundary, parse);
}
- public static CompletableFuture from(Attributes attributes, ComplianceViolation.Listener listener, String boundary, Function> parse)
+ /**
+ * Returns {@code multipart/form-data} parts using the given {@link MultiPartCompliance} and listener.
+ *
+ * @param attributes the attributes where the futureParts are tracked
+ * @param compliance the compliance mode
+ * @param listener the compliance violation listener
+ * @param boundary the boundary for the {@code multipart/form-data} parts
+ * @param parse the parser completable future
+ * @return the future parts
+ */
+ public static CompletableFuture from(Attributes attributes, MultiPartCompliance compliance, ComplianceViolation.Listener listener, String boundary, Function> parse)
{
@SuppressWarnings("unchecked")
CompletableFuture futureParts = (CompletableFuture)attributes.getAttribute(MultiPartFormData.class.getName());
if (futureParts == null)
{
- futureParts = parse.apply(new Parser(boundary, listener));
+ futureParts = parse.apply(new Parser(boundary, compliance, listener));
attributes.setAttribute(MultiPartFormData.class.getName(), futureParts);
}
return futureParts;
@@ -209,7 +219,8 @@ public static class Parser
{
private final PartsListener listener = new PartsListener();
private final MultiPart.Parser parser;
- private ComplianceViolation.Listener complianceViolationListener;
+ private final MultiPartCompliance compliance;
+ private final ComplianceViolation.Listener complianceListener;
private boolean useFilesForPartsWithoutFileName;
private Path filesDirectory;
private long maxFileSize = -1;
@@ -220,13 +231,14 @@ public static class Parser
public Parser(String boundary)
{
- this(boundary, null);
+ this(boundary, MultiPartCompliance.RFC7578, ComplianceViolation.Listener.NOOP);
}
- public Parser(String boundary, ComplianceViolation.Listener complianceViolationListener)
+ public Parser(String boundary, MultiPartCompliance multiPartCompliance, ComplianceViolation.Listener complianceViolationListener)
{
- parser = new MultiPart.Parser(Objects.requireNonNull(boundary), listener);
- this.complianceViolationListener = complianceViolationListener != null ? complianceViolationListener : ComplianceViolation.Listener.NOOP;
+ compliance = Objects.requireNonNull(multiPartCompliance);
+ complianceListener = Objects.requireNonNull(complianceViolationListener);
+ parser = new MultiPart.Parser(Objects.requireNonNull(boundary), compliance, listener);
}
public CompletableFuture parse(Content.Source content)
@@ -533,11 +545,38 @@ public void onPart(String name, String fileName, HttpFields headers)
memoryFileSize = 0;
try (AutoLock ignored = lock.lock())
{
- if (headers.contains("content-transfer-encoding"))
+ // Content-Transfer-Encoding is not a multi-valued field.
+ String value = headers.get(HttpHeader.CONTENT_TRANSFER_ENCODING);
+ if (value != null)
{
- String value = headers.get("content-transfer-encoding");
- if (!"8bit".equalsIgnoreCase(value) && !"binary".equalsIgnoreCase(value))
- complianceViolationListener.onComplianceViolation(new ComplianceViolation.Event(MultiPartCompliance.RFC7578, MultiPartCompliance.Violation.CONTENT_TRANSFER_ENCODING, value));
+ switch (StringUtil.asciiToLowerCase(value))
+ {
+ case "base64" ->
+ {
+ complianceListener.onComplianceViolation(
+ new ComplianceViolation.Event(compliance,
+ MultiPartCompliance.Violation.BASE64_TRANSFER_ENCODING,
+ value));
+ }
+ case "quoted-printable" ->
+ {
+ complianceListener.onComplianceViolation(
+ new ComplianceViolation.Event(compliance,
+ MultiPartCompliance.Violation.QUOTED_PRINTABLE_TRANSFER_ENCODING,
+ value));
+ }
+ case "8bit", "binary" ->
+ {
+ // ignore
+ }
+ default ->
+ {
+ complianceListener.onComplianceViolation(
+ new ComplianceViolation.Event(compliance,
+ MultiPartCompliance.Violation.CONTENT_TRANSFER_ENCODING,
+ value));
+ }
+ }
}
MultiPart.Part part;
@@ -594,6 +633,21 @@ public void onFailure(Throwable failure)
fail(failure);
}
+ @Override
+ public void onViolation(MultiPartCompliance.Violation violation)
+ {
+ try
+ {
+ ComplianceViolation.Event event = new ComplianceViolation.Event(compliance, violation, "multipart spec violation");
+ complianceListener.onComplianceViolation(event);
+ }
+ catch (Throwable x)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("failure while notifying listener {}", complianceListener, x);
+ }
+ }
+
private void fail(Throwable cause)
{
List partsToFail;
diff --git a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartCaptureTest.java b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartCaptureTest.java
index dabe245e146f..7469cb2af516 100644
--- a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartCaptureTest.java
+++ b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartCaptureTest.java
@@ -13,294 +13,154 @@
package org.eclipse.jetty.http;
-import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
import java.nio.file.Path;
-import java.security.DigestOutputStream;
-import java.security.MessageDigest;
import java.util.ArrayList;
-import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Stream;
import org.eclipse.jetty.io.Content;
-import org.eclipse.jetty.toolchain.test.Hex;
-import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.io.content.ByteBufferContentSource;
+import org.eclipse.jetty.tests.multipart.MultiPartExpectations;
+import org.eclipse.jetty.tests.multipart.MultiPartFormArgumentsProvider;
+import org.eclipse.jetty.tests.multipart.MultiPartRequest;
+import org.eclipse.jetty.tests.multipart.MultiPartResults;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenPaths;
import org.eclipse.jetty.util.BufferUtil;
-import org.eclipse.jetty.util.IO;
-import org.eclipse.jetty.util.Promise;
-import org.eclipse.jetty.util.QuotedStringTokenizer;
-import org.eclipse.jetty.util.StringUtil;
-import org.hamcrest.Matchers;
import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.greaterThan;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.notNullValue;
+import org.junit.jupiter.params.provider.ArgumentsSource;
public class MultiPartCaptureTest
{
- public static Stream data()
- {
- return Stream.of(
- // == Arbitrary / Non-Standard Examples ==
-
- "multipart-uppercase",
- // "multipart-base64", // base64 transfer encoding deprecated
- // "multipart-base64-long", // base64 transfer encoding deprecated
-
- // == Capture of raw request body contents from Apache HttpClient 4.5.5 ==
-
- "browser-capture-company-urlencoded-apache-httpcomp",
- "browser-capture-complex-apache-httpcomp",
- "browser-capture-duplicate-names-apache-httpcomp",
- "browser-capture-encoding-mess-apache-httpcomp",
- "browser-capture-nested-apache-httpcomp",
- "browser-capture-nested-binary-apache-httpcomp",
- "browser-capture-number-only2-apache-httpcomp",
- "browser-capture-number-only-apache-httpcomp",
- "browser-capture-sjis-apache-httpcomp",
- "browser-capture-strange-quoting-apache-httpcomp",
- "browser-capture-text-files-apache-httpcomp",
- "browser-capture-unicode-names-apache-httpcomp",
- "browser-capture-zalgo-text-plain-apache-httpcomp",
-
- // == Capture of raw request body contents from Eclipse Jetty Http Client 9.4.9 ==
-
- "browser-capture-complex-jetty-client",
- "browser-capture-duplicate-names-jetty-client",
- "browser-capture-encoding-mess-jetty-client",
- "browser-capture-nested-jetty-client",
- "browser-capture-number-only-jetty-client",
- "browser-capture-sjis-jetty-client",
- "browser-capture-text-files-jetty-client",
- "browser-capture-unicode-names-jetty-client",
- "browser-capture-whitespace-only-jetty-client",
-
- // == Capture of raw request body contents from various browsers ==
-
- // simple form - 2 fields
- "browser-capture-form1-android-chrome",
- "browser-capture-form1-android-firefox",
- "browser-capture-form1-chrome",
- "browser-capture-form1-edge",
- "browser-capture-form1-firefox",
- "browser-capture-form1-ios-safari",
- "browser-capture-form1-msie",
- "browser-capture-form1-osx-safari",
-
- // form submitted as shift-jis
- "browser-capture-sjis-form-edge",
- "browser-capture-sjis-form-msie",
- // TODO: these might be addressable via Issue #2398
- // "browser-capture-sjis-form-android-chrome", // contains html encoded character and unspecified charset defaults to utf-8
- // "browser-capture-sjis-form-android-firefox", // contains html encoded character and unspecified charset defaults to utf-8
- // "browser-capture-sjis-form-chrome", // contains html encoded character and unspecified charset defaults to utf-8
- // "browser-capture-sjis-form-firefox", // contains html encoded character and unspecified charset defaults to utf-8
- // "browser-capture-sjis-form-ios-safari", // contains html encoded character and unspecified charset defaults to utf-8
- // "browser-capture-sjis-form-safari", // contains html encoded character and unspecified charset defaults to utf-8
-
- // form submitted as shift-jis (with HTML5 specific hidden _charset_ field)
- "browser-capture-sjis-charset-form-android-chrome", // contains html encoded character
- "browser-capture-sjis-charset-form-android-firefox", // contains html encoded character
- "browser-capture-sjis-charset-form-chrome", // contains html encoded character
- "browser-capture-sjis-charset-form-edge",
- "browser-capture-sjis-charset-form-firefox", // contains html encoded character
- "browser-capture-sjis-charset-form-ios-safari", // contains html encoded character
- "browser-capture-sjis-charset-form-msie",
- "browser-capture-sjis-charset-form-safari", // contains html encoded character
-
- // form submitted with simple file upload
- "browser-capture-form-fileupload-android-chrome",
- "browser-capture-form-fileupload-android-firefox",
- "browser-capture-form-fileupload-chrome",
- "browser-capture-form-fileupload-edge",
- "browser-capture-form-fileupload-firefox",
- "browser-capture-form-fileupload-ios-safari",
- "browser-capture-form-fileupload-msie",
- "browser-capture-form-fileupload-safari",
-
- // form submitted with 2 files (1 binary, 1 text) and 2 text fields
- "browser-capture-form-fileupload-alt-chrome",
- "browser-capture-form-fileupload-alt-edge",
- "browser-capture-form-fileupload-alt-firefox",
- "browser-capture-form-fileupload-alt-msie",
- "browser-capture-form-fileupload-alt-safari"
- ).map(Arguments::of);
- }
-
@ParameterizedTest
- @MethodSource("data")
- public void testMultipartCapture(String fileName) throws Exception
+ @ArgumentsSource(MultiPartFormArgumentsProvider.class)
+ public void testMultipartCapture(MultiPartRequest formRequest, Charset defaultCharset, MultiPartExpectations formExpectations) throws Exception
{
- Path rawPath = MavenTestingUtils.getTestResourcePathFile("multipart/" + fileName + ".raw");
- Path expectationPath = MavenTestingUtils.getTestResourcePathFile("multipart/" + fileName + ".expected.txt");
- MultiPartExpectations expectations = new MultiPartExpectations(expectationPath);
-
- String boundaryAttribute = "boundary=";
- int boundaryIndex = expectations.contentType.indexOf(boundaryAttribute);
- assertThat(boundaryIndex, greaterThan(0));
- String boundary = HttpField.PARAMETER_TOKENIZER.unquote(expectations.contentType.substring(boundaryIndex + boundaryAttribute.length()));
-
- TestPartsListener listener = new TestPartsListener(expectations);
+ String boundary = MultiPart.extractBoundary(formExpectations.getContentType());
+ TestPartsListener listener = new TestPartsListener();
MultiPart.Parser parser = new MultiPart.Parser(boundary, listener);
- parser.parse(Content.Chunk.from(ByteBuffer.wrap(Files.readAllBytes(rawPath)), true));
- listener.assertParts();
+ ByteBuffer rawByteBuffer = formRequest.asByteBuffer();
+ parser.parse(Content.Chunk.from(rawByteBuffer, true));
+ formExpectations.assertParts(mapActualResults(listener.parts), defaultCharset);
}
- private record NameValue(String name, String value)
+ @ParameterizedTest
+ @ArgumentsSource(MultiPartFormArgumentsProvider.class)
+ public void testMultiPartFormDataParse(MultiPartRequest formRequest, Charset defaultCharset, MultiPartExpectations formExpectations) throws Exception
{
+ String boundary = MultiPart.extractBoundary(formExpectations.getContentType());
+ Path tempDir = MavenPaths.targetTestDir(MultiPartCaptureTest.class.getSimpleName() + "-temp");
+ FS.ensureDirExists(tempDir);
+
+ MultiPartFormData.Parser parser = new MultiPartFormData.Parser(boundary);
+ parser.setUseFilesForPartsWithoutFileName(false);
+ parser.setFilesDirectory(tempDir);
+ ByteBufferContentSource contentSource = new ByteBufferContentSource(formRequest.asByteBuffer());
+ MultiPartFormData.Parts parts = parser.parse(contentSource).get();
+ formExpectations.assertParts(mapActualResults(parts), defaultCharset);
}
- private static class MultiPartExpectations
+ private MultiPartResults mapActualResults(final MultiPartFormData.Parts parts)
{
- public final String contentType;
- public final int partCount;
- public final List partFilenames = new ArrayList<>();
- public final List partSha1Sums = new ArrayList<>();
- public final List partContainsContents = new ArrayList<>();
-
- public MultiPartExpectations(Path expectationsPath) throws IOException
+ return new MultiPartResults()
{
- String parsedContentType = null;
- String parsedPartCount = "-1";
+ @Override
+ public int getCount()
+ {
+ return parts.size();
+ }
- try (BufferedReader reader = Files.newBufferedReader(expectationsPath))
+ @Override
+ public List get(String name)
{
- String line;
- while ((line = reader.readLine()) != null)
- {
- line = line.trim();
- if (StringUtil.isBlank(line) || line.startsWith("#"))
- {
- // skip blanks and comments
- continue;
- }
+ List namedParts = parts.getAll(name);
+
+ if (namedParts == null)
+ return null;
- String[] split = line.split("\\|");
- switch (split[0])
- {
- case "Request-Header":
- if (split[1].equalsIgnoreCase("Content-Type"))
- {
- parsedContentType = split[2];
- }
- break;
- case "Content-Type":
- parsedContentType = split[1];
- break;
- case "Parts-Count":
- parsedPartCount = split[1];
- break;
- case "Part-ContainsContents":
- {
- NameValue pair = new NameValue(split[1], split[2]);
- partContainsContents.add(pair);
- break;
- }
- case "Part-Filename":
- {
- NameValue pair = new NameValue(split[1], split[2]);
- partFilenames.add(pair);
- break;
- }
- case "Part-Sha1sum":
- {
- NameValue pair = new NameValue(split[1], split[2]);
- partSha1Sums.add(pair);
- break;
- }
- default:
- throw new IOException("Bad Line in " + expectationsPath + ": " + line);
- }
+ List results = new ArrayList<>();
+ for (MultiPart.Part namedPart : namedParts)
+ {
+ results.add(new NamedPartResult(namedPart));
}
+ return results;
}
+ };
+ }
- Objects.requireNonNull(parsedContentType, "Missing required 'Content-Type' declaration: " + expectationsPath);
- this.contentType = parsedContentType;
- this.partCount = Integer.parseInt(parsedPartCount);
- }
-
- private void assertParts(Map> allParts) throws Exception
+ private MultiPartResults mapActualResults(final Map> parts)
+ {
+ return new MultiPartResults()
{
- if (partCount >= 0)
- assertThat(allParts.values().stream().mapToInt(List::size).sum(), is(partCount));
-
- String defaultCharset = UTF_8.toString();
- List charSetParts = allParts.get("_charset_");
- if (charSetParts != null)
+ @Override
+ public int getCount()
{
- defaultCharset = Promise.Completable.with(p -> Content.Source.asString(charSetParts.get(0).getContentSource(), StandardCharsets.US_ASCII, p))
- .get();
+ return parts.values().stream().mapToInt(List::size).sum();
}
- for (NameValue expected : partContainsContents)
+ @Override
+ public List get(String name)
{
- List parts = allParts.get(expected.name);
- assertThat("Part[" + expected.name + "]", parts, is(notNullValue()));
- MultiPart.Part part = parts.get(0);
- String charset = getCharsetFromContentType(part.getHeaders().get(HttpHeader.CONTENT_TYPE), defaultCharset);
- String partContent = Content.Source.asString(part.newContentSource(), Charset.forName(charset));
- assertThat("Part[" + expected.name + "].contents", partContent, containsString(expected.value));
- }
+ List namedParts = parts.get(name);
- // Evaluate expected filenames
- for (NameValue expected : partFilenames)
- {
- List parts = allParts.get(expected.name);
- assertThat("Part[" + expected.name + "]", parts, is(notNullValue()));
- MultiPart.Part part = parts.get(0);
- assertThat("Part[" + expected.name + "]", part.getFileName(), is(expected.value));
- }
+ if (namedParts == null)
+ return null;
- // Evaluate expected contents checksums
- for (NameValue expected : partSha1Sums)
- {
- List parts = allParts.get(expected.name);
- assertThat("Part[" + expected.name + "]", parts, is(notNullValue()));
- MultiPart.Part part = parts.get(0);
- MessageDigest digest = MessageDigest.getInstance("SHA1");
- try (InputStream partInputStream = Content.Source.asInputStream(part.newContentSource());
- DigestOutputStream digester = new DigestOutputStream(OutputStream.nullOutputStream(), digest))
+ List results = new ArrayList<>();
+ for (MultiPart.Part namedPart : namedParts)
{
- IO.copy(partInputStream, digester);
- String actualSha1sum = Hex.asHex(digest.digest()).toLowerCase(Locale.US);
- assertThat("Part[" + expected.name + "].sha1sum", actualSha1sum, Matchers.equalToIgnoringCase(expected.value));
+ results.add(new NamedPartResult(namedPart));
}
+ return results;
}
+ };
+ }
+
+ public static class NamedPartResult implements MultiPartResults.PartResult
+ {
+ private final MultiPart.Part namedPart;
+
+ public NamedPartResult(MultiPart.Part namedPart)
+ {
+ this.namedPart = namedPart;
}
- private String getCharsetFromContentType(String contentType, String defaultCharset)
+ @Override
+ public String getContentType()
{
- if (StringUtil.isBlank(contentType))
- return defaultCharset;
+ return namedPart.getHeaders().get(HttpHeader.CONTENT_TYPE);
+ }
- QuotedStringTokenizer tok = QuotedStringTokenizer.builder().delimiters(";").ignoreOptionalWhiteSpace().build();
- for (Iterator i = tok.tokenize(contentType); i.hasNext();)
- {
- String str = i.next().trim();
- if (str.startsWith("charset="))
- {
- return str.substring("charset=".length());
- }
- }
+ @Override
+ public ByteBuffer asByteBuffer() throws IOException
+ {
+ return Content.Source.asByteBuffer(namedPart.newContentSource());
+ }
+
+ @Override
+ public String asString(Charset charset) throws IOException
+ {
+ if (charset == null)
+ return Content.Source.asString(namedPart.newContentSource());
+ else
+ return Content.Source.asString(namedPart.newContentSource(), charset);
+ }
+
+ @Override
+ public String getFileName()
+ {
+ return namedPart.getFileName();
+ }
- return defaultCharset;
+ @Override
+ public InputStream asInputStream()
+ {
+ return Content.Source.asInputStream(namedPart.newContentSource());
}
}
@@ -309,12 +169,6 @@ private static class TestPartsListener extends MultiPart.AbstractPartsListener
// Preserve parts order.
private final Map> parts = new LinkedHashMap<>();
private final List partByteBuffers = new ArrayList<>();
- private final MultiPartExpectations expectations;
-
- private TestPartsListener(MultiPartExpectations expectations)
- {
- this.expectations = expectations;
- }
@Override
public void onPartContent(Content.Chunk chunk)
@@ -326,14 +180,14 @@ public void onPartContent(Content.Chunk chunk)
@Override
public void onPart(String name, String fileName, HttpFields headers)
{
- MultiPart.Part newPart = new MultiPart.ByteBufferPart(name, fileName, headers, List.copyOf(partByteBuffers));
+ List copyOfByteBuffers = new ArrayList<>();
+ for (ByteBuffer capture: partByteBuffers)
+ {
+ copyOfByteBuffers.add(BufferUtil.copy(capture));
+ }
+ MultiPart.Part newPart = new MultiPart.ByteBufferPart(name, fileName, headers, copyOfByteBuffers);
partByteBuffers.clear();
parts.compute(newPart.getName(), (k, v) -> v == null ? new ArrayList<>() : v).add(newPart);
}
-
- private void assertParts() throws Exception
- {
- expectations.assertParts(parts);
- }
}
}
diff --git a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormDataTest.java b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormDataTest.java
index b3e2fb295612..13878b3a0a69 100644
--- a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormDataTest.java
+++ b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormDataTest.java
@@ -14,6 +14,7 @@
package org.eclipse.jetty.http;
import java.io.ByteArrayInputStream;
+import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
@@ -42,9 +43,11 @@
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.containsStringIgnoringCase;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -56,6 +59,8 @@
public class MultiPartFormDataTest
{
+ private static final String CR = "\r";
+ private static final String LF = "\n";
private static final AtomicInteger testCounter = new AtomicInteger();
private Path _tmpDir;
@@ -242,6 +247,316 @@ public void testEmptyStringBoundary() throws Exception
}
}
+ @Test
+ public void testContentTransferEncodingQuotedPrintable() throws Exception
+ {
+ String boundary = "BEEF";
+ String str = """
+ --$B\r
+ Content-Disposition: form-data; name="greeting"\r
+ Content-Type: text/plain; charset=US-ASCII\r
+ Content-Transfer-Encoding: quoted-printable\r
+ \r
+ Hello World\r
+ --$B--\r
+ """.replace("$B", boundary);
+
+ AsyncContent source = new TestContent();
+ CaptureMultiPartViolations violations = new CaptureMultiPartViolations();
+ MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary, MultiPartCompliance.RFC7578, violations);
+ formData.setFilesDirectory(_tmpDir);
+ formData.setMaxMemoryFileSize(-1);
+ Content.Sink.write(source, true, str, Callback.NOOP);
+
+ try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
+ {
+ assertThat(parts.size(), is(1));
+
+ MultiPart.Part greeting = parts.getFirst("greeting");
+ assertThat(greeting, notNullValue());
+ Content.Source partContent = greeting.getContentSource();
+ assertThat(partContent.getLength(), is(11L));
+ assertThat(Content.Source.asString(partContent), is("Hello World"));
+
+ List events = violations.getEvents();
+ assertThat(events.size(), is(1));
+ ComplianceViolation.Event event = events.get(0);
+ assertThat(event.violation(), is(MultiPartCompliance.Violation.QUOTED_PRINTABLE_TRANSFER_ENCODING));
+ }
+ }
+
+ @Test
+ public void testLFOnlyNoCRInPreviousChunk() throws Exception
+ {
+ String str1 = """
+ --BEEF\r
+ Content-Disposition: form-data; name="greeting"\r
+ Content-Type: text/plain; charset=US-ASCII\r
+ \r
+ """;
+ String str2 = "Hello World"; // not ending with CR
+ String str3 = """
+ \n--BEEF--\r
+ """;
+
+ AsyncContent source = new TestContent();
+ CaptureMultiPartViolations violations = new CaptureMultiPartViolations();
+ MultiPartFormData.Parser formData = new MultiPartFormData.Parser("BEEF", MultiPartCompliance.RFC7578, violations);
+ formData.setMaxMemoryFileSize(-1);
+ Content.Sink.write(source, false, str1, Callback.NOOP);
+ Content.Sink.write(source, false, str2, Callback.NOOP);
+ Content.Sink.write(source, true, str3, Callback.NOOP);
+
+ try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
+ {
+ assertThat(parts.size(), is(1));
+
+ MultiPart.Part greeting = parts.getFirst("greeting");
+ assertThat(greeting, notNullValue());
+ Content.Source partContent = greeting.getContentSource();
+ assertThat(partContent.getLength(), is(11L));
+ assertThat(Content.Source.asString(partContent), is("Hello World"));
+
+ List events = violations.getEvents();
+ assertThat(events.size(), is(1));
+ ComplianceViolation.Event event = events.get(0);
+ assertThat(event.violation(), is(MultiPartCompliance.Violation.LF_LINE_TERMINATION));
+ }
+ }
+
+ @Test
+ public void testLFOnlyNoCRInCurrentChunk() throws Exception
+ {
+ String str1 = """
+ --BEEF\r
+ Content-Disposition: form-data; name="greeting"\r
+ Content-Type: text/plain; charset=US-ASCII\r
+ \r
+ """;
+ // Do not end Hello World with "\r".
+ String str2 = """
+ Hello World\n--BEEF--\r
+ """;
+
+ AsyncContent source = new TestContent();
+ CaptureMultiPartViolations violations = new CaptureMultiPartViolations();
+ MultiPartFormData.Parser formData = new MultiPartFormData.Parser("BEEF", MultiPartCompliance.RFC7578, violations);
+ formData.setMaxMemoryFileSize(-1);
+ Content.Sink.write(source, false, str1, Callback.NOOP);
+ Content.Sink.write(source, true, str2, Callback.NOOP);
+
+ try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
+ {
+ assertThat(parts.size(), is(1));
+
+ MultiPart.Part greeting = parts.getFirst("greeting");
+ assertThat(greeting, notNullValue());
+ Content.Source partContent = greeting.getContentSource();
+ assertThat(partContent.getLength(), is(11L));
+ assertThat(Content.Source.asString(partContent), is("Hello World"));
+
+ List events = violations.getEvents();
+ assertThat(events.size(), is(1));
+ ComplianceViolation.Event event = events.get(0);
+ assertThat(event.violation(), is(MultiPartCompliance.Violation.LF_LINE_TERMINATION));
+ }
+ }
+
+ @Test
+ public void testLFOnlyEOLLenient() throws Exception
+ {
+ String boundary = "BEEF";
+ String str = """
+ --$B
+ Content-Disposition: form-data; name="greeting"
+ Content-Type: text/plain; charset=US-ASCII
+
+ Hello World
+ --$B--
+ """.replace("$B", boundary);
+
+ assertThat("multipart str cannot contain CR for this test", str, not(containsString(CR)));
+
+ AsyncContent source = new TestContent();
+ CaptureMultiPartViolations violations = new CaptureMultiPartViolations();
+ MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary, MultiPartCompliance.RFC7578, violations);
+ formData.setMaxMemoryFileSize(-1);
+ Content.Sink.write(source, true, str, Callback.NOOP);
+
+ try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
+ {
+ assertThat(parts.size(), is(1));
+
+ MultiPart.Part greeting = parts.getFirst("greeting");
+ assertThat(greeting, notNullValue());
+ Content.Source partContent = greeting.getContentSource();
+ assertThat(partContent.getLength(), is(11L));
+ assertThat(Content.Source.asString(partContent), is("Hello World"));
+
+ List events = violations.getEvents();
+ assertThat(events.size(), is(1));
+ ComplianceViolation.Event event = events.get(0);
+ assertThat(event.violation(), is(MultiPartCompliance.Violation.LF_LINE_TERMINATION));
+ }
+ }
+
+ @Test
+ public void testLFOnlyEOLStrict()
+ {
+ String boundary = "BEEF";
+ String str = """
+ --$B
+ Content-Disposition: form-data; name="greeting"
+ Content-Type: text/plain; charset=US-ASCII
+
+ Hello World
+ --$B--
+ """.replace("$B", boundary);
+
+ assertThat("multipart str cannot contain CR for this test", str, not(containsString(CR)));
+
+ AsyncContent source = new TestContent();
+ CaptureMultiPartViolations violations = new CaptureMultiPartViolations();
+ MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary, MultiPartCompliance.RFC7578_STRICT, violations);
+ formData.setMaxMemoryFileSize(-1);
+ Content.Sink.write(source, true, str, Callback.NOOP);
+
+ ExecutionException ee = assertThrows(ExecutionException.class, () -> formData.parse(source).get(5, TimeUnit.SECONDS));
+ assertThat(ee.getCause(), instanceOf(BadMessageException.class));
+ BadMessageException bme = (BadMessageException)ee.getCause();
+ assertThat(bme.getMessage(), containsString("invalid LF-only EOL"));
+ }
+
+ /**
+ * Test of parsing where there is whitespace before the boundary.
+ *
+ * @see MultiPartCompliance.Violation#WHITESPACE_BEFORE_BOUNDARY
+ */
+ @Test
+ public void testWhiteSpaceBeforeBoundary()
+ {
+ String boundary = "BEEF";
+ String str = """
+ preamble\r
+ --$B\r
+ Content-Disposition: form-data; name="greeting"\r
+ Content-Type: text/plain; charset=US-ASCII\r
+ \r
+ Hello World\r
+ --$B--\r
+ """.replace("$B", boundary);
+
+ AsyncContent source = new TestContent();
+ CaptureMultiPartViolations violations = new CaptureMultiPartViolations();
+ MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary, MultiPartCompliance.RFC7578, violations);
+ formData.setFilesDirectory(_tmpDir);
+ formData.setMaxMemoryFileSize(-1);
+ Content.Sink.write(source, true, str, Callback.NOOP);
+
+ ExecutionException ee = assertThrows(ExecutionException.class, () -> formData.parse(source).get());
+ assertThat(ee.getCause(), instanceOf(EOFException.class));
+ EOFException bme = (EOFException)ee.getCause();
+ assertThat(bme.getMessage(), containsString("unexpected EOF"));
+ }
+
+ @Test
+ public void testCROnlyEOL()
+ {
+ String boundary = "BEEF";
+ String str = """
+ --$B
+ Content-Disposition: form-data; name="greeting"
+ Content-Type: text/plain; charset=US-ASCII
+
+ Hello World
+ --$B--
+ """.replace("$B", boundary);
+
+ // change every '\n' LF to a CR.
+ str = str.replace(LF, CR);
+
+ assertThat("multipart str cannot contain LF for this test", str, not(containsString(LF)));
+
+ AsyncContent source = new TestContent();
+ CaptureMultiPartViolations violations = new CaptureMultiPartViolations();
+ MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary, MultiPartCompliance.RFC7578, violations);
+ formData.setFilesDirectory(_tmpDir);
+ formData.setMaxMemoryFileSize(-1);
+ Content.Sink.write(source, true, str, Callback.NOOP);
+
+ ExecutionException ee = assertThrows(ExecutionException.class, () -> formData.parse(source).get(5, TimeUnit.SECONDS));
+ assertThat(ee.getCause(), instanceOf(BadMessageException.class));
+ BadMessageException bme = (BadMessageException)ee.getCause();
+ assertThat(bme.getMessage(), containsString("invalid CR-only EOL"));
+ }
+
+ @Test
+ public void testTooManyCRs()
+ {
+ String boundary = "BEEF";
+ String str = """
+ --$B
+ Content-Disposition: form-data; name="greeting"
+ Content-Type: text/plain; charset=US-ASCII
+
+ Hello World
+ --$B--
+ """.replace("$B", boundary);
+
+ // change every '\n' LF to a multiple CR then a LF.
+ str = str.replace("\n", "\r\r\r\r\r\r\r\n");
+
+ AsyncContent source = new TestContent();
+ CaptureMultiPartViolations violations = new CaptureMultiPartViolations();
+ MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary, MultiPartCompliance.RFC7578, violations);
+ formData.setFilesDirectory(_tmpDir);
+ formData.setMaxMemoryFileSize(-1);
+ Content.Sink.write(source, true, str, Callback.NOOP);
+
+ ExecutionException ee = assertThrows(ExecutionException.class, () -> formData.parse(source).get());
+ assertThat(ee.getCause(), instanceOf(BadMessageException.class));
+ BadMessageException bme = (BadMessageException)ee.getCause();
+ assertThat(bme.getMessage(), containsString("invalid CR-only EOL"));
+ }
+
+ @Test
+ public void testContentTransferEncodingBase64() throws Exception
+ {
+ String boundary = "BEEF";
+ String str = """
+ --$B\r
+ Content-Disposition: form-data; name="greeting"\r
+ Content-Type: text/plain; charset=US-ASCII\r
+ Content-Transfer-Encoding: base64\r
+ \r
+ SGVsbG8gV29ybGQK\r
+ --$B--\r
+ """.replace("$B", boundary);
+
+ AsyncContent source = new TestContent();
+ CaptureMultiPartViolations violations = new CaptureMultiPartViolations();
+ MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary, MultiPartCompliance.RFC7578, violations);
+ formData.setFilesDirectory(_tmpDir);
+ formData.setMaxMemoryFileSize(-1);
+ Content.Sink.write(source, true, str, Callback.NOOP);
+
+ try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
+ {
+ assertThat(parts.size(), is(1));
+
+ MultiPart.Part greeting = parts.getFirst("greeting");
+ assertThat(greeting, notNullValue());
+ Content.Source partContent = greeting.getContentSource();
+ assertThat(partContent.getLength(), is(16L));
+ assertThat(Content.Source.asString(partContent), is("SGVsbG8gV29ybGQK"));
+
+ List events = violations.getEvents();
+ assertThat(events.size(), is(1));
+ ComplianceViolation.Event event = events.get(0);
+ assertThat(event.violation(), is(MultiPartCompliance.Violation.BASE64_TRANSFER_ENCODING));
+ }
+ }
+
@Test
public void testNoBody() throws Exception
{
@@ -633,7 +948,7 @@ public void testDefaultCharset() throws Exception
\r
--AaB03x\r
Content-Disposition: form-data; name="utf"\r
- Content-Type: text/plain; charset="UTF-8"
+ Content-Type: text/plain; charset="UTF-8"\r
\r
""";
ByteBuffer utfCedilla = UTF_8.encode("ç");
@@ -1324,4 +1639,20 @@ public void retain()
throw new UnsupportedOperationException();
}
}
+
+ private static class CaptureMultiPartViolations implements ComplianceViolation.Listener
+ {
+ private final List events = new ArrayList<>();
+
+ @Override
+ public void onComplianceViolation(ComplianceViolation.Event event)
+ {
+ events.add(event);
+ }
+
+ public List getEvents()
+ {
+ return events;
+ }
+ }
}
diff --git a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartTest.java b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartTest.java
index bb2f589a9f0f..717c658e43ac 100644
--- a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartTest.java
+++ b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartTest.java
@@ -22,6 +22,7 @@
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.util.BufferUtil;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@@ -370,6 +371,48 @@ public void testSimple() throws Exception
assertEquals(0, data.remaining());
}
+ /**
+ * Whitespace before boundaries.
+ *
+ * @see MultiPartCompliance.Violation#WHITESPACE_BEFORE_BOUNDARY
+ */
+ @Test
+ @Disabled
+ public void testWhitespaceBeforeBoundary() throws Exception
+ {
+ TestPartsListener listener = new TestPartsListener();
+ MultiPart.Parser parser = new MultiPart.Parser("BOUNDARY", listener);
+
+ ByteBuffer data = BufferUtil.toBuffer("""
+ preamble\r
+ --BOUNDARY\r
+ name: value\r
+ \r
+ Hello\r
+ --BOUNDARY\r
+ powerLevel: 9001\r
+ \r
+ secondary\r
+ content\r
+ --BOUNDARY--epi\r
+ logue\r
+ """);
+
+ parser.parse(Content.Chunk.from(data, true));
+
+ assertEquals(2, listener.parts.size());
+
+ MultiPart.Part part1 = listener.parts.get(0);
+ assertEquals("value", part1.getHeaders().get("name"));
+ assertEquals("Hello", Content.Source.asString(part1.getContentSource()));
+
+ MultiPart.Part part2 = listener.parts.get(1);
+ assertEquals("9001", part2.getHeaders().get("powerLevel"));
+ assertEquals("secondary\r\ncontent", Content.Source.asString(part2.getContentSource()));
+
+ assertEquals(0, data.remaining());
+ }
+
@Test
public void testLineFeed() throws Exception
{
@@ -575,7 +618,7 @@ public void testOnlyCRAfterHeaders()
parser.parse(Content.Chunk.from(data, true));
assertNotNull(listener.failure);
- assertThat(listener.failure.getMessage(), containsStringIgnoringCase("Invalid EOL"));
+ assertThat(listener.failure.getMessage(), containsStringIgnoringCase("Invalid CR-only EOL"));
}
private static List badHeaders()
diff --git a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-firefox.expected.txt b/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-firefox.expected.txt
deleted file mode 100644
index 0a05edeeeff7..000000000000
--- a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-android-firefox.expected.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
-Request-Header|Accept-Encoding|gzip, deflate
-Request-Header|Accept-Language|en-US,en;q=0.5
-Request-Header|Connection|keep-alive
-Request-Header|Content-Length|430
-Request-Header|Content-Type|multipart/form-data; boundary=---------------------------117031256520586657911714164254
-Request-Header|Host|192.168.0.119:9090
-Request-Header|Referer|http://192.168.0.119:9090/sjis-form-charset.html
-Request-Header|Upgrade-Insecure-Requests|1
-Request-Header|User-Agent|Mozilla/5.0 (Android 8.1.0; Mobile; rv:59.0) Gecko/59.0 Firefox/59.0
-Parts-Count|3
-Part-ContainsContents|_charset_|Shift_JIS
-Part-ContainsContents|japanese|健治
-Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-chrome.expected.txt b/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-chrome.expected.txt
deleted file mode 100644
index 0d91a3d3545b..000000000000
--- a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-chrome.expected.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
-Request-Header|Accept-Encoding|gzip, deflate, br
-Request-Header|Accept-Language|en-US,en;q=0.9
-Request-Header|Cache-Control|max-age=0
-Request-Header|Connection|keep-alive
-Request-Header|Content-Length|354
-Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundaryDHtjXxgNUcgLjcKs
-Request-Header|Cookie|visited=yes
-Request-Header|DNT|1
-Request-Header|Host|localhost:9090
-Request-Header|Origin|http://localhost:9090
-Request-Header|Referer|http://localhost:9090/sjis-form-charset.html
-Request-Header|Upgrade-Insecure-Requests|1
-Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36
-Parts-Count|3
-Part-ContainsContents|_charset_|Shift_JIS
-Part-ContainsContents|japanese|健治
-Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-edge.expected.txt b/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-edge.expected.txt
deleted file mode 100644
index 4b4cc724c95f..000000000000
--- a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-edge.expected.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-Request-Header|Accept|text/html, application/xhtml+xml, image/jxr, */*
-Request-Header|Accept-Encoding|gzip, deflate
-Request-Header|Accept-Language|en-US
-Request-Header|Cache-Control|no-cache
-Request-Header|Connection|keep-alive
-Request-Header|Content-Length|362
-Request-Header|Content-Type|multipart/form-data; boundary=---------------------------7e227e17151054
-Request-Header|Host|localhost:9090
-Request-Header|Referer|http://localhost:9090/sjis-form-charset.html
-Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299
-Parts-Count|3
-Part-ContainsContents|_charset_|utf-8
-Part-ContainsContents|japanese|健治
-Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-ios-safari.expected.txt b/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-ios-safari.expected.txt
deleted file mode 100644
index 2d6fbab768cd..000000000000
--- a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-ios-safari.expected.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
-Request-Header|Accept-Encoding|gzip, deflate
-Request-Header|Accept-Language|en-us
-Request-Header|Connection|keep-alive
-Request-Header|Content-Length|354
-Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundaryvshQXGBfIsRjfMBN
-Request-Header|Host|192.168.0.119:9090
-Request-Header|Origin|http://192.168.0.119:9090
-Request-Header|Referer|http://192.168.0.119:9090/sjis-form-charset.html
-Request-Header|Upgrade-Insecure-Requests|1
-Request-Header|User-Agent|Mozilla/5.0 (iPad; CPU OS 11_2_6 like Mac OS X) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0 Mobile/15D100 Safari/604.1
-Parts-Count|3
-Part-ContainsContents|_charset_|Shift_JIS
-Part-ContainsContents|japanese|健治
-Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-msie.expected.txt b/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-msie.expected.txt
deleted file mode 100644
index 5d84aa6eb753..000000000000
--- a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-msie.expected.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-Request-Header|Accept|text/html, application/xhtml+xml, image/jxr, */*
-Request-Header|Accept-Encoding|gzip, deflate
-Request-Header|Accept-Language|en-US
-Request-Header|Cache-Control|no-cache
-Request-Header|Connection|keep-alive
-Request-Header|Content-Length|358
-Request-Header|Content-Type|multipart/form-data; boundary=---------------------------7e226e1b2109c
-Request-Header|Host|localhost:9090
-Request-Header|Referer|http://localhost:9090/sjis-form-charset.html
-Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko
-Parts-Count|3
-Part-ContainsContents|_charset_|utf-8
-Part-ContainsContents|japanese|健治
-Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-safari.expected.txt b/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-safari.expected.txt
deleted file mode 100644
index 18452b29e95c..000000000000
--- a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-charset-form-safari.expected.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
-Request-Header|Accept-Encoding|gzip, deflate
-Request-Header|Accept-Language|en-us
-Request-Header|Connection|keep-alive
-Request-Header|Content-Length|354
-Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundaryHFCTTESrC7sXQ2Gf
-Request-Header|Host|192.168.0.119:9090
-Request-Header|Origin|http://192.168.0.119:9090
-Request-Header|Referer|http://192.168.0.119:9090/sjis-form-charset.html
-Request-Header|Upgrade-Insecure-Requests|1
-Request-Header|User-Agent|Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6
-Parts-Count|3
-Part-ContainsContents|_charset_|Shift_JIS
-Part-ContainsContents|japanese|健治
-Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-firefox.expected.txt b/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-firefox.expected.txt
deleted file mode 100644
index b3baf1946894..000000000000
--- a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-android-firefox.expected.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
-Request-Header|Accept-Encoding|gzip, deflate
-Request-Header|Accept-Language|en-US,en;q=0.5
-Request-Header|Connection|keep-alive
-Request-Header|Content-Length|303
-Request-Header|Content-Type|multipart/form-data; boundary=---------------------------18591390852002031541755421242
-Request-Header|Host|192.168.0.119:9090
-Request-Header|Referer|http://192.168.0.119:9090/sjis-form.html
-Request-Header|Upgrade-Insecure-Requests|1
-Request-Header|User-Agent|Mozilla/5.0 (Android 8.1.0; Mobile; rv:59.0) Gecko/59.0 Firefox/59.0
-Parts-Count|2
-Part-ContainsContents|japanese|健治
-Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-chrome.expected.txt b/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-chrome.expected.txt
deleted file mode 100644
index 6cba2d9e365b..000000000000
--- a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-chrome.expected.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
-Request-Header|Accept-Encoding|gzip, deflate, br
-Request-Header|Accept-Language|en-US,en;q=0.9
-Request-Header|Cache-Control|max-age=0
-Request-Header|Connection|keep-alive
-Request-Header|Content-Length|249
-Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundarysKD6As9BBil2g6Fc
-Request-Header|Cookie|visited=yes
-Request-Header|DNT|1
-Request-Header|Host|localhost:9090
-Request-Header|Origin|http://localhost:9090
-Request-Header|Referer|http://localhost:9090/sjis-form.html
-Request-Header|Upgrade-Insecure-Requests|1
-Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36
-Parts-Count|2
-Part-ContainsContents|japanese|健治
-Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-firefox.expected.txt b/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-firefox.expected.txt
deleted file mode 100644
index ad25c45b3216..000000000000
--- a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-firefox.expected.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
-Request-Header|Accept-Encoding|gzip, deflate
-Request-Header|Accept-Language|en-US,en;q=0.5
-Request-Header|Connection|keep-alive
-Request-Header|Content-Length|261
-Request-Header|Content-Type|multipart/form-data; boundary=---------------------------265001916915724
-Request-Header|Host|localhost:9090
-Request-Header|Referer|http://localhost:9090/sjis-form.html
-Request-Header|Upgrade-Insecure-Requests|1
-Request-Header|User-Agent|Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0
-Parts-Count|2
-Part-ContainsContents|japanese|健治
-Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-safari.expected.txt b/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-safari.expected.txt
deleted file mode 100644
index 2acbd52718bf..000000000000
--- a/jetty-core/jetty-http/src/test/resources/multipart/browser-capture-sjis-form-safari.expected.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-Request-Header|Accept|text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
-Request-Header|Accept-Encoding|gzip, deflate
-Request-Header|Accept-Language|en-us
-Request-Header|Connection|keep-alive
-Request-Header|Content-Length|249
-Request-Header|Content-Type|multipart/form-data; boundary=----WebKitFormBoundarytsFILMzOBBWaETUj
-Request-Header|Host|192.168.0.119:9090
-Request-Header|Origin|http://192.168.0.119:9090
-Request-Header|Referer|http://192.168.0.119:9090/sjis-form.html
-Request-Header|Upgrade-Insecure-Requests|1
-Request-Header|User-Agent|Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6
-Parts-Count|2
-Part-ContainsContents|japanese|健治
-Part-ContainsContents|hello|ャユ戆タ
\ No newline at end of file
diff --git a/jetty-core/jetty-http/src/test/resources/multipart/multipart-base64-long.expected.txt b/jetty-core/jetty-http/src/test/resources/multipart/multipart-base64-long.expected.txt
deleted file mode 100644
index 2b1d5ded0601..000000000000
--- a/jetty-core/jetty-http/src/test/resources/multipart/multipart-base64-long.expected.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Content-Type|multipart/form-data; boundary="JuH4rALGPJfmAquncS_U1du8s59GjKKiG9a8"
-Parts-Count|1
-Part-Filename|png|jetty-avatar-256.png
-Part-Sha1sum|png|e75b73644afe9b234d70da9ff225229de68cdff8
\ No newline at end of file
diff --git a/jetty-core/jetty-http/src/test/resources/multipart/multipart-base64.expected.txt b/jetty-core/jetty-http/src/test/resources/multipart/multipart-base64.expected.txt
deleted file mode 100644
index 5d4a189b8dc5..000000000000
--- a/jetty-core/jetty-http/src/test/resources/multipart/multipart-base64.expected.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Content-Type|multipart/form-data; boundary="8GbcZNTauFWYMt7GeM9BxFMdlNBJ6aLJhGdXp"
-Parts-Count|1
-Part-Filename|png|jetty-avatar-256.png
-Part-Sha1sum|png|e75b73644afe9b234d70da9ff225229de68cdff8
\ No newline at end of file
diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/MultiPartParser.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/MultiPartParser.java
deleted file mode 100644
index ec705640473d..000000000000
--- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/MultiPartParser.java
+++ /dev/null
@@ -1,713 +0,0 @@
-//
-// ========================================================================
-// 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.server.internal;
-
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.EnumSet;
-
-import org.eclipse.jetty.http.BadMessageException;
-import org.eclipse.jetty.http.HttpParser.RequestHandler;
-import org.eclipse.jetty.http.HttpTokens;
-import org.eclipse.jetty.util.BufferUtil;
-import org.eclipse.jetty.util.SearchPattern;
-import org.eclipse.jetty.util.Utf8StringBuilder;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A parser for MultiPart content type.
- *
- * @see https://tools.ietf.org/html/rfc2046#section-5.1
- * @see https://tools.ietf.org/html/rfc2045
- *
- * TODO convert to use a {@link org.eclipse.jetty.io.Content.Source} and to be async
- */
-public class MultiPartParser
-{
- public static final Logger LOG = LoggerFactory.getLogger(MultiPartParser.class);
-
- // States
- public enum FieldState
- {
- FIELD,
- IN_NAME,
- AFTER_NAME,
- VALUE,
- IN_VALUE
- }
-
- // States
- public enum State
- {
- PREAMBLE,
- DELIMITER,
- DELIMITER_PADDING,
- DELIMITER_CLOSE,
- BODY_PART,
- FIRST_OCTETS,
- OCTETS,
- EPILOGUE,
- END
- }
-
- private static final EnumSet __delimiterStates = EnumSet.of(State.DELIMITER, State.DELIMITER_CLOSE, State.DELIMITER_PADDING);
- private static final int MAX_HEADER_LINE_LENGTH = 998;
-
- private final boolean debugEnabled = LOG.isDebugEnabled();
- private final Handler _handler;
- private final SearchPattern _delimiterSearch;
-
- private String _fieldName;
- private String _fieldValue;
-
- private State _state = State.PREAMBLE;
- private FieldState _fieldState = FieldState.FIELD;
- private int _partialBoundary = 2; // No CRLF if no preamble
- private boolean _cr;
- private ByteBuffer _patternBuffer;
-
- private final Utf8StringBuilder _string = new Utf8StringBuilder();
- private int _length;
-
- private int _totalHeaderLineLength = -1;
-
- public MultiPartParser(Handler handler, String boundary)
- {
- _handler = handler;
-
- String delimiter = "\r\n--" + boundary;
- _patternBuffer = ByteBuffer.wrap(delimiter.getBytes(StandardCharsets.US_ASCII));
- _delimiterSearch = SearchPattern.compile(_patternBuffer.array());
- }
-
- public void reset()
- {
- _state = State.PREAMBLE;
- _fieldState = FieldState.FIELD;
- _partialBoundary = 2; // No CRLF if no preamble
- }
-
- public Handler getHandler()
- {
- return _handler;
- }
-
- public State getState()
- {
- return _state;
- }
-
- public boolean isState(State state)
- {
- return _state == state;
- }
-
- private static boolean hasNextByte(ByteBuffer buffer)
- {
- return BufferUtil.hasContent(buffer);
- }
-
- private HttpTokens.Token next(ByteBuffer buffer)
- {
- byte ch = buffer.get();
- HttpTokens.Token t = HttpTokens.TOKENS[0xff & ch];
-
- switch (t.getType())
- {
- case CNTL:
- throw new IllegalCharacterException(_state, t, buffer);
-
- case LF:
- _cr = false;
- break;
-
- case CR:
- if (_cr)
- throw new BadMessageException("Bad EOL");
-
- _cr = true;
- return null;
-
- case ALPHA:
- case DIGIT:
- case TCHAR:
- case VCHAR:
- case HTAB:
- case SPACE:
- case OTEXT:
- case COLON:
- if (_cr)
- throw new BadMessageException("Bad EOL");
- break;
-
- default:
- break;
- }
-
- return t;
- }
-
- private void setString(String s)
- {
- _string.reset();
- _string.append(s);
- _length = s.length();
- }
-
- /*
- * Mime Field strings are treated as UTF-8 as per https://tools.ietf.org/html/rfc7578#section-5.1
- */
- private String takeString()
- {
- String s = _string.takeCompleteString(null);
- // trim trailing whitespace.
- if (s.length() > _length)
- s = s.substring(0, _length);
- _length = -1;
- return s;
- }
-
- /**
- * Parse until next Event.
- *
- * @param buffer the buffer to parse
- * @param last whether this buffer contains last bit of content
- * @return True if an {@link RequestHandler} method was called and it returned true;
- */
- public boolean parse(ByteBuffer buffer, boolean last)
- {
- boolean handle = false;
- while (!handle && BufferUtil.hasContent(buffer))
- {
- switch (_state)
- {
- case PREAMBLE:
- parsePreamble(buffer);
- continue;
-
- case DELIMITER:
- case DELIMITER_PADDING:
- case DELIMITER_CLOSE:
- parseDelimiter(buffer);
- continue;
-
- case BODY_PART:
- handle = parseMimePartHeaders(buffer);
- break;
-
- case FIRST_OCTETS:
- case OCTETS:
- handle = parseOctetContent(buffer);
- break;
-
- case EPILOGUE:
- BufferUtil.clear(buffer);
- break;
-
- case END:
- handle = true;
- break;
-
- default:
- throw new IllegalStateException();
- }
- }
-
- if (last && BufferUtil.isEmpty(buffer))
- {
- if (_state == State.EPILOGUE)
- {
- _state = State.END;
-
- if (LOG.isDebugEnabled())
- LOG.debug("messageComplete {}", this);
-
- return _handler.messageComplete();
- }
- else
- {
- if (LOG.isDebugEnabled())
- LOG.debug("earlyEOF {}", this);
-
- _handler.earlyEOF();
- return true;
- }
- }
-
- return handle;
- }
-
- private void parsePreamble(ByteBuffer buffer)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("parsePreamble({})", BufferUtil.toDetailString(buffer));
-
- if (_partialBoundary > 0)
- {
- int partial = _delimiterSearch.startsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining(), _partialBoundary);
- if (partial > 0)
- {
- if (partial == _delimiterSearch.getLength())
- {
- buffer.position(buffer.position() + partial - _partialBoundary);
- _partialBoundary = 0;
- setState(State.DELIMITER);
- return;
- }
-
- _partialBoundary = partial;
- BufferUtil.clear(buffer);
- return;
- }
-
- _partialBoundary = 0;
- }
-
- int delimiter = _delimiterSearch.match(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
- if (delimiter >= 0)
- {
- buffer.position(delimiter - buffer.arrayOffset() + _delimiterSearch.getLength());
- setState(State.DELIMITER);
- return;
- }
-
- _partialBoundary = _delimiterSearch.endsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
- BufferUtil.clear(buffer);
- }
-
- private void parseDelimiter(ByteBuffer buffer)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("parseDelimiter({})", BufferUtil.toDetailString(buffer));
-
- while (__delimiterStates.contains(_state) && hasNextByte(buffer))
- {
- HttpTokens.Token t = next(buffer);
- if (t == null)
- return;
-
- if (t.getType() == HttpTokens.Type.LF)
- {
- setState(State.BODY_PART);
-
- if (LOG.isDebugEnabled())
- LOG.debug("startPart {}", this);
-
- _handler.startPart();
- return;
- }
-
- switch (_state)
- {
- case DELIMITER:
- if (t.getChar() == '-')
- setState(State.DELIMITER_CLOSE);
- else
- setState(State.DELIMITER_PADDING);
- continue;
-
- case DELIMITER_CLOSE:
- if (t.getChar() == '-')
- {
- setState(State.EPILOGUE);
- return;
- }
- setState(State.DELIMITER_PADDING);
- continue;
-
- case DELIMITER_PADDING:
- default:
- }
- }
- }
-
- /*
- * Parse the message headers and return true if the handler has signaled for a return
- */
- protected boolean parseMimePartHeaders(ByteBuffer buffer)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("parseMimePartHeaders({})", BufferUtil.toDetailString(buffer));
-
- // Process headers
- while (_state == State.BODY_PART && hasNextByte(buffer))
- {
- // process each character
- HttpTokens.Token t = next(buffer);
- if (t == null)
- break;
-
- if (t.getType() != HttpTokens.Type.LF)
- _totalHeaderLineLength++;
-
- if (_totalHeaderLineLength > MAX_HEADER_LINE_LENGTH)
- throw new IllegalStateException("Header Line Exceeded Max Length");
-
- switch (_fieldState)
- {
- case FIELD:
- switch (t.getType())
- {
- case SPACE:
- case HTAB:
- {
- // Folded field value!
-
- if (_fieldName == null)
- throw new IllegalStateException("First field folded");
-
- if (_fieldValue == null)
- {
- _string.reset();
- _length = 0;
- }
- else
- {
- setString(_fieldValue);
- _string.append(' ');
- _length++;
- _fieldValue = null;
- }
- setState(FieldState.VALUE);
- break;
- }
-
- case LF:
- handleField();
- setState(State.FIRST_OCTETS);
- _partialBoundary = 2; // CRLF is option for empty parts
-
- if (LOG.isDebugEnabled())
- LOG.debug("headerComplete {}", this);
-
- if (_handler.headerComplete())
- return true;
- break;
-
- case ALPHA:
- case DIGIT:
- case TCHAR:
- // process previous header
- handleField();
-
- // New header
- setState(FieldState.IN_NAME);
- _string.reset();
- _string.append(t.getChar());
- _length = 1;
-
- break;
-
- default:
- throw new IllegalCharacterException(_state, t, buffer);
- }
- break;
-
- case IN_NAME:
- switch (t.getType())
- {
- case COLON:
- _fieldName = takeString();
- _length = -1;
- setState(FieldState.VALUE);
- break;
-
- case SPACE:
- // Ignore trailing whitespaces
- setState(FieldState.AFTER_NAME);
- break;
-
- case LF:
- {
- if (LOG.isDebugEnabled())
- LOG.debug("Line Feed in Name {}", this);
-
- handleField();
- setState(FieldState.FIELD);
- break;
- }
-
- case ALPHA:
- case DIGIT:
- case TCHAR:
- _string.append(t.getChar());
- _length = _string.length();
- break;
-
- default:
- throw new IllegalCharacterException(_state, t, buffer);
- }
- break;
-
- case AFTER_NAME:
- switch (t.getType())
- {
- case COLON:
- _fieldName = takeString();
- _length = -1;
- setState(FieldState.VALUE);
- break;
-
- case LF:
- _fieldName = takeString();
- _string.reset();
- _fieldValue = "";
- _length = -1;
- break;
-
- case SPACE:
- break;
-
- default:
- throw new IllegalCharacterException(_state, t, buffer);
- }
- break;
-
- case VALUE:
- switch (t.getType())
- {
- case LF:
- _string.reset();
- _fieldValue = "";
- _length = -1;
-
- setState(FieldState.FIELD);
- break;
-
- case SPACE:
- case HTAB:
- break;
-
- case ALPHA:
- case DIGIT:
- case TCHAR:
- case VCHAR:
- case COLON:
- case OTEXT:
- _string.append(t.getByte());
- _length = _string.length();
- setState(FieldState.IN_VALUE);
- break;
-
- default:
- throw new IllegalCharacterException(_state, t, buffer);
- }
- break;
-
- case IN_VALUE:
- switch (t.getType())
- {
- case SPACE:
- case HTAB:
- _string.append(' ');
- break;
-
- case LF:
- if (_length > 0)
- {
- _fieldValue = takeString();
- _length = -1;
- _totalHeaderLineLength = -1;
- }
- setState(FieldState.FIELD);
- break;
-
- case ALPHA:
- case DIGIT:
- case TCHAR:
- case VCHAR:
- case COLON:
- case OTEXT:
- _string.append(t.getByte());
- _length = _string.length();
- break;
-
- default:
- throw new IllegalCharacterException(_state, t, buffer);
- }
- break;
-
- default:
- throw new IllegalStateException(_state.toString());
- }
- }
- return false;
- }
-
- private void handleField()
- {
- if (LOG.isDebugEnabled())
- LOG.debug("parsedField: _fieldName={} _fieldValue={} {}", _fieldName, _fieldValue, this);
-
- if (_fieldName != null && _fieldValue != null)
- _handler.parsedField(_fieldName, _fieldValue);
- _fieldName = _fieldValue = null;
- }
-
- protected boolean parseOctetContent(ByteBuffer buffer)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("parseOctetContent({})", BufferUtil.toDetailString(buffer));
-
- // Starts With
- if (_partialBoundary > 0)
- {
- int partial = _delimiterSearch.startsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining(), _partialBoundary);
- if (partial > 0)
- {
- if (partial == _delimiterSearch.getLength())
- {
- buffer.position(buffer.position() + _delimiterSearch.getLength() - _partialBoundary);
- setState(State.DELIMITER);
- _partialBoundary = 0;
-
- if (LOG.isDebugEnabled())
- LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(BufferUtil.EMPTY_BUFFER), true, this);
-
- return _handler.content(BufferUtil.EMPTY_BUFFER, true);
- }
-
- _partialBoundary = partial;
- BufferUtil.clear(buffer);
- return false;
- }
- else
- {
- // output up to _partialBoundary of the search pattern
- ByteBuffer content = _patternBuffer.slice();
- if (_state == State.FIRST_OCTETS)
- {
- setState(State.OCTETS);
- content.position(2);
- }
- content.limit(_partialBoundary);
- _partialBoundary = 0;
-
- if (LOG.isDebugEnabled())
- LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(content), false, this);
-
- if (_handler.content(content, false))
- return true;
- }
- }
-
- // Contains
- int delimiter = _delimiterSearch.match(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
- if (delimiter >= 0)
- {
- ByteBuffer content = buffer.slice();
- content.limit(delimiter - buffer.arrayOffset() - buffer.position());
-
- buffer.position(delimiter - buffer.arrayOffset() + _delimiterSearch.getLength());
- setState(State.DELIMITER);
-
- if (LOG.isDebugEnabled())
- LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(content), true, this);
-
- return _handler.content(content, true);
- }
-
- // Ends With
- _partialBoundary = _delimiterSearch.endsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
- if (_partialBoundary > 0)
- {
- ByteBuffer content = buffer.slice();
- content.limit(content.limit() - _partialBoundary);
-
- if (LOG.isDebugEnabled())
- LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(content), false, this);
-
- BufferUtil.clear(buffer);
- return _handler.content(content, false);
- }
-
- // There is normal content with no delimiter
- ByteBuffer content = buffer.slice();
-
- if (LOG.isDebugEnabled())
- LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(content), false, this);
-
- BufferUtil.clear(buffer);
- return _handler.content(content, false);
- }
-
- private void setState(State state)
- {
- if (debugEnabled)
- LOG.debug("{} --> {}", _state, state);
- _state = state;
- }
-
- private void setState(FieldState state)
- {
- if (debugEnabled)
- LOG.debug("{}:{} --> {}", _state, _fieldState, state);
- _fieldState = state;
- }
-
- @Override
- public String toString()
- {
- return String.format("%s{s=%s}", getClass().getSimpleName(), _state);
- }
-
- /*
- * Event Handler interface These methods return true if the caller should process the events so far received (eg return from parseNext and call
- * HttpChannel.handle). If multiple callbacks are called in sequence (eg headerComplete then messageComplete) from the same point in the parsing then it is
- * sufficient for the caller to process the events only once.
- */
- public interface Handler
- {
- default void startPart()
- {
- }
-
- @SuppressWarnings("unused")
- default void parsedField(String name, String value)
- {
- }
-
- default boolean headerComplete()
- {
- return false;
- }
-
- @SuppressWarnings("unused")
- default boolean content(ByteBuffer item, boolean last)
- {
- return false;
- }
-
- default boolean messageComplete()
- {
- return false;
- }
-
- default void earlyEOF()
- {
- }
- }
-
- @SuppressWarnings("serial")
- private static class IllegalCharacterException extends BadMessageException
- {
- private IllegalCharacterException(State state, HttpTokens.Token token, ByteBuffer buffer)
- {
- super(400, String.format("Illegal character %s", token));
- if (LOG.isDebugEnabled())
- LOG.debug(String.format("Illegal character %s in state=%s for buffer %s", token, state, BufferUtil.toDetailString(buffer)));
- }
- }
-}
diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java
index 6f78d3300cc5..27b5d9758b49 100644
--- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java
+++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java
@@ -717,7 +717,7 @@ public static String toString(ByteBuffer buffer)
}
/**
- * Convert the buffer to an ISO-8859-1 String
+ * Convert buffer to a String with specified Charset
*
* @param buffer The buffer to convert in flush mode. The buffer is unchanged
* @param charset The {@link Charset} to use to convert the bytes
diff --git a/jetty-ee10/jetty-ee10-servlet/pom.xml b/jetty-ee10/jetty-ee10-servlet/pom.xml
index 3691cb35d2f0..33e3554defbc 100644
--- a/jetty-ee10/jetty-ee10-servlet/pom.xml
+++ b/jetty-ee10/jetty-ee10-servlet/pom.xml
@@ -62,12 +62,17 @@
jetty-http-tools
test