Merge remote-tracking branch 'origin/jetty-9.4.x' into jetty-10.0.x
This commit is contained in:
commit
8763c6065d
|
@ -5,4 +5,5 @@
|
|||
*.java eol=lf
|
||||
*.xml eol=lf
|
||||
Jenkinsfile eol=lf
|
||||
*.js eol=lf
|
||||
*.js eol=lf
|
||||
*.raw binary
|
|
@ -1,6 +1,6 @@
|
|||
#!groovy
|
||||
|
||||
def jdks = ["jdk8", "jdk9"]
|
||||
def jdks = ["jdk8","jdk9","jdk10","jdk11"]
|
||||
def oss = ["linux"] //windows? ,"linux-docker"
|
||||
def builds = [:]
|
||||
for (def os in oss) {
|
||||
|
@ -15,8 +15,10 @@ def getFullBuild(jdk, os) {
|
|||
return {
|
||||
node(os) {
|
||||
// System Dependent Locations
|
||||
def mvntool = tool name: 'maven3', type: 'hudson.tasks.Maven$MavenInstallation'
|
||||
def mvntool = tool name: 'maven3.5', type: 'hudson.tasks.Maven$MavenInstallation'
|
||||
def jdktool = tool name: "$jdk", type: 'hudson.model.JDK'
|
||||
def mvnName = 'maven3.5'
|
||||
def localRepo = ".repository" // "${env.JENKINS_HOME}/${env.EXECUTOR_NUMBER}"
|
||||
|
||||
// Environment
|
||||
List mvnEnv = ["PATH+MVN=${mvntool}/bin", "PATH+JDK=${jdktool}/bin", "JAVA_HOME=${jdktool}/", "MAVEN_HOME=${mvntool}"]
|
||||
|
@ -38,11 +40,11 @@ def getFullBuild(jdk, os) {
|
|||
withEnv(mvnEnv) {
|
||||
timeout(time: 15, unit: 'MINUTES') {
|
||||
withMaven(
|
||||
maven: 'maven3',
|
||||
maven: mvnName,
|
||||
jdk: "$jdk",
|
||||
publisherStrategy: 'EXPLICIT',
|
||||
globalMavenSettingsConfig: 'oss-settings.xml',
|
||||
mavenLocalRepo: "${env.JENKINS_HOME}/${env.EXECUTOR_NUMBER}") {
|
||||
mavenLocalRepo: localRepo) {
|
||||
sh "mvn -V -B clean install -DskipTests -T6"
|
||||
}
|
||||
|
||||
|
@ -60,11 +62,11 @@ def getFullBuild(jdk, os) {
|
|||
withEnv(mvnEnv) {
|
||||
timeout(time: 20, unit: 'MINUTES') {
|
||||
withMaven(
|
||||
maven: 'maven3',
|
||||
maven: mvnName,
|
||||
jdk: "$jdk",
|
||||
publisherStrategy: 'EXPLICIT',
|
||||
globalMavenSettingsConfig: 'oss-settings.xml',
|
||||
mavenLocalRepo: "${env.JENKINS_HOME}/${env.EXECUTOR_NUMBER}") {
|
||||
mavenLocalRepo: localRepo) {
|
||||
sh "mvn -V -B javadoc:javadoc -T5"
|
||||
}
|
||||
}
|
||||
|
@ -82,12 +84,12 @@ def getFullBuild(jdk, os) {
|
|||
timeout(time: 90, unit: 'MINUTES') {
|
||||
// Run test phase / ignore test failures
|
||||
withMaven(
|
||||
maven: 'maven3',
|
||||
maven: mvnName,
|
||||
jdk: "$jdk",
|
||||
publisherStrategy: 'EXPLICIT',
|
||||
//options: [invokerPublisher(disabled: false)],
|
||||
globalMavenSettingsConfig: 'oss-settings.xml',
|
||||
mavenLocalRepo: "${env.JENKINS_HOME}/${env.EXECUTOR_NUMBER}") {
|
||||
mavenLocalRepo: localRepo) {
|
||||
//
|
||||
sh "mvn -V -B install -Dmaven.test.failure.ignore=true -Prun-its -T3 -e -Dmaven.repo.local=${env.JENKINS_HOME}/${env.EXECUTOR_NUMBER} -Pmongodb"
|
||||
}
|
||||
|
@ -134,17 +136,14 @@ def getFullBuild(jdk, os) {
|
|||
try
|
||||
{
|
||||
stage ("Compact3 - ${jdk}") {
|
||||
|
||||
dir("aggregates/jetty-all-compact3") {
|
||||
withEnv(mvnEnv) {
|
||||
withMaven(
|
||||
maven: 'maven3',
|
||||
jdk: "$jdk",
|
||||
publisherStrategy: 'EXPLICIT',
|
||||
globalMavenSettingsConfig: 'oss-settings.xml',
|
||||
mavenLocalRepo: "${env.JENKINS_HOME}/${env.EXECUTOR_NUMBER}") {
|
||||
sh "mvn -V -B -Pcompact3 clean install -T5"
|
||||
}
|
||||
withEnv(mvnEnv) {
|
||||
withMaven(
|
||||
maven: mvnName,
|
||||
jdk: "$jdk",
|
||||
publisherStrategy: 'EXPLICIT',
|
||||
globalMavenSettingsConfig: 'oss-settings.xml',
|
||||
mavenLocalRepo: localRepo) {
|
||||
sh "mvn -f aggregates/jetty-all-compact3 -V -B -Pcompact3 clean install -T5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<Export-Package>org.eclipse.jetty.apache.jsp.*;version="${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}",
|
||||
org.eclipse.jetty.jsp.*;version="${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}"
|
||||
</Export-Package>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional</Require-Capability>
|
||||
<Provide-Capability>osgi.serviceloader;osgi.serviceloader=javax.servlet.ServletContainerInitializer,osgi.serviceloader;osgi.serviceloader=org.apache.juli.logging.Log</Provide-Capability>
|
||||
<_nouses>true</_nouses>
|
||||
</instructions>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<configuration>
|
||||
<instructions>
|
||||
<Import-Package>org.eclipse.jetty.alpn;resolution:=optional,*</Import-Package>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)", osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.io.ssl.ALPNProcessor$Client)";cardinality:=multiple</Require-Capability>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional, osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.io.ssl.ALPNProcessor$Client)";resolution:=optional;cardinality:=multiple</Require-Capability>
|
||||
</instructions>
|
||||
</configuration>
|
||||
</execution>
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
<Bundle-Description>Conscrypt Client ALPN</Bundle-Description>
|
||||
<Import-Package>org.conscrypt;version="${conscrypt.version}",*</Import-Package>
|
||||
<Export-Package>*</Export-Package>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional</Require-Capability>
|
||||
<Provide-Capability>osgi.serviceloader; osgi.serviceloader=org.eclipse.jetty.io.ssl.ALPNProcessor$Client</Provide-Capability>
|
||||
<_nouses>true</_nouses>
|
||||
</instructions>
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
<instructions>
|
||||
<Bundle-Description>Conscrypt ALPN</Bundle-Description>
|
||||
<Import-Package>org.conscrypt;version="${conscrypt.version}",*</Import-Package>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional</Require-Capability>
|
||||
<Provide-Capability>osgi.serviceloader;osgi.serviceloader=org.eclipse.jetty.io.ssl.ALPNProcessor$Server</Provide-Capability>
|
||||
<_nouses>true</_nouses>
|
||||
</instructions>
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
<instructions>
|
||||
<Bundle-Description>JDK9 Client ALPN</Bundle-Description>
|
||||
<Export-Package>*</Export-Package>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional</Require-Capability>
|
||||
<Provide-Capability>osgi.serviceloader; osgi.serviceloader=org.eclipse.jetty.io.ssl.ALPNProcessor$Client</Provide-Capability>
|
||||
<_nouses>true</_nouses>
|
||||
</instructions>
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<configuration>
|
||||
<instructions>
|
||||
<Bundle-Description>JDK9 Server ALPN</Bundle-Description>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional</Require-Capability>
|
||||
<Provide-Capability>osgi.serviceloader;osgi.serviceloader=org.eclipse.jetty.io.ssl.ALPNProcessor$Server</Provide-Capability>
|
||||
<_nouses>true</_nouses>
|
||||
</instructions>
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
<Bundle-Description>OpenJDK8 Client ALPN</Bundle-Description>
|
||||
<Import-Package>org.eclipse.jetty.alpn;version="${alpn.majorVersion}.${alpn.minorVersion}.${alpn.incrementalVersion}",*</Import-Package>
|
||||
<Export-Package>*</Export-Package>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional</Require-Capability>
|
||||
<Provide-Capability>osgi.serviceloader; osgi.serviceloader=org.eclipse.jetty.io.ssl.ALPNProcessor$Client</Provide-Capability>
|
||||
<_nouses>true</_nouses>
|
||||
</instructions>
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
<Bundle-Description>OpenJDK8 Server ALPN</Bundle-Description>
|
||||
<Export-Package>*</Export-Package>
|
||||
<Import-Package>org.eclipse.jetty.alpn;version="${alpn.majorVersion}.${alpn.minorVersion}.${alpn.incrementalVersion}",*</Import-Package>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional</Require-Capability>
|
||||
<Provide-Capability>osgi.serviceloader; osgi.serviceloader=org.eclipse.jetty.io.ssl.ALPNProcessor$Server</Provide-Capability>
|
||||
<_nouses>true</_nouses>
|
||||
</instructions>
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
<Bundle-SymbolicName>${bundle-symbolic-name};singleton:=true</Bundle-SymbolicName>
|
||||
<Export-Package>org.eclipse.jetty.alpn.server,*</Export-Package>
|
||||
<Import-Package>org.eclipse.jetty.alpn;version="${alpn.majorVersion}.${alpn.minorVersion}.${alpn.incrementalVersion}",*</Import-Package>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)", osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.io.ssl.ALPNProcessor$Server)";resolution:=optional;cardinality:=multiple</Require-Capability>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional, osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.io.ssl.ALPNProcessor$Server)";resolution:=optional;cardinality:=multiple</Require-Capability>
|
||||
</instructions>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
|
||||
|
||||
[files]
|
||||
http://central.maven.org/maven2/org/mortbay/jetty/alpn/alpn-boot/8.1.12.v20180117/alpn-boot-8.1.12.v20180117.jar|lib/alpn/alpn-boot-8.1.12.v20180117.jar
|
||||
|
||||
[exec]
|
||||
-Xbootclasspath/p:lib/alpn/alpn-boot-8.1.12.v20180117.jar
|
|
@ -0,0 +1,7 @@
|
|||
DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
|
||||
|
||||
[files]
|
||||
http://central.maven.org/maven2/org/mortbay/jetty/alpn/alpn-boot/8.1.12.v20180117/alpn-boot-8.1.12.v20180117.jar|lib/alpn/alpn-boot-8.1.12.v20180117.jar
|
||||
|
||||
[exec]
|
||||
-Xbootclasspath/p:lib/alpn/alpn-boot-8.1.12.v20180117.jar
|
|
@ -21,7 +21,7 @@
|
|||
<configuration>
|
||||
<instructions>
|
||||
<Import-Package>org.objectweb.asm;version="[5.0,7)",*</Import-Package>
|
||||
<Require-Capability>osgi.serviceloader; filter:="(osgi.serviceloader=javax.servlet.ServletContainerInitializer)";resolution:=optional;cardinality:=multiple, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)"</Require-Capability>
|
||||
<Require-Capability>osgi.serviceloader; filter:="(osgi.serviceloader=javax.servlet.ServletContainerInitializer)";resolution:=optional;cardinality:=multiple, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional</Require-Capability>
|
||||
</instructions>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
|
|
@ -85,14 +85,14 @@ public class AnnotationParser
|
|||
public static int asmVersion ()
|
||||
{
|
||||
int asmVersion = ASM_OPCODE_VERSION;
|
||||
Package asm = Package.getPackage("org.objectweb.asm");
|
||||
Package asm = Opcodes.class.getPackage();
|
||||
if (asm == null)
|
||||
LOG.warn("Unknown asm runtime version, assuming version {}", asmVersion);
|
||||
else
|
||||
{
|
||||
String s = asm.getImplementationVersion();
|
||||
if (s==null)
|
||||
LOG.warn("Unknown asm runtime version, assuming version {}", asmVersion);
|
||||
LOG.warn("Unknown asm implementation version, assuming version {}", asmVersion);
|
||||
else
|
||||
{
|
||||
int dot = s.indexOf('.');
|
||||
|
|
|
@ -29,7 +29,6 @@ import java.io.FileOutputStream;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
@ -48,6 +47,7 @@ import org.eclipse.jetty.toolchain.test.FS;
|
|||
import org.eclipse.jetty.toolchain.test.IO;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.toolchain.test.TestingDir;
|
||||
import org.eclipse.jetty.util.resource.PathResource;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
|
@ -221,6 +221,17 @@ public class TestAnnotationParser
|
|||
// Should throw no exceptions, and skip the META-INF/versions/9/* files
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJep238MultiReleaseInJar_JDK10() throws Exception
|
||||
{
|
||||
File jdk10Jar = MavenTestingUtils.getTestResourceFile("jdk10/multirelease-10.jar");
|
||||
AnnotationParser parser = new AnnotationParser();
|
||||
DuplicateClassScanHandler handler = new DuplicateClassScanHandler();
|
||||
Set<Handler> handlers = Collections.singleton(handler);
|
||||
parser.parse(handlers, new PathResource(jdk10Jar));
|
||||
// Should throw no exceptions
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBasedirExclusion() throws Exception
|
||||
{
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,3 @@
|
|||
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
|
||||
#org.eclipse.jetty.LEVEL=DEBUG
|
||||
#org.eclipse.jetty.annotations.LEVEL=DEBUG
|
|
@ -47,7 +47,7 @@ public class JettyRunTask extends Task
|
|||
private File tempDirectory;
|
||||
|
||||
/** List of web applications to be deployed. */
|
||||
private List<AntWebAppContext> webapps = new ArrayList<AntWebAppContext>();
|
||||
private List<AntWebAppContext> webapps = new ArrayList<>();
|
||||
|
||||
/** Location of jetty.xml file. */
|
||||
private File jettyXml;
|
||||
|
@ -147,20 +147,17 @@ public class JettyRunTask extends Task
|
|||
{
|
||||
try
|
||||
{
|
||||
this.requestLog = (RequestLog) Class.forName(className).newInstance();
|
||||
}
|
||||
catch (InstantiationException e)
|
||||
{
|
||||
throw new BuildException("Request logger instantiation exception: " + e);
|
||||
}
|
||||
catch (IllegalAccessException e)
|
||||
{
|
||||
throw new BuildException("Request logger instantiation exception: " + e);
|
||||
this.requestLog = (RequestLog) Class.forName(className).getDeclaredConstructor().newInstance();
|
||||
}
|
||||
catch (ClassNotFoundException e)
|
||||
{
|
||||
throw new BuildException("Unknown request logger class: " + className);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new BuildException("Request logger instantiation exception: " + e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public String getRequestLog()
|
||||
|
|
|
@ -42,7 +42,7 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler
|
|||
{
|
||||
public static final int DEFAULT_MAX_CONTENT_LENGTH = 16*1024;
|
||||
public static final Logger LOG = Log.getLogger(AuthenticationProtocolHandler.class);
|
||||
private static final Pattern AUTHENTICATE_PATTERN = Pattern.compile("([^\\s]+)\\s+realm=\"([^\"]*)\"(.*)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern AUTHENTICATE_PATTERN = Pattern.compile("([^\\s]+)\\s+(.*,\\s*)?realm=\"([^\"]*)\"\\s*,?\\s*(.*)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private final HttpClient client;
|
||||
private final int maxContentLength;
|
||||
|
@ -241,8 +241,16 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler
|
|||
if (matcher.matches())
|
||||
{
|
||||
String type = matcher.group(1);
|
||||
String realm = matcher.group(2);
|
||||
String params = matcher.group(3);
|
||||
String realm = matcher.group(3);
|
||||
String beforeRealm = matcher.group(2);
|
||||
String afterRealm = matcher.group(4);
|
||||
|
||||
String params;
|
||||
if (beforeRealm != null)
|
||||
params = beforeRealm + afterRealm;
|
||||
else
|
||||
params = afterRealm;
|
||||
|
||||
Authentication.HeaderInfo headerInfo = new Authentication.HeaderInfo(type, realm, params, getAuthorizationHeader());
|
||||
result.add(headerInfo);
|
||||
}
|
||||
|
|
|
@ -344,6 +344,8 @@ The ALPN implementation, relying on modifications of OpenJDK classes, updates ev
|
|||
|1.8.0u152 |8.1.11.v20170118
|
||||
|1.8.0u161 |8.1.12.v20180117
|
||||
|1.8.0u162 |8.1.12.v20180117
|
||||
|1.8.0u171 |8.1.12.v20180117
|
||||
|1.8.0u172 |8.1.12.v20180117
|
||||
|=============================
|
||||
|
||||
[[alpn-build]]
|
||||
|
|
|
@ -100,7 +100,7 @@ To test a Jetty release, complete the following steps for each release you want
|
|||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<jetty-version>7.6.2.v20120308</jetty-version>
|
||||
<jetty-plugin-version>${jetty-version}</jetty-plugin-version>
|
||||
<slf4j-version>1.6.4</slf4j-version>
|
||||
<slf4j.version>1.6.4</slf4j.version>
|
||||
<spring-version>3.1.0.RELEASE</spring-version>
|
||||
</properties>
|
||||
<repositories>
|
||||
|
|
|
@ -303,6 +303,19 @@
|
|||
</artifactItems>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>copy-servlet-src-deps</id>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<includeGroupIds>javax.servlet</includeGroupIds>
|
||||
<includeTypes>jar</includeTypes>
|
||||
<classifier>sources</classifier>
|
||||
<outputDirectory>${source-assembly-directory}/lib/servlet-api</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>copy-annotations-deps</id>
|
||||
<phase>generate-resources</phase>
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
<configuration>
|
||||
<instructions>
|
||||
<Bundle-Description>Jetty Http SPI</Bundle-Description>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional</Require-Capability>
|
||||
<Provide-Capability>osgi.serviceloader; osgi.serviceloader=com.sun.net.httpserver.spi.HttpServerProvider</Provide-Capability>
|
||||
<_nouses>true</_nouses>
|
||||
</instructions>
|
||||
|
|
|
@ -23,11 +23,28 @@
|
|||
<artifactId>jetty-io</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.toolchain</groupId>
|
||||
<artifactId>jetty-test-helper</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-core</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-generator-annprocess</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
@ -38,9 +55,6 @@
|
|||
<configuration>
|
||||
<instructions>
|
||||
<Require-Capability>osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.http.HttpFieldPreEncoder)";resolution:=optional;cardinality:=multiple, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional</Require-Capability>
|
||||
<!--
|
||||
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
|
||||
-->
|
||||
<Provide-Capability>osgi.serviceloader; osgi.serviceloader=org.eclipse.jetty.http.HttpFieldPreEncoder</Provide-Capability>
|
||||
</instructions>
|
||||
</configuration>
|
||||
|
@ -64,7 +78,49 @@
|
|||
<onlyAnalyze>org.eclipse.jetty.http.*</onlyAnalyze>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<finalName>${jmhjar.name}</finalName>
|
||||
<shadeTestJar>true</shadeTestJar>
|
||||
<artifactSet>
|
||||
<includes>
|
||||
<include>org.openjdk.jmh:jmh-core</include>
|
||||
</includes>
|
||||
</artifactSet>
|
||||
<transformers>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.openjdk.jmh.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>org.openjdk.jmh:jmh-core</artifact>
|
||||
<includes>
|
||||
<include>**</include>
|
||||
</includes>
|
||||
</filter>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
|
@ -49,7 +49,7 @@ import org.eclipse.jetty.util.log.Logger;
|
|||
* be altered in code and will affect all usages of the mode.
|
||||
*/
|
||||
public enum HttpCompliance // TODO in Jetty-10 convert this enum to a class so that extra custom modes can be defined dynamically
|
||||
{
|
||||
{
|
||||
/** A Legacy compliance mode to match jetty's behavior prior to RFC2616 and RFC7230. It only
|
||||
* contains {@link HttpComplianceSection#METHOD_CASE_SENSITIVE}
|
||||
*/
|
||||
|
@ -82,6 +82,8 @@ public enum HttpCompliance // TODO in Jetty-10 convert this enum to a class so t
|
|||
@Deprecated
|
||||
CUSTOM3(sectionsByProperty("CUSTOM3"));
|
||||
|
||||
public static final String VIOLATIONS_ATTR = "org.eclipse.jetty.http.compliance.violations";
|
||||
|
||||
private static final Logger LOG = Log.getLogger(HttpParser.class);
|
||||
private static EnumSet<HttpComplianceSection> sectionsByProperty(String property)
|
||||
{
|
||||
|
|
|
@ -58,6 +58,7 @@ public class MimeTypes
|
|||
FORM_ENCODED("application/x-www-form-urlencoded"),
|
||||
MESSAGE_HTTP("message/http"),
|
||||
MULTIPART_BYTERANGES("multipart/byteranges"),
|
||||
MULTIPART_FORM_DATA("multipart/form-data"),
|
||||
|
||||
TEXT_HTML("text/html"),
|
||||
TEXT_PLAIN("text/plain"),
|
||||
|
|
|
@ -0,0 +1,856 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.MultipartConfigElement;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.Part;
|
||||
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.ByteArrayOutputStream2;
|
||||
import org.eclipse.jetty.util.LazyList;
|
||||
import org.eclipse.jetty.util.MultiException;
|
||||
import org.eclipse.jetty.util.MultiMap;
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* MultiPartInputStream
|
||||
*
|
||||
* Handle a MultiPart Mime input stream, breaking it up on the boundary into files and strings.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7578">https://tools.ietf.org/html/rfc7578</a>
|
||||
*/
|
||||
public class MultiPartFormInputStream
|
||||
{
|
||||
private static final Logger LOG = Log.getLogger(MultiPartFormInputStream.class);
|
||||
private final int _bufferSize = 16 * 1024;
|
||||
public static final MultipartConfigElement __DEFAULT_MULTIPART_CONFIG = new MultipartConfigElement(System.getProperty("java.io.tmpdir"));
|
||||
public static final MultiMap<Part> EMPTY_MAP = new MultiMap<>(Collections.emptyMap());
|
||||
protected InputStream _in;
|
||||
protected MultipartConfigElement _config;
|
||||
protected String _contentType;
|
||||
protected MultiMap<Part> _parts;
|
||||
protected Throwable _err;
|
||||
protected File _tmpDir;
|
||||
protected File _contextTmpDir;
|
||||
protected boolean _deleteOnExit;
|
||||
protected boolean _writeFilesWithFilenames;
|
||||
protected boolean _parsed;
|
||||
|
||||
public class MultiPart implements Part
|
||||
{
|
||||
protected String _name;
|
||||
protected String _filename;
|
||||
protected File _file;
|
||||
protected OutputStream _out;
|
||||
protected ByteArrayOutputStream2 _bout;
|
||||
protected String _contentType;
|
||||
protected MultiMap<String> _headers;
|
||||
protected long _size = 0;
|
||||
protected boolean _temporary = true;
|
||||
|
||||
public MultiPart(String name, String filename) throws IOException
|
||||
{
|
||||
_name = name;
|
||||
_filename = filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("Part{n=%s,fn=%s,ct=%s,s=%d,tmp=%b,file=%s}",_name,_filename,_contentType,_size,_temporary,_file);
|
||||
}
|
||||
|
||||
protected void setContentType(String contentType)
|
||||
{
|
||||
_contentType = contentType;
|
||||
}
|
||||
|
||||
protected void open() throws IOException
|
||||
{
|
||||
// We will either be writing to a file, if it has a filename on the content-disposition
|
||||
// and otherwise a byte-array-input-stream, OR if we exceed the getFileSizeThreshold, we
|
||||
// will need to change to write to a file.
|
||||
if (isWriteFilesWithFilenames() && _filename != null && _filename.trim().length() > 0)
|
||||
{
|
||||
createFile();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write to a buffer in memory until we discover we've exceed the
|
||||
// MultipartConfig fileSizeThreshold
|
||||
_out = _bout = new ByteArrayOutputStream2();
|
||||
}
|
||||
}
|
||||
|
||||
protected void close() throws IOException
|
||||
{
|
||||
_out.close();
|
||||
}
|
||||
|
||||
protected void write(int b) throws IOException
|
||||
{
|
||||
if (MultiPartFormInputStream.this._config.getMaxFileSize() > 0 && _size + 1 > MultiPartFormInputStream.this._config.getMaxFileSize())
|
||||
throw new IllegalStateException("Multipart Mime part " + _name + " exceeds max filesize");
|
||||
|
||||
if (MultiPartFormInputStream.this._config.getFileSizeThreshold() > 0 && _size + 1 > MultiPartFormInputStream.this._config.getFileSizeThreshold()
|
||||
&& _file == null)
|
||||
createFile();
|
||||
|
||||
_out.write(b);
|
||||
_size++;
|
||||
}
|
||||
|
||||
protected void write(byte[] bytes, int offset, int length) throws IOException
|
||||
{
|
||||
if (MultiPartFormInputStream.this._config.getMaxFileSize() > 0 && _size + length > MultiPartFormInputStream.this._config.getMaxFileSize())
|
||||
throw new IllegalStateException("Multipart Mime part " + _name + " exceeds max filesize");
|
||||
|
||||
if (MultiPartFormInputStream.this._config.getFileSizeThreshold() > 0
|
||||
&& _size + length > MultiPartFormInputStream.this._config.getFileSizeThreshold() && _file == null)
|
||||
createFile();
|
||||
|
||||
_out.write(bytes,offset,length);
|
||||
_size += length;
|
||||
}
|
||||
|
||||
protected void createFile() throws IOException
|
||||
{
|
||||
/*
|
||||
* Some statics just to make the code below easier to understand This get optimized away during the compile anyway
|
||||
*/
|
||||
final boolean USER = true;
|
||||
final boolean WORLD = false;
|
||||
|
||||
_file = File.createTempFile("MultiPart","",MultiPartFormInputStream.this._tmpDir);
|
||||
_file.setReadable(false,WORLD); // (reset) disable it for everyone first
|
||||
_file.setReadable(true,USER); // enable for user only
|
||||
|
||||
if (_deleteOnExit)
|
||||
_file.deleteOnExit();
|
||||
FileOutputStream fos = new FileOutputStream(_file);
|
||||
BufferedOutputStream bos = new BufferedOutputStream(fos);
|
||||
|
||||
if (_size > 0 && _out != null)
|
||||
{
|
||||
// already written some bytes, so need to copy them into the file
|
||||
_out.flush();
|
||||
_bout.writeTo(bos);
|
||||
_out.close();
|
||||
}
|
||||
_bout = null;
|
||||
_out = bos;
|
||||
}
|
||||
|
||||
protected void setHeaders(MultiMap<String> headers)
|
||||
{
|
||||
_headers = headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getContentType()
|
||||
*/
|
||||
@Override
|
||||
public String getContentType()
|
||||
{
|
||||
return _contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getHeader(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public String getHeader(String name)
|
||||
{
|
||||
if (name == null)
|
||||
return null;
|
||||
return _headers.getValue(StringUtil.asciiToLowerCase(name),0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getHeaderNames()
|
||||
*/
|
||||
@Override
|
||||
public Collection<String> getHeaderNames()
|
||||
{
|
||||
return _headers.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getHeaders(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public Collection<String> getHeaders(String name)
|
||||
{
|
||||
return _headers.getValues(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getInputStream()
|
||||
*/
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException
|
||||
{
|
||||
if (_file != null)
|
||||
{
|
||||
// written to a file, whether temporary or not
|
||||
return new BufferedInputStream(new FileInputStream(_file));
|
||||
}
|
||||
else
|
||||
{
|
||||
// part content is in memory
|
||||
return new ByteArrayInputStream(_bout.getBuf(),0,_bout.size());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getSubmittedFileName()
|
||||
*/
|
||||
@Override
|
||||
public String getSubmittedFileName()
|
||||
{
|
||||
return getContentDispositionFilename();
|
||||
}
|
||||
|
||||
public byte[] getBytes()
|
||||
{
|
||||
if (_bout != null)
|
||||
return _bout.toByteArray();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getName()
|
||||
*/
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return _name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getSize()
|
||||
*/
|
||||
@Override
|
||||
public long getSize()
|
||||
{
|
||||
return _size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#write(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public void write(String fileName) throws IOException
|
||||
{
|
||||
if (_file == null)
|
||||
{
|
||||
_temporary = false;
|
||||
|
||||
// part data is only in the ByteArrayOutputStream and never been written to disk
|
||||
_file = new File(_tmpDir,fileName);
|
||||
|
||||
BufferedOutputStream bos = null;
|
||||
try
|
||||
{
|
||||
bos = new BufferedOutputStream(new FileOutputStream(_file));
|
||||
_bout.writeTo(bos);
|
||||
bos.flush();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (bos != null)
|
||||
bos.close();
|
||||
_bout = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// the part data is already written to a temporary file, just rename it
|
||||
_temporary = false;
|
||||
|
||||
Path src = _file.toPath();
|
||||
Path target = src.resolveSibling(fileName);
|
||||
Files.move(src,target,StandardCopyOption.REPLACE_EXISTING);
|
||||
_file = target.toFile();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the file, whether or not Part.write() was called on it (ie no longer temporary)
|
||||
*
|
||||
* @see javax.servlet.http.Part#delete()
|
||||
*/
|
||||
@Override
|
||||
public void delete() throws IOException
|
||||
{
|
||||
if (_file != null && _file.exists())
|
||||
_file.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Only remove tmp files.
|
||||
*
|
||||
* @throws IOException
|
||||
* if unable to delete the file
|
||||
*/
|
||||
public void cleanUp() throws IOException
|
||||
{
|
||||
if (_temporary && _file != null && _file.exists())
|
||||
_file.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file
|
||||
*
|
||||
* @return the file, if any, the data has been written to.
|
||||
*/
|
||||
public File getFile()
|
||||
{
|
||||
return _file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filename from the content-disposition.
|
||||
*
|
||||
* @return null or the filename
|
||||
*/
|
||||
public String getContentDispositionFilename()
|
||||
{
|
||||
return _filename;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param in
|
||||
* Request input stream
|
||||
* @param contentType
|
||||
* Content-Type header
|
||||
* @param config
|
||||
* MultipartConfigElement
|
||||
* @param contextTmpDir
|
||||
* javax.servlet.context.tempdir
|
||||
*/
|
||||
public MultiPartFormInputStream(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
|
||||
{
|
||||
_contentType = contentType;
|
||||
_config = config;
|
||||
_contextTmpDir = contextTmpDir;
|
||||
if (_contextTmpDir == null)
|
||||
_contextTmpDir = new File(System.getProperty("java.io.tmpdir"));
|
||||
|
||||
if (_config == null)
|
||||
_config = new MultipartConfigElement(_contextTmpDir.getAbsolutePath());
|
||||
|
||||
if (in instanceof ServletInputStream)
|
||||
{
|
||||
if (((ServletInputStream)in).isFinished())
|
||||
{
|
||||
_parts = EMPTY_MAP;
|
||||
_parsed = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
_in = new BufferedInputStream(in);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the list of parsed parts is empty
|
||||
*/
|
||||
public boolean isEmpty()
|
||||
{
|
||||
if (_parts == null)
|
||||
return true;
|
||||
|
||||
Collection<List<Part>> values = _parts.values();
|
||||
for (List<Part> partList : values)
|
||||
{
|
||||
if(partList.size() != 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the already parsed parts.
|
||||
*
|
||||
* @return the parts that were parsed
|
||||
*/
|
||||
@Deprecated
|
||||
public Collection<Part> getParsedParts()
|
||||
{
|
||||
if (_parts == null)
|
||||
return Collections.emptyList();
|
||||
|
||||
Collection<List<Part>> values = _parts.values();
|
||||
List<Part> parts = new ArrayList<>();
|
||||
for (List<Part> o : values)
|
||||
{
|
||||
List<Part> asList = LazyList.getList(o,false);
|
||||
parts.addAll(asList);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete any tmp storage for parts, and clear out the parts list.
|
||||
*/
|
||||
public void deleteParts()
|
||||
{
|
||||
if (!_parsed)
|
||||
return;
|
||||
|
||||
Collection<Part> parts;
|
||||
try
|
||||
{
|
||||
parts = getParts();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
MultiException err = new MultiException();
|
||||
|
||||
for (Part p : parts)
|
||||
{
|
||||
try
|
||||
{
|
||||
((MultiPart)p).cleanUp();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
err.add(e);
|
||||
}
|
||||
}
|
||||
_parts.clear();
|
||||
|
||||
err.ifExceptionThrowRuntime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse, if necessary, the multipart data and return the list of Parts.
|
||||
*
|
||||
* @return the parts
|
||||
* @throws IOException
|
||||
* if unable to get the parts
|
||||
*/
|
||||
public Collection<Part> getParts() throws IOException
|
||||
{
|
||||
if (!_parsed)
|
||||
parse();
|
||||
throwIfError();
|
||||
|
||||
Collection<List<Part>> values = _parts.values();
|
||||
List<Part> parts = new ArrayList<>();
|
||||
for (List<Part> o : values)
|
||||
{
|
||||
List<Part> asList = LazyList.getList(o,false);
|
||||
parts.addAll(asList);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the named Part.
|
||||
*
|
||||
* @param name
|
||||
* the part name
|
||||
* @return the parts
|
||||
* @throws IOException
|
||||
* if unable to get the part
|
||||
*/
|
||||
public Part getPart(String name) throws IOException
|
||||
{
|
||||
if(!_parsed)
|
||||
parse();
|
||||
throwIfError();
|
||||
return _parts.getValue(name,0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception if one has been latched.
|
||||
*
|
||||
* @throws IOException
|
||||
* the exception (if present)
|
||||
*/
|
||||
protected void throwIfError() throws IOException
|
||||
{
|
||||
if (_err != null)
|
||||
{
|
||||
_err.addSuppressed(new Throwable());
|
||||
if (_err instanceof IOException)
|
||||
throw (IOException)_err;
|
||||
if (_err instanceof IllegalStateException)
|
||||
throw (IllegalStateException)_err;
|
||||
throw new IllegalStateException(_err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse, if necessary, the multipart stream.
|
||||
*
|
||||
*/
|
||||
protected void parse()
|
||||
{
|
||||
// have we already parsed the input?
|
||||
if (_parsed)
|
||||
return;
|
||||
_parsed = true;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
// initialize
|
||||
_parts = new MultiMap<>();
|
||||
|
||||
// if its not a multipart request, don't parse it
|
||||
if (_contentType == null || !_contentType.startsWith("multipart/form-data"))
|
||||
return;
|
||||
|
||||
// sort out the location to which to write the files
|
||||
if (_config.getLocation() == null)
|
||||
_tmpDir = _contextTmpDir;
|
||||
else if ("".equals(_config.getLocation()))
|
||||
_tmpDir = _contextTmpDir;
|
||||
else
|
||||
{
|
||||
File f = new File(_config.getLocation());
|
||||
if (f.isAbsolute())
|
||||
_tmpDir = f;
|
||||
else
|
||||
_tmpDir = new File(_contextTmpDir,_config.getLocation());
|
||||
}
|
||||
|
||||
if (!_tmpDir.exists())
|
||||
_tmpDir.mkdirs();
|
||||
|
||||
String contentTypeBoundary = "";
|
||||
int bstart = _contentType.indexOf("boundary=");
|
||||
if (bstart >= 0)
|
||||
{
|
||||
int bend = _contentType.indexOf(";",bstart);
|
||||
bend = (bend < 0?_contentType.length():bend);
|
||||
contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart,bend)).trim());
|
||||
}
|
||||
|
||||
Handler handler = new Handler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,contentTypeBoundary);
|
||||
|
||||
// Create a buffer to store data from stream //
|
||||
byte[] data = new byte[_bufferSize];
|
||||
int len = 0;
|
||||
|
||||
/*
|
||||
* keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfigElement._maxRequestSize
|
||||
*/
|
||||
long total = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
|
||||
len = _in.read(data);
|
||||
|
||||
if (len > 0)
|
||||
{
|
||||
total += len;
|
||||
if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
|
||||
{
|
||||
_err = new IllegalStateException("Request exceeds maxRequestSize (" + _config.getMaxRequestSize() + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer buffer = BufferUtil.toBuffer(data);
|
||||
buffer.limit(len);
|
||||
if (parser.parse(buffer,false))
|
||||
break;
|
||||
|
||||
if(buffer.hasRemaining())
|
||||
throw new IllegalStateException("Buffer did not fully consume");
|
||||
|
||||
}
|
||||
else if (len == -1)
|
||||
{
|
||||
parser.parse(BufferUtil.EMPTY_BUFFER,true);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// check for exceptions
|
||||
if (_err != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// check we read to the end of the message
|
||||
if (parser.getState() != MultiPartParser.State.END)
|
||||
{
|
||||
if (parser.getState() == MultiPartParser.State.PREAMBLE)
|
||||
_err = new IOException("Missing initial multi part boundary");
|
||||
else
|
||||
_err = new IOException("Incomplete Multipart");
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
{
|
||||
LOG.debug("Parsing Complete {} err={}",parser,_err);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
_err = e;
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Handler implements MultiPartParser.Handler
|
||||
{
|
||||
private MultiPart _part = null;
|
||||
private String contentDisposition = null;
|
||||
private String contentType = null;
|
||||
private MultiMap<String> headers = new MultiMap<>();
|
||||
|
||||
@Override
|
||||
public boolean messageComplete()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parsedField(String key, String value)
|
||||
{
|
||||
// Add to headers and mark if one of these fields. //
|
||||
headers.put(StringUtil.asciiToLowerCase(key),value);
|
||||
if (key.equalsIgnoreCase("content-disposition"))
|
||||
contentDisposition = value;
|
||||
else if (key.equalsIgnoreCase("content-type"))
|
||||
contentType = value;
|
||||
|
||||
// Transfer encoding is not longer considers as it is deprecated as per
|
||||
// https://tools.ietf.org/html/rfc7578#section-4.7
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean headerComplete()
|
||||
{
|
||||
if(LOG.isDebugEnabled())
|
||||
{
|
||||
LOG.debug("headerComplete {}",this);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Extract content-disposition
|
||||
boolean form_data = false;
|
||||
if (contentDisposition == null)
|
||||
{
|
||||
throw new IOException("Missing content-disposition");
|
||||
}
|
||||
|
||||
QuotedStringTokenizer tok = new QuotedStringTokenizer(contentDisposition,";",false,true);
|
||||
String name = null;
|
||||
String filename = null;
|
||||
while (tok.hasMoreTokens())
|
||||
{
|
||||
String t = tok.nextToken().trim();
|
||||
String tl = StringUtil.asciiToLowerCase(t);
|
||||
if (tl.startsWith("form-data"))
|
||||
form_data = true;
|
||||
else if (tl.startsWith("name="))
|
||||
name = value(t);
|
||||
else if (tl.startsWith("filename="))
|
||||
filename = filenameValue(t);
|
||||
}
|
||||
|
||||
// Check disposition
|
||||
if (!form_data)
|
||||
throw new IOException("Part not form-data");
|
||||
|
||||
// It is valid for reset and submit buttons to have an empty name.
|
||||
// If no name is supplied, the browser skips sending the info for that field.
|
||||
// However, if you supply the empty string as the name, the browser sends the
|
||||
// field, with name as the empty string. So, only continue this loop if we
|
||||
// have not yet seen a name field.
|
||||
if (name == null)
|
||||
throw new IOException("No name in part");
|
||||
|
||||
|
||||
// create the new part
|
||||
_part = new MultiPart(name,filename);
|
||||
_part.setHeaders(headers);
|
||||
_part.setContentType(contentType);
|
||||
_parts.add(name,_part);
|
||||
|
||||
try
|
||||
{
|
||||
_part.open();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
_err = e;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_err = e;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean content(ByteBuffer buffer, boolean last)
|
||||
{
|
||||
if(_part == null)
|
||||
return false;
|
||||
|
||||
if (BufferUtil.hasContent(buffer))
|
||||
{
|
||||
try
|
||||
{
|
||||
_part.write(buffer.array(),buffer.arrayOffset() + buffer.position(),buffer.remaining());
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
_err = e;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (last)
|
||||
{
|
||||
try
|
||||
{
|
||||
_part.close();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
_err = e;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startPart()
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void earlyEOF()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Early EOF {}",MultiPartFormInputStream.this);
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
_part = null;
|
||||
contentDisposition = null;
|
||||
contentType = null;
|
||||
headers = new MultiMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
public void setDeleteOnExit(boolean deleteOnExit)
|
||||
{
|
||||
_deleteOnExit = deleteOnExit;
|
||||
}
|
||||
|
||||
public void setWriteFilesWithFilenames(boolean writeFilesWithFilenames)
|
||||
{
|
||||
_writeFilesWithFilenames = writeFilesWithFilenames;
|
||||
}
|
||||
|
||||
public boolean isWriteFilesWithFilenames()
|
||||
{
|
||||
return _writeFilesWithFilenames;
|
||||
}
|
||||
|
||||
public boolean isDeleteOnExit()
|
||||
{
|
||||
return _deleteOnExit;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
private String value(String nameEqualsValue)
|
||||
{
|
||||
int idx = nameEqualsValue.indexOf('=');
|
||||
String value = nameEqualsValue.substring(idx + 1).trim();
|
||||
return QuotedStringTokenizer.unquoteOnly(value);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
private String filenameValue(String nameEqualsValue)
|
||||
{
|
||||
int idx = nameEqualsValue.indexOf('=');
|
||||
String value = nameEqualsValue.substring(idx + 1).trim();
|
||||
|
||||
if (value.matches(".??[a-z,A-Z]\\:\\\\[^\\\\].*"))
|
||||
{
|
||||
// incorrectly escaped IE filenames that have the whole path
|
||||
// we just strip any leading & trailing quotes and leave it as is
|
||||
char first = value.charAt(0);
|
||||
if (first == '"' || first == '\'')
|
||||
value = value.substring(1);
|
||||
char last = value.charAt(value.length() - 1);
|
||||
if (last == '"' || last == '\'')
|
||||
value = value.substring(0,value.length() - 1);
|
||||
|
||||
return value;
|
||||
}
|
||||
else
|
||||
// unquote the string, but allow any backslashes that don't
|
||||
// form a valid escape sequence to remain as many browsers
|
||||
// even on *nix systems will not escape a filename containing
|
||||
// backslashes
|
||||
return QuotedStringTokenizer.unquoteOnly(value,true);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,766 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import org.eclipse.jetty.http.HttpParser.RequestHandler;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.SearchPattern;
|
||||
import org.eclipse.jetty.util.Utf8StringBuilder;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** A parser for MultiPart content type.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc2046#section-5.1">https://tools.ietf.org/html/rfc2046#section-5.1</a>
|
||||
* @see <a href="https://tools.ietf.org/html/rfc2045">https://tools.ietf.org/html/rfc2045</a>
|
||||
*/
|
||||
public class MultiPartParser
|
||||
{
|
||||
public static final Logger LOG = Log.getLogger(MultiPartParser.class);
|
||||
|
||||
static final byte COLON = (byte)':';
|
||||
static final byte TAB = 0x09;
|
||||
static final byte LINE_FEED = 0x0A;
|
||||
static final byte CARRIAGE_RETURN = 0x0D;
|
||||
static final byte SPACE = 0x20;
|
||||
static final byte[] CRLF =
|
||||
{ CARRIAGE_RETURN, LINE_FEED };
|
||||
static final byte SEMI_COLON = (byte)';';
|
||||
|
||||
// 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 final static EnumSet<State> __delimiterStates = EnumSet.of(State.DELIMITER,State.DELIMITER_CLOSE,State.DELIMITER_PADDING);
|
||||
|
||||
private final boolean DEBUG = 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;
|
||||
private int _maxHeaderLineLength = 998;
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
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;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
enum CharState
|
||||
{
|
||||
ILLEGAL, CR, LF, LEGAL
|
||||
}
|
||||
private final static CharState[] __charState;
|
||||
static
|
||||
{
|
||||
// token = 1*tchar
|
||||
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
||||
// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
||||
// / DIGIT / ALPHA
|
||||
// ; any VCHAR, except delimiters
|
||||
// quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
|
||||
// qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
|
||||
// obs-text = %x80-FF
|
||||
// comment = "(" *( ctext / quoted-pair / comment ) ")"
|
||||
// ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
|
||||
// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
||||
|
||||
__charState = new CharState[256];
|
||||
Arrays.fill(__charState,CharState.ILLEGAL);
|
||||
__charState[LINE_FEED] = CharState.LF;
|
||||
__charState[CARRIAGE_RETURN] = CharState.CR;
|
||||
__charState[TAB] = CharState.LEGAL;
|
||||
__charState[SPACE] = CharState.LEGAL;
|
||||
|
||||
__charState['!'] = CharState.LEGAL;
|
||||
__charState['#'] = CharState.LEGAL;
|
||||
__charState['$'] = CharState.LEGAL;
|
||||
__charState['%'] = CharState.LEGAL;
|
||||
__charState['&'] = CharState.LEGAL;
|
||||
__charState['\''] = CharState.LEGAL;
|
||||
__charState['*'] = CharState.LEGAL;
|
||||
__charState['+'] = CharState.LEGAL;
|
||||
__charState['-'] = CharState.LEGAL;
|
||||
__charState['.'] = CharState.LEGAL;
|
||||
__charState['^'] = CharState.LEGAL;
|
||||
__charState['_'] = CharState.LEGAL;
|
||||
__charState['`'] = CharState.LEGAL;
|
||||
__charState['|'] = CharState.LEGAL;
|
||||
__charState['~'] = CharState.LEGAL;
|
||||
|
||||
__charState['"'] = CharState.LEGAL;
|
||||
|
||||
__charState['\\'] = CharState.LEGAL;
|
||||
__charState['('] = CharState.LEGAL;
|
||||
__charState[')'] = CharState.LEGAL;
|
||||
Arrays.fill(__charState,0x21,0x27 + 1,CharState.LEGAL);
|
||||
Arrays.fill(__charState,0x2A,0x5B + 1,CharState.LEGAL);
|
||||
Arrays.fill(__charState,0x5D,0x7E + 1,CharState.LEGAL);
|
||||
Arrays.fill(__charState,0x80,0xFF + 1,CharState.LEGAL);
|
||||
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
private boolean hasNextByte(ByteBuffer buffer)
|
||||
{
|
||||
return BufferUtil.hasContent(buffer);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
private byte getNextByte(ByteBuffer buffer)
|
||||
{
|
||||
|
||||
byte ch = buffer.get();
|
||||
|
||||
CharState s = __charState[0xff & ch];
|
||||
switch (s)
|
||||
{
|
||||
case LF:
|
||||
_cr = false;
|
||||
return ch;
|
||||
|
||||
case CR:
|
||||
if (_cr)
|
||||
throw new BadMessageException("Bad EOL");
|
||||
|
||||
_cr = true;
|
||||
if (buffer.hasRemaining())
|
||||
return getNextByte(buffer);
|
||||
|
||||
// Can return 0 here to indicate the need for more characters,
|
||||
// because a real 0 in the buffer would cause a BadMessage below
|
||||
return 0;
|
||||
|
||||
case LEGAL:
|
||||
if (_cr)
|
||||
throw new BadMessageException("Bad EOL");
|
||||
|
||||
return ch;
|
||||
|
||||
case ILLEGAL:
|
||||
default:
|
||||
throw new IllegalCharacterException(_state,ch,buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
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.toString();
|
||||
// trim trailing whitespace.
|
||||
if (s.length()>_length)
|
||||
s = s.substring(0,_length);
|
||||
_string.reset();
|
||||
_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 == false && 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 (_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);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
private void parseDelimiter(ByteBuffer buffer)
|
||||
{
|
||||
while (__delimiterStates.contains(_state) && hasNextByte(buffer))
|
||||
{
|
||||
byte b = getNextByte(buffer);
|
||||
if (b == 0)
|
||||
return;
|
||||
|
||||
if (b == '\n')
|
||||
{
|
||||
setState(State.BODY_PART);
|
||||
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("startPart {}",this);
|
||||
|
||||
_handler.startPart();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (_state)
|
||||
{
|
||||
case DELIMITER:
|
||||
if (b == '-')
|
||||
setState(State.DELIMITER_CLOSE);
|
||||
else
|
||||
setState(State.DELIMITER_PADDING);
|
||||
continue;
|
||||
|
||||
case DELIMITER_CLOSE:
|
||||
if (b == '-')
|
||||
{
|
||||
setState(State.EPILOGUE);
|
||||
return;
|
||||
}
|
||||
setState(State.DELIMITER_PADDING);
|
||||
continue;
|
||||
|
||||
case DELIMITER_PADDING:
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
/*
|
||||
* Parse the message headers and return true if the handler has signaled for a return
|
||||
*/
|
||||
protected boolean parseMimePartHeaders(ByteBuffer buffer)
|
||||
{
|
||||
// Process headers
|
||||
while (_state == State.BODY_PART && hasNextByte(buffer))
|
||||
{
|
||||
// process each character
|
||||
byte b = getNextByte(buffer);
|
||||
if (b == 0)
|
||||
break;
|
||||
|
||||
if (b != LINE_FEED)
|
||||
_totalHeaderLineLength++;
|
||||
|
||||
if (_totalHeaderLineLength > _maxHeaderLineLength)
|
||||
throw new IllegalStateException("Header Line Exceeded Max Length");
|
||||
|
||||
switch (_fieldState)
|
||||
{
|
||||
case FIELD:
|
||||
switch (b)
|
||||
{
|
||||
case SPACE:
|
||||
case TAB:
|
||||
{
|
||||
// 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 LINE_FEED:
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
// process previous header
|
||||
handleField();
|
||||
|
||||
// New header
|
||||
setState(FieldState.IN_NAME);
|
||||
_string.reset();
|
||||
_string.append(b);
|
||||
_length = 1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case IN_NAME:
|
||||
switch (b)
|
||||
{
|
||||
case COLON:
|
||||
_fieldName = takeString();
|
||||
_length = -1;
|
||||
setState(FieldState.VALUE);
|
||||
break;
|
||||
|
||||
case SPACE:
|
||||
// Ignore trailing whitespaces
|
||||
setState(FieldState.AFTER_NAME);
|
||||
break;
|
||||
|
||||
case LINE_FEED:
|
||||
{
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("Line Feed in Name {}", this);
|
||||
|
||||
handleField();
|
||||
setState(FieldState.FIELD);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
_string.append(b);
|
||||
_length = _string.length();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case AFTER_NAME:
|
||||
switch (b)
|
||||
{
|
||||
case COLON:
|
||||
_fieldName = takeString();
|
||||
_length = -1;
|
||||
setState(FieldState.VALUE);
|
||||
break;
|
||||
|
||||
case LINE_FEED:
|
||||
_fieldName = takeString();
|
||||
_string.reset();
|
||||
_fieldValue = "";
|
||||
_length = -1;
|
||||
break;
|
||||
|
||||
case SPACE:
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalCharacterException(_state,b,buffer);
|
||||
}
|
||||
break;
|
||||
|
||||
case VALUE:
|
||||
switch (b)
|
||||
{
|
||||
case LINE_FEED:
|
||||
_string.reset();
|
||||
_fieldValue = "";
|
||||
_length = -1;
|
||||
|
||||
setState(FieldState.FIELD);
|
||||
break;
|
||||
|
||||
case SPACE:
|
||||
case TAB:
|
||||
break;
|
||||
|
||||
default:
|
||||
_string.append(b);
|
||||
_length = _string.length();
|
||||
setState(FieldState.IN_VALUE);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case IN_VALUE:
|
||||
switch (b)
|
||||
{
|
||||
case SPACE:
|
||||
_string.append(b);
|
||||
break;
|
||||
|
||||
case LINE_FEED:
|
||||
if (_length > 0)
|
||||
{
|
||||
_fieldValue = takeString();
|
||||
_length = -1;
|
||||
_totalHeaderLineLength = -1;
|
||||
}
|
||||
setState(FieldState.FIELD);
|
||||
break;
|
||||
|
||||
default:
|
||||
_string.append(b);
|
||||
if (b > SPACE || b < 0)
|
||||
_length = _string.length();
|
||||
break;
|
||||
}
|
||||
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)
|
||||
{
|
||||
|
||||
// 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 (DEBUG)
|
||||
LOG.debug("{} --> {}",_state,state);
|
||||
_state = state;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
private void setState(FieldState state)
|
||||
{
|
||||
if (DEBUG)
|
||||
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
|
||||
{
|
||||
public default void startPart()
|
||||
{
|
||||
}
|
||||
|
||||
public default void parsedField(String name, String value)
|
||||
{
|
||||
}
|
||||
|
||||
public default boolean headerComplete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public default boolean content(ByteBuffer item, boolean last)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public default boolean messageComplete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public default void earlyEOF()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
@SuppressWarnings("serial")
|
||||
private static class IllegalCharacterException extends IllegalArgumentException
|
||||
{
|
||||
private IllegalCharacterException(State state, byte ch, ByteBuffer buffer)
|
||||
{
|
||||
super(String.format("Illegal character 0x%X",ch));
|
||||
// Bug #460642 - don't reveal buffers to end user
|
||||
LOG.warn(String.format("Illegal character 0x%X in state=%s for buffer %s",ch,state,BufferUtil.toDetailString(buffer)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,404 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
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.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.servlet.MultipartConfigElement;
|
||||
import javax.servlet.http.Part;
|
||||
|
||||
import org.eclipse.jetty.toolchain.test.Hex;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.toolchain.test.TestingDir;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class MultiPartCaptureTest
|
||||
{
|
||||
|
||||
public static final int MAX_FILE_SIZE = 2 * 1024 * 1024;
|
||||
public static final int MAX_REQUEST_SIZE = MAX_FILE_SIZE + (60 * 1024);
|
||||
public static final int FILE_SIZE_THRESHOLD = 50;
|
||||
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
public static List<Object[]> data()
|
||||
{
|
||||
List<Object[]> ret = new ArrayList<>();
|
||||
|
||||
// == Arbitrary / Non-Standard Examples ==
|
||||
|
||||
ret.add(new String[]{"multipart-uppercase"});
|
||||
// ret.add(new String[]{"multipart-base64"}); // base64 transfer encoding deprecated
|
||||
// ret.add(new String[]{"multipart-base64-long"}); // base64 transfer encoding deprecated
|
||||
|
||||
// == Capture of raw request body contents from Apache HttpClient 4.5.5 ==
|
||||
|
||||
ret.add(new String[]{"browser-capture-company-urlencoded-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-complex-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-duplicate-names-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-encoding-mess-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-nested-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-nested-binary-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-number-only2-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-number-only-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-sjis-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-strange-quoting-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-text-files-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-unicode-names-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-zalgo-text-plain-apache-httpcomp"});
|
||||
|
||||
// == Capture of raw request body contents from Eclipse Jetty Http Client 9.4.9 ==
|
||||
|
||||
ret.add(new String[]{"browser-capture-complex-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-duplicate-names-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-encoding-mess-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-nested-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-number-only-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-sjis-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-text-files-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-unicode-names-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-whitespace-only-jetty-client"});
|
||||
|
||||
// == Capture of raw request body contents from various browsers ==
|
||||
|
||||
// simple form - 2 fields
|
||||
ret.add(new String[]{"browser-capture-form1-android-chrome"});
|
||||
ret.add(new String[]{"browser-capture-form1-android-firefox"});
|
||||
ret.add(new String[]{"browser-capture-form1-chrome"});
|
||||
ret.add(new String[]{"browser-capture-form1-edge"});
|
||||
ret.add(new String[]{"browser-capture-form1-firefox"});
|
||||
ret.add(new String[]{"browser-capture-form1-ios-safari"});
|
||||
ret.add(new String[]{"browser-capture-form1-msie"});
|
||||
ret.add(new String[]{"browser-capture-form1-osx-safari"});
|
||||
|
||||
// form submitted as shift-jis
|
||||
ret.add(new String[]{"browser-capture-sjis-form-edge"});
|
||||
ret.add(new String[]{"browser-capture-sjis-form-msie"});
|
||||
// TODO: these might be addressable via Issue #2398
|
||||
// ret.add(new String[]{"browser-capture-sjis-form-android-chrome"}); // contains html encoded character and unspecified charset defaults to utf-8
|
||||
// ret.add(new String[]{"browser-capture-sjis-form-android-firefox"}); // contains html encoded character and unspecified charset defaults to utf-8
|
||||
// ret.add(new String[]{"browser-capture-sjis-form-chrome"}); // contains html encoded character and unspecified charset defaults to utf-8
|
||||
// ret.add(new String[]{"browser-capture-sjis-form-firefox"}); // contains html encoded character and unspecified charset defaults to utf-8
|
||||
// ret.add(new String[]{"browser-capture-sjis-form-ios-safari"}); // contains html encoded character and unspecified charset defaults to utf-8
|
||||
// ret.add(new String[]{"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)
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-android-chrome"}); // contains html encoded character
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-android-firefox"}); // contains html encoded character
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-chrome"}); // contains html encoded character
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-edge"});
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-firefox"}); // contains html encoded character
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-ios-safari"}); // contains html encoded character
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-msie"});
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-safari"}); // contains html encoded character
|
||||
|
||||
// form submitted with simple file upload
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-android-chrome"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-android-firefox"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-chrome"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-edge"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-firefox"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-ios-safari"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-msie"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-safari"});
|
||||
|
||||
// form submitted with 2 files (1 binary, 1 text) and 2 text fields
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-alt-chrome"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-alt-edge"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-alt-firefox"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-alt-msie"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-alt-safari"});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Rule
|
||||
public TestingDir testingDir = new TestingDir();
|
||||
|
||||
private final Path multipartRawFile;
|
||||
private final MultipartExpectations multipartExpectations;
|
||||
|
||||
public MultiPartCaptureTest(String rawPrefix) throws IOException
|
||||
{
|
||||
multipartRawFile = MavenTestingUtils.getTestResourcePathFile("multipart/" + rawPrefix + ".raw");
|
||||
Path expectationPath = MavenTestingUtils.getTestResourcePathFile("multipart/" + rawPrefix + ".expected.txt");
|
||||
multipartExpectations = new MultipartExpectations(expectationPath);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUtilParse() throws Exception
|
||||
{
|
||||
Path outputDir = testingDir.getEmptyPathDir();
|
||||
MultipartConfigElement config = newMultipartConfigElement(outputDir);
|
||||
try (InputStream in = Files.newInputStream(multipartRawFile))
|
||||
{
|
||||
org.eclipse.jetty.util.MultiPartInputStreamParser parser = new org.eclipse.jetty.util.MultiPartInputStreamParser(in,multipartExpectations.contentType,config,outputDir.toFile());
|
||||
|
||||
checkParts(parser.getParts(),s->
|
||||
{
|
||||
try
|
||||
{
|
||||
return parser.getPart(s);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpParse() throws Exception
|
||||
{
|
||||
Path outputDir = testingDir.getEmptyPathDir();
|
||||
MultipartConfigElement config = newMultipartConfigElement(outputDir);
|
||||
try (InputStream in = Files.newInputStream(multipartRawFile))
|
||||
{
|
||||
MultiPartFormInputStream parser = new MultiPartFormInputStream(in, multipartExpectations.contentType, config, outputDir.toFile());
|
||||
|
||||
checkParts(parser.getParts(),s->
|
||||
{
|
||||
try
|
||||
{
|
||||
return parser.getPart(s);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void checkParts(Collection<Part> parts, Function<String, Part> getPart) throws Exception
|
||||
{
|
||||
// Evaluate Count
|
||||
if (multipartExpectations.partCount >= 0)
|
||||
{
|
||||
assertThat("Mulitpart.parts.size", parts.size(), is(multipartExpectations.partCount));
|
||||
}
|
||||
|
||||
String defaultCharset = UTF_8.toString();
|
||||
Part charSetPart = getPart.apply("_charset_");
|
||||
if(charSetPart != null)
|
||||
{
|
||||
defaultCharset = IO.toString(charSetPart.getInputStream());
|
||||
}
|
||||
|
||||
|
||||
// Evaluate expected Contents
|
||||
for (NameValue expected : multipartExpectations.partContainsContents)
|
||||
{
|
||||
Part part = getPart.apply(expected.name);
|
||||
assertThat("Part[" + expected.name + "]", part, is(notNullValue()));
|
||||
try (InputStream partInputStream = part.getInputStream())
|
||||
{
|
||||
String charset = getCharsetFromContentType(part.getContentType(), defaultCharset);
|
||||
String contents = IO.toString(partInputStream, charset);
|
||||
assertThat("Part[" + expected.name + "].contents", contents, containsString(expected.value));
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate expected filenames
|
||||
for (NameValue expected : multipartExpectations.partFilenames)
|
||||
{
|
||||
Part part = getPart.apply(expected.name);
|
||||
assertThat("Part[" + expected.name + "]", part, is(notNullValue()));
|
||||
assertThat("Part[" + expected.name + "]", part.getSubmittedFileName(), is(expected.value));
|
||||
}
|
||||
|
||||
// Evaluate expected contents checksums
|
||||
for (NameValue expected : multipartExpectations.partSha1sums)
|
||||
{
|
||||
Part part = getPart.apply(expected.name);
|
||||
assertThat("Part[" + expected.name + "]", part, is(notNullValue()));
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA1");
|
||||
try (InputStream partInputStream = part.getInputStream();
|
||||
NoOpOutputStream noop = new NoOpOutputStream();
|
||||
DigestOutputStream digester = new DigestOutputStream(noop, digest))
|
||||
{
|
||||
IO.copy(partInputStream, digester);
|
||||
String actualSha1sum = Hex.asHex(digest.digest()).toLowerCase(Locale.US);
|
||||
assertThat("Part[" + expected.name + "].sha1sum", actualSha1sum, Matchers.equalToIgnoringCase(expected.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MultipartConfigElement newMultipartConfigElement(Path path)
|
||||
{
|
||||
return new MultipartConfigElement(path.toString(), MAX_FILE_SIZE, MAX_REQUEST_SIZE, FILE_SIZE_THRESHOLD);
|
||||
}
|
||||
|
||||
private String getCharsetFromContentType(String contentType, String defaultCharset)
|
||||
{
|
||||
if(StringUtil.isBlank(contentType))
|
||||
{
|
||||
return defaultCharset;
|
||||
}
|
||||
|
||||
QuotedStringTokenizer tok = new QuotedStringTokenizer(contentType, ";", false, false);
|
||||
while(tok.hasMoreTokens())
|
||||
{
|
||||
String str = tok.nextToken().trim();
|
||||
if(str.startsWith("charset="))
|
||||
{
|
||||
return str.substring("charset=".length());
|
||||
}
|
||||
}
|
||||
|
||||
return defaultCharset;
|
||||
}
|
||||
|
||||
public static class NameValue
|
||||
{
|
||||
public String name;
|
||||
public String value;
|
||||
}
|
||||
|
||||
public static class MultipartExpectations
|
||||
{
|
||||
public final String contentType;
|
||||
public final int partCount;
|
||||
public final List<NameValue> partFilenames = new ArrayList<>();
|
||||
public final List<NameValue> partSha1sums = new ArrayList<>();
|
||||
public final List<NameValue> partContainsContents = new ArrayList<>();
|
||||
|
||||
public MultipartExpectations(Path expectationsPath) throws IOException
|
||||
{
|
||||
String parsedContentType = null;
|
||||
String parsedPartCount = "-1";
|
||||
|
||||
try (BufferedReader reader = Files.newBufferedReader(expectationsPath))
|
||||
{
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null)
|
||||
{
|
||||
line = line.trim();
|
||||
if (StringUtil.isBlank(line) || line.startsWith("#"))
|
||||
{
|
||||
// skip blanks and comments
|
||||
continue;
|
||||
}
|
||||
|
||||
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();
|
||||
pair.name = split[1];
|
||||
pair.value = split[2];
|
||||
partContainsContents.add(pair);
|
||||
break;
|
||||
}
|
||||
case "Part-Filename":
|
||||
{
|
||||
NameValue pair = new NameValue();
|
||||
pair.name = split[1];
|
||||
pair.value = split[2];
|
||||
partFilenames.add(pair);
|
||||
break;
|
||||
}
|
||||
case "Part-Sha1sum":
|
||||
{
|
||||
NameValue pair = new NameValue();
|
||||
pair.name = split[1];
|
||||
pair.value = split[2];
|
||||
partSha1sums.add(pair);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new IOException("Bad Line in " + expectationsPath + ": " + line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Objects.requireNonNull(parsedContentType, "Missing required 'Content-Type' declaration: " + expectationsPath);
|
||||
this.contentType = parsedContentType;
|
||||
this.partCount = Integer.parseInt(parsedPartCount);
|
||||
}
|
||||
}
|
||||
|
||||
class NoOpOutputStream extends OutputStream
|
||||
{
|
||||
@Override
|
||||
public void write(byte[] b) throws IOException
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,711 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import org.eclipse.jetty.http.MultiPartParser.State;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
|
||||
public class MultiPartParserTest
|
||||
{
|
||||
|
||||
@Test
|
||||
public void testEmptyPreamble()
|
||||
{
|
||||
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler(){},"BOUNDARY");
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoPreamble()
|
||||
{
|
||||
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler(){},"BOUNDARY");
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY \r\n");
|
||||
parser.parse(data,false);
|
||||
assertTrue(parser.isState(State.BODY_PART));
|
||||
assertThat(data.remaining(),is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreamble()
|
||||
{
|
||||
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler(){},"BOUNDARY");
|
||||
ByteBuffer data;
|
||||
|
||||
data = BufferUtil.toBuffer("This is not part of a part\r\n");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
|
||||
data = BufferUtil.toBuffer("More data that almost includes \n--BOUNDARY but no CR before.");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
|
||||
data = BufferUtil.toBuffer("Could be a boundary \r\n--BOUNDAR");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
|
||||
data = BufferUtil.toBuffer("but not it isn't \r\n--BOUN");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
|
||||
data = BufferUtil.toBuffer("DARX nor is this");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreambleCompleteBoundary()
|
||||
{
|
||||
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler(){},"BOUNDARY");
|
||||
ByteBuffer data;
|
||||
|
||||
data = BufferUtil.toBuffer("This is not part of a part\r\n--BOUNDARY \r\n");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.BODY_PART));
|
||||
assertThat(data.remaining(),is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreambleSplitBoundary()
|
||||
{
|
||||
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler(){},"BOUNDARY");
|
||||
ByteBuffer data;
|
||||
|
||||
data = BufferUtil.toBuffer("This is not part of a part\r\n");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
data = BufferUtil.toBuffer("-");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
data = BufferUtil.toBuffer("-");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
data = BufferUtil.toBuffer("B");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
data = BufferUtil.toBuffer("OUNDARY-");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.DELIMITER_CLOSE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
data = BufferUtil.toBuffer("ignore\r");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.DELIMITER_PADDING));
|
||||
assertThat(data.remaining(),is(0));
|
||||
data = BufferUtil.toBuffer("\n");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.BODY_PART));
|
||||
assertThat(data.remaining(),is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartNoFields()
|
||||
{
|
||||
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler(){},"BOUNDARY");
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n\r\n");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.FIRST_OCTETS));
|
||||
assertThat(data.remaining(),is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartFields()
|
||||
{
|
||||
TestHandler handler = new TestHandler()
|
||||
{
|
||||
@Override
|
||||
public boolean headerComplete()
|
||||
{
|
||||
super.headerComplete();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
|
||||
+ "name0: value0\r\n"
|
||||
+ "name1 :value1 \r\n"
|
||||
+ "name2:value\r\n"
|
||||
+ " 2\r\n"
|
||||
+ "\r\n"
|
||||
+ "Content");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.FIRST_OCTETS));
|
||||
assertThat(data.remaining(),is(7));
|
||||
assertThat(handler.fields,Matchers.contains("name0: value0","name1: value1", "name2: value 2", "<<COMPLETE>>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartNoContent()
|
||||
{
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
|
||||
+ "name: value\r\n"
|
||||
+ "\r\n"
|
||||
+ "\r\n"
|
||||
+ "--BOUNDARY");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("<<LAST>>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartNoContentNoCRLF()
|
||||
{
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
|
||||
+ "name: value\r\n"
|
||||
+ "\r\n"
|
||||
+ "--BOUNDARY");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("<<LAST>>"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartContentLookingLikeNoCRLF()
|
||||
{
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
|
||||
+ "name: value\r\n"
|
||||
+ "\r\n"
|
||||
+ "-");
|
||||
parser.parse(data,false);
|
||||
data = BufferUtil.toBuffer("Content!");
|
||||
parser.parse(data,false);
|
||||
|
||||
|
||||
assertThat(parser.getState(), is(State.OCTETS));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("-","Content!"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartPartialContent()
|
||||
{
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
|
||||
+ "name: value\n"
|
||||
+ "\r\n"
|
||||
+ "Hello\r\n");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.OCTETS));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("Hello"));
|
||||
|
||||
data = BufferUtil.toBuffer(
|
||||
"Now is the time for all good ment to come to the aid of the party.\r\n"
|
||||
+ "How now brown cow.\r\n"
|
||||
+ "The quick brown fox jumped over the lazy dog.\r\n"
|
||||
+ "this is not a --BOUNDARY\r\n");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.OCTETS));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("Hello","\r\n","Now is the time for all good ment to come to the aid of the party.\r\n"
|
||||
+ "How now brown cow.\r\n"
|
||||
+ "The quick brown fox jumped over the lazy dog.\r\n"
|
||||
+ "this is not a --BOUNDARY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartShortContent()
|
||||
{
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
|
||||
+ "name: value\n"
|
||||
+ "\r\n"
|
||||
+ "Hello\r\n"
|
||||
+ "--BOUNDARY");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("Hello","<<LAST>>"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testFirstPartLongContent()
|
||||
{
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
|
||||
+ "name: value\n"
|
||||
+ "\r\n"
|
||||
+ "Now is the time for all good ment to come to the aid of the party.\r\n"
|
||||
+ "How now brown cow.\r\n"
|
||||
+ "The quick brown fox jumped over the lazy dog.\r\n"
|
||||
+ "\r\n"
|
||||
+ "--BOUNDARY");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("Now is the time for all good ment to come to the aid of the party.\r\n"
|
||||
+ "How now brown cow.\r\n"
|
||||
+ "The quick brown fox jumped over the lazy dog.\r\n","<<LAST>>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartLongContentNoCarriageReturn()
|
||||
{
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
//boundary still requires carriage return
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\n"
|
||||
+ "name: value\n"
|
||||
+ "\n"
|
||||
+ "Now is the time for all good men to come to the aid of the party.\n"
|
||||
+ "How now brown cow.\n"
|
||||
+ "The quick brown fox jumped over the lazy dog.\n"
|
||||
+ "\r\n"
|
||||
+ "--BOUNDARY");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("Now is the time for all good men to come to the aid of the party.\n"
|
||||
+ "How now brown cow.\n"
|
||||
+ "The quick brown fox jumped over the lazy dog.\n","<<LAST>>"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBinaryPart()
|
||||
{
|
||||
byte[] random = new byte[8192];
|
||||
final ByteBuffer bytes = BufferUtil.allocate(random.length);
|
||||
ThreadLocalRandom.current().nextBytes(random);
|
||||
// Arrays.fill(random,(byte)'X');
|
||||
|
||||
TestHandler handler = new TestHandler()
|
||||
{
|
||||
@Override
|
||||
public boolean content(ByteBuffer buffer, boolean last)
|
||||
{
|
||||
BufferUtil.append(bytes,buffer);
|
||||
return last;
|
||||
}
|
||||
};
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
String preamble = "Blah blah blah\r\n--BOUNDARY\r\n\r\n";
|
||||
String epilogue = "\r\n--BOUNDARY\r\nBlah blah blah!\r\n";
|
||||
|
||||
ByteBuffer data = BufferUtil.allocate(preamble.length()+random.length+epilogue.length());
|
||||
BufferUtil.append(data,BufferUtil.toBuffer(preamble));
|
||||
BufferUtil.append(data,ByteBuffer.wrap(random));
|
||||
BufferUtil.append(data,BufferUtil.toBuffer(epilogue));
|
||||
|
||||
parser.parse(data,true);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(data.remaining(),is(19));
|
||||
assertThat(bytes.array(),is(random));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEpilogue() {
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer(""
|
||||
+ "--BOUNDARY\r\n"
|
||||
+ "name: value\n"
|
||||
+ "\r\n"
|
||||
+ "Hello\r\n"
|
||||
+ "--BOUNDARY--"
|
||||
+ "epilogue here:"
|
||||
+ "\r\n"
|
||||
+ "--BOUNDARY--"
|
||||
+ "\r\n"
|
||||
+ "--BOUNDARY");
|
||||
|
||||
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("Hello","<<LAST>>"));
|
||||
|
||||
parser.parse(data,true);
|
||||
assertThat(parser.getState(), is(State.END));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testMultipleContent() {
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer(""
|
||||
+ "--BOUNDARY\r\n"
|
||||
+ "name: value\n"
|
||||
+ "\r\n"
|
||||
+ "Hello"
|
||||
+ "\r\n"
|
||||
+ "--BOUNDARY\r\n"
|
||||
+ "powerLevel: 9001\n"
|
||||
+ "\r\n"
|
||||
+ "secondary"
|
||||
+ "\r\n"
|
||||
+ "content"
|
||||
+ "\r\n--BOUNDARY--"
|
||||
+ "epilogue here");
|
||||
|
||||
/* Test First Content Section */
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content, Matchers.contains("Hello","<<LAST>>"));
|
||||
|
||||
/* Test Second Content Section */
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>","powerLevel: 9001","<<COMPLETE>>"));
|
||||
assertThat(handler.content, Matchers.contains("Hello","<<LAST>>","secondary\r\ncontent","<<LAST>>"));
|
||||
|
||||
/* Test Progression to END State */
|
||||
parser.parse(data,true);
|
||||
assertThat(parser.getState(), is(State.END));
|
||||
assertThat(data.remaining(),is(0));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void testCrAsLineTermination() {
|
||||
TestHandler handler = new TestHandler()
|
||||
{
|
||||
@Override public boolean messageComplete(){ return true; }
|
||||
|
||||
@Override
|
||||
public boolean content(ByteBuffer buffer, boolean last)
|
||||
{
|
||||
super.content(buffer,last);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
MultiPartParser parser = new MultiPartParser(handler,"AaB03x");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer(
|
||||
"--AaB03x\r\n"+
|
||||
"content-disposition: form-data; name=\"field1\"\r\n"+
|
||||
"\r"+
|
||||
"Joe Blow\r\n"+
|
||||
"--AaB03x--\r\n");
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
parser.parse(data,true);
|
||||
fail("Invalid End of Line");
|
||||
}
|
||||
catch(BadMessageException e) {
|
||||
assertTrue(e.getMessage().contains("Bad EOL"));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void splitTest()
|
||||
{
|
||||
TestHandler handler = new TestHandler()
|
||||
{
|
||||
@Override
|
||||
public boolean messageComplete()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean content(ByteBuffer buffer, boolean last)
|
||||
{
|
||||
super.content(buffer,last);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
MultiPartParser parser = new MultiPartParser(handler,"---------------------------9051914041544843365972754266");
|
||||
ByteBuffer data = BufferUtil.toBuffer(""+
|
||||
"POST / HTTP/1.1\n" +
|
||||
"Host: localhost:8000\n" +
|
||||
"User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:29.0) Gecko/20100101 Firefox/29.0\n" +
|
||||
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n" +
|
||||
"Accept-Language: en-US,en;q=0.5\n" +
|
||||
"Accept-Encoding: gzip, deflate\n" +
|
||||
"Cookie: __atuvc=34%7C7; permanent=0; _gitlab_session=226ad8a0be43681acf38c2fab9497240; __profilin=p%3Dt; request_method=GET\n" +
|
||||
"Connection: keep-alive\n" +
|
||||
"Content-Type: multipart/form-data; boundary=---------------------------9051914041544843365972754266\n" +
|
||||
"Content-Length: 554\n" +
|
||||
"\r\n" +
|
||||
"-----------------------------9051914041544843365972754266\n" +
|
||||
"Content-Disposition: form-data; name=\"text\"\n" +
|
||||
"\n" +
|
||||
"text default\r\n" +
|
||||
"-----------------------------9051914041544843365972754266\n" +
|
||||
"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\n" +
|
||||
"Content-Type: text/plain\n" +
|
||||
"\n" +
|
||||
"Content of a.txt.\n" +
|
||||
"\r\n" +
|
||||
"-----------------------------9051914041544843365972754266\n" +
|
||||
"Content-Disposition: form-data; name=\"file2\"; filename=\"a.html\"\n" +
|
||||
"Content-Type: text/html\n" +
|
||||
"\n" +
|
||||
"<!DOCTYPE html><title>Content of a.html.</title>\n" +
|
||||
"\r\n" +
|
||||
"-----------------------------9051914041544843365972754266\n" +
|
||||
"Field1: value1\n" +
|
||||
"Field2: value2\n" +
|
||||
"Field3: value3\n" +
|
||||
"Field4: value4\n" +
|
||||
"Field5: value5\n" +
|
||||
"Field6: value6\n" +
|
||||
"Field7: value7\n" +
|
||||
"Field8: value8\n" +
|
||||
"Field9: value\n" +
|
||||
" 9\n" +
|
||||
"\r\n" +
|
||||
"-----------------------------9051914041544843365972754266\n" +
|
||||
"Field1: value1\n" +
|
||||
"\r\n"+
|
||||
"But the amount of denudation which the strata have\n" +
|
||||
"in many places suffered, independently of the rate\n" +
|
||||
"of accumulation of the degraded matter, probably\n" +
|
||||
"offers the best evidence of the lapse of time. I remember\n" +
|
||||
"having been much struck with the evidence of\n" +
|
||||
"denudation, when viewing volcanic islands, which\n" +
|
||||
"have been worn by the waves and pared all round\n" +
|
||||
"into perpendicular cliffs of one or two thousand feet\n" +
|
||||
"in height; for the gentle slope of the lava-streams,\n" +
|
||||
"due to their formerly liquid state, showed at a glance\n" +
|
||||
"how far the hard, rocky beds had once extended into\n" +
|
||||
"the open ocean.\n" +
|
||||
"\r\n" +
|
||||
"-----------------------------9051914041544843365972754266--" +
|
||||
"===== ajlkfja;lkdj;lakjd;lkjf ==== epilogue here ==== kajflajdfl;kjafl;kjl;dkfja ====\n\r\n\r\r\r\n\n\n");
|
||||
|
||||
|
||||
int length = data.remaining();
|
||||
for(int i = 0; i<length-1; i++){
|
||||
//partition 0 to i
|
||||
ByteBuffer dataSeg = data.slice();
|
||||
dataSeg.position(0);
|
||||
dataSeg.limit(i);
|
||||
assertThat("First "+i,parser.parse(dataSeg,false),is(false));
|
||||
|
||||
//partition i
|
||||
dataSeg = data.slice();
|
||||
dataSeg.position(i);
|
||||
dataSeg.limit(i+1);
|
||||
assertThat("Second "+i,parser.parse(dataSeg,false),is(false));
|
||||
|
||||
//partition i to length
|
||||
dataSeg = data.slice();
|
||||
dataSeg.position(i+1);
|
||||
dataSeg.limit(length);
|
||||
assertThat("Third "+i,parser.parse(dataSeg,true),is(true));
|
||||
|
||||
assertThat(handler.fields, Matchers.contains( "Content-Disposition: form-data; name=\"text\"","<<COMPLETE>>"
|
||||
, "Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\""
|
||||
, "Content-Type: text/plain","<<COMPLETE>>"
|
||||
, "Content-Disposition: form-data; name=\"file2\"; filename=\"a.html\""
|
||||
, "Content-Type: text/html","<<COMPLETE>>"
|
||||
, "Field1: value1", "Field2: value2", "Field3: value3"
|
||||
, "Field4: value4", "Field5: value5", "Field6: value6"
|
||||
, "Field7: value7", "Field8: value8", "Field9: value 9", "<<COMPLETE>>"
|
||||
, "Field1: value1","<<COMPLETE>>"));
|
||||
|
||||
|
||||
assertThat(handler.contentString(), is(new String("text default"+"<<LAST>>"
|
||||
+ "Content of a.txt.\n"+"<<LAST>>"
|
||||
+ "<!DOCTYPE html><title>Content of a.html.</title>\n"+"<<LAST>>"
|
||||
+ "<<LAST>>"
|
||||
+ "But the amount of denudation which the strata have\n" +
|
||||
"in many places suffered, independently of the rate\n" +
|
||||
"of accumulation of the degraded matter, probably\n" +
|
||||
"offers the best evidence of the lapse of time. I remember\n" +
|
||||
"having been much struck with the evidence of\n" +
|
||||
"denudation, when viewing volcanic islands, which\n" +
|
||||
"have been worn by the waves and pared all round\n" +
|
||||
"into perpendicular cliffs of one or two thousand feet\n" +
|
||||
"in height; for the gentle slope of the lava-streams,\n" +
|
||||
"due to their formerly liquid state, showed at a glance\n" +
|
||||
"how far the hard, rocky beds had once extended into\n" +
|
||||
"the open ocean.\n"+ "<<LAST>>")));
|
||||
|
||||
handler.clear();
|
||||
parser.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testGeneratedForm()
|
||||
{
|
||||
TestHandler handler = new TestHandler()
|
||||
{
|
||||
@Override
|
||||
public boolean messageComplete()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean content(ByteBuffer buffer, boolean last)
|
||||
{
|
||||
super.content(buffer,last);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean headerComplete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
MultiPartParser parser = new MultiPartParser(handler,"WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW");
|
||||
ByteBuffer data = BufferUtil.toBuffer(""
|
||||
+ "Content-Type: multipart/form-data; boundary=WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" +
|
||||
"\r\n" +
|
||||
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" +
|
||||
"Content-Disposition: form-data; name=\"part1\"\r\n" +
|
||||
"\n" +
|
||||
"wNfミxVamt\r\n" +
|
||||
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\n" +
|
||||
"Content-Disposition: form-data; name=\"part2\"\r\n" +
|
||||
"\r\n" +
|
||||
"&ᄈ취ᅢO\r\n" +
|
||||
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW--");
|
||||
|
||||
parser.parse(data,true);
|
||||
assertThat(parser.getState(), is(State.END));
|
||||
assertThat(handler.fields.size(), is(2));
|
||||
|
||||
}
|
||||
|
||||
|
||||
static class TestHandler implements MultiPartParser.Handler
|
||||
{
|
||||
List<String> fields = new ArrayList<>();
|
||||
List<String> content = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void parsedField(String name, String value)
|
||||
{
|
||||
fields.add(name+": "+value);
|
||||
}
|
||||
|
||||
public String contentString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(String s : content) sb.append(s);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean headerComplete()
|
||||
{
|
||||
fields.add("<<COMPLETE>>");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean content(ByteBuffer buffer, boolean last)
|
||||
{
|
||||
if (BufferUtil.hasContent(buffer))
|
||||
content.add(BufferUtil.toString(buffer));
|
||||
if (last)
|
||||
content.add("<<LAST>>");
|
||||
return last;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
fields.clear();
|
||||
content.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,297 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.jmh;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.servlet.MultipartConfigElement;
|
||||
import javax.servlet.http.Part;
|
||||
|
||||
import org.eclipse.jetty.http.MultiPartFormInputStream;
|
||||
import org.eclipse.jetty.http.MultiPartCaptureTest.MultipartExpectations;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.toolchain.test.TestingDir;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.junit.Rule;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Level;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.Param;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.Setup;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.annotations.TearDown;
|
||||
import org.openjdk.jmh.profile.CompilerProfiler;
|
||||
import org.openjdk.jmh.runner.Runner;
|
||||
import org.openjdk.jmh.runner.RunnerException;
|
||||
import org.openjdk.jmh.runner.options.Options;
|
||||
import org.openjdk.jmh.runner.options.OptionsBuilder;
|
||||
|
||||
@State(Scope.Benchmark)
|
||||
public class MultiPartBenchmark
|
||||
{
|
||||
|
||||
public static final int MAX_FILE_SIZE = Integer.MAX_VALUE;
|
||||
public static final int MAX_REQUEST_SIZE = Integer.MAX_VALUE;
|
||||
public static final int FILE_SIZE_THRESHOLD = 50;
|
||||
|
||||
public int count = 0;
|
||||
static String _contentType;
|
||||
static File _file;
|
||||
static int _numSections;
|
||||
static int _numBytesPerSection;
|
||||
|
||||
|
||||
public static List<String> data = new ArrayList<>();
|
||||
static
|
||||
{
|
||||
// Capture of raw request body contents from various browsers
|
||||
|
||||
// simple form - 2 fields
|
||||
data.add("browser-capture-form1-android-chrome");
|
||||
data.add("browser-capture-form1-android-firefox");
|
||||
data.add("browser-capture-form1-chrome");
|
||||
data.add("browser-capture-form1-edge");
|
||||
data.add("browser-capture-form1-firefox");
|
||||
data.add("browser-capture-form1-ios-safari");
|
||||
data.add("browser-capture-form1-msie");
|
||||
data.add("browser-capture-form1-osx-safari");
|
||||
|
||||
// form submitted as shift-jis
|
||||
data.add("browser-capture-sjis-form-edge");
|
||||
data.add("browser-capture-sjis-form-msie");
|
||||
|
||||
// form submitted as shift-jis (with HTML5 specific hidden _charset_ field)
|
||||
data.add("browser-capture-sjis-charset-form-edge");
|
||||
data.add("browser-capture-sjis-charset-form-msie");
|
||||
|
||||
// form submitted with simple file upload
|
||||
data.add("browser-capture-form-fileupload-android-chrome");
|
||||
data.add("browser-capture-form-fileupload-android-firefox");
|
||||
data.add("browser-capture-form-fileupload-chrome");
|
||||
data.add("browser-capture-form-fileupload-edge");
|
||||
data.add("browser-capture-form-fileupload-firefox");
|
||||
data.add("browser-capture-form-fileupload-ios-safari");
|
||||
data.add("browser-capture-form-fileupload-msie");
|
||||
data.add("browser-capture-form-fileupload-safari");
|
||||
|
||||
// form submitted with 2 files (1 binary, 1 text) and 2 text fields
|
||||
data.add("browser-capture-form-fileupload-alt-chrome");
|
||||
data.add("browser-capture-form-fileupload-alt-edge");
|
||||
data.add("browser-capture-form-fileupload-alt-firefox");
|
||||
data.add("browser-capture-form-fileupload-alt-msie");
|
||||
data.add("browser-capture-form-fileupload-alt-safari");
|
||||
}
|
||||
|
||||
|
||||
@Param({"UTIL","HTTP"})
|
||||
public static String parserType;
|
||||
|
||||
@Setup(Level.Trial)
|
||||
public static void setupTrial() throws Exception
|
||||
{
|
||||
_file = File.createTempFile("test01",null);
|
||||
_file.deleteOnExit();
|
||||
|
||||
_numSections = 1;
|
||||
_numBytesPerSection = 1024*1024*10;
|
||||
|
||||
_contentType = "multipart/form-data, boundary=WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW";
|
||||
String initialBoundary = "--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n";
|
||||
String boundary = "\r\n--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n";
|
||||
String closingBoundary = "\r\n--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW--\r\n";
|
||||
String headerStart = "Content-Disposition: form-data; name=\"";
|
||||
|
||||
|
||||
for(int i=0; i<_numSections; i++) {
|
||||
//boundary and headers
|
||||
if(i==0)
|
||||
Files.write(_file.toPath(), initialBoundary.getBytes(), StandardOpenOption.APPEND);
|
||||
else
|
||||
Files.write(_file.toPath(), boundary.getBytes(), StandardOpenOption.APPEND);
|
||||
|
||||
Files.write(_file.toPath(), headerStart.getBytes(), StandardOpenOption.APPEND);
|
||||
Files.write(_file.toPath(), new String("part"+(i+1)).getBytes(), StandardOpenOption.APPEND);
|
||||
Files.write(_file.toPath(), new String("\"\r\n\r\n").getBytes(), StandardOpenOption.APPEND);
|
||||
|
||||
//append random data
|
||||
byte[] data = new byte[_numBytesPerSection];
|
||||
new Random().nextBytes(data);
|
||||
Files.write(_file.toPath(), data, StandardOpenOption.APPEND);
|
||||
}
|
||||
|
||||
//closing boundary
|
||||
Files.write(_file.toPath(), closingBoundary.getBytes(), StandardOpenOption.APPEND);
|
||||
|
||||
/*
|
||||
// print out file to verify that it contains valid contents (just for testing)
|
||||
InputStream in = Files.newInputStream(_file.toPath());
|
||||
System.out.println();
|
||||
while(in.available()>0) {
|
||||
byte b[] = new byte[100];
|
||||
int read = in.read(b,0,100);
|
||||
for(int i=0; i<read; i++)
|
||||
System.out.print((char)b[i]);
|
||||
}
|
||||
System.out.println();
|
||||
|
||||
//exit
|
||||
throw new RuntimeException("Stop Here");
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode({Mode.AverageTime})
|
||||
@SuppressWarnings("deprecation")
|
||||
public long testLargeGenerated() throws Exception
|
||||
{
|
||||
Path multipartRawFile = _file.toPath();
|
||||
Path outputDir = new File("/tmp").toPath();
|
||||
|
||||
MultipartConfigElement config = newMultipartConfigElement(outputDir);
|
||||
|
||||
try (InputStream in = Files.newInputStream(multipartRawFile))
|
||||
{
|
||||
switch(parserType)
|
||||
{
|
||||
case "HTTP":
|
||||
{
|
||||
MultiPartFormInputStream parser = new MultiPartFormInputStream(in, _contentType, config, outputDir.toFile());
|
||||
if(parser.getParts().size() != _numSections)
|
||||
throw new IllegalStateException("Incorrect Parsing");
|
||||
for(Part p : parser.getParts()) {
|
||||
count += p.getSize();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "UTIL":
|
||||
{
|
||||
org.eclipse.jetty.util.MultiPartInputStreamParser parser = new org.eclipse.jetty.util.MultiPartInputStreamParser(in, _contentType,config,outputDir.toFile());
|
||||
|
||||
// TODO this is using the http version of part (which should be the same anyway)
|
||||
if(parser.getParts().size() != _numSections)
|
||||
throw new IllegalStateException("Incorrect Parsing");
|
||||
for(Part p : parser.getParts()) {
|
||||
count += p.getSize();
|
||||
}
|
||||
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unknown parserType Parameter");
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@TearDown(Level.Trial)
|
||||
public static void stopTrial() throws Exception
|
||||
{
|
||||
_file = null;
|
||||
}
|
||||
|
||||
private MultipartConfigElement newMultipartConfigElement(Path path)
|
||||
{
|
||||
return new MultipartConfigElement(path.toString(), MAX_FILE_SIZE, MAX_REQUEST_SIZE, FILE_SIZE_THRESHOLD);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode({Mode.AverageTime})
|
||||
@SuppressWarnings("deprecation")
|
||||
public long testParser() throws Exception
|
||||
{
|
||||
for(String multiPart : data)
|
||||
{
|
||||
Path multipartRawFile = MavenTestingUtils.getTestResourcePathFile("multipart/" + multiPart + ".raw");
|
||||
Path expectationPath = MavenTestingUtils.getTestResourcePathFile("multipart/" + multiPart + ".expected.txt");
|
||||
|
||||
Path outputDir = new File("/tmp").toPath();
|
||||
|
||||
MultipartExpectations multipartExpectations = new MultipartExpectations(expectationPath);
|
||||
MultipartConfigElement config = newMultipartConfigElement(outputDir);
|
||||
|
||||
try (InputStream in = Files.newInputStream(multipartRawFile))
|
||||
{
|
||||
switch(parserType)
|
||||
{
|
||||
case "HTTP":
|
||||
{
|
||||
MultiPartFormInputStream parser = new MultiPartFormInputStream(in, multipartExpectations.contentType, config, outputDir.toFile());
|
||||
for(Part p : parser.getParts()) {
|
||||
count += p.getSize();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "UTIL":
|
||||
{
|
||||
org.eclipse.jetty.util.MultiPartInputStreamParser parser = new org.eclipse.jetty.util.MultiPartInputStreamParser(in,multipartExpectations.contentType,config,outputDir.toFile());
|
||||
|
||||
// TODO this is using the http version of part (which should be the same anyway)
|
||||
for(Part p : parser.getParts()) {
|
||||
count += p.getSize();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unknown parserType Parameter");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws RunnerException
|
||||
{
|
||||
Options opt = new OptionsBuilder()
|
||||
.include(MultiPartBenchmark.class.getSimpleName())
|
||||
.warmupIterations(20)
|
||||
.measurementIterations(10)
|
||||
.forks(1)
|
||||
.threads(1)
|
||||
// .syncIterations(true) // Don't start all threads at same time
|
||||
// .warmupTime(new TimeValue(10000,TimeUnit.MILLISECONDS))
|
||||
// .measurementTime(new TimeValue(10000,TimeUnit.MILLISECONDS))
|
||||
// .addProfiler(CompilerProfiler.class)
|
||||
// .addProfiler(LinuxPerfProfiler.class)
|
||||
// .addProfiler(LinuxPerfNormProfiler.class)
|
||||
// .addProfiler(LinuxPerfAsmProfiler.class)
|
||||
// .resultFormat(ResultFormatType.CSV)
|
||||
.build();
|
||||
|
||||
new Runner(opt).run();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
Request-Header|Accept-Encoding|gzip,deflate
|
||||
Request-Header|Connection|keep-alive
|
||||
Request-Header|Content-Length|248
|
||||
Request-Header|Content-Type|multipart/form-data; boundary=DHbU6ChASebwm4iE8z9Lakv4ybMmkp
|
||||
Request-Header|Host|localhost:9090
|
||||
Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
|
||||
Request-Header|X-BrowserId|apache-httpcomp
|
||||
Parts-Count|1
|
||||
Part-ContainsContents|company|bob+%26+frank%27s+shoe+repair
|
|
@ -0,0 +1,7 @@
|
|||
--DHbU6ChASebwm4iE8z9Lakv4ybMmkp
|
||||
Content-Disposition: form-data; name="company"
|
||||
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
bob+%26+frank%27s+shoe+repair
|
||||
--DHbU6ChASebwm4iE8z9Lakv4ybMmkp--
|
|
@ -0,0 +1,15 @@
|
|||
Request-Header|Accept-Encoding|gzip,deflate
|
||||
Request-Header|Connection|keep-alive
|
||||
Request-Header|Content-Length|22940
|
||||
Request-Header|Content-Type|multipart/form-data; boundary=owr6UQGvVNunA_sx2AsizBtyq_uK-OjsQXrF
|
||||
Request-Header|Host|localhost:9090
|
||||
Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
|
||||
Request-Header|X-BrowserId|apache-httpcomp
|
||||
Parts-Count|6
|
||||
Part-ContainsContents|pi|3.14159265358979323846264338327950288419716939937510
|
||||
Part-ContainsContents|company|bob & frank's shoe repair
|
||||
Part-ContainsContents|power|ꬵо𝗋ⲥ𝖾
|
||||
Part-ContainsContents|japanese|オープンソース
|
||||
Part-ContainsContents|hello|日食桟橋
|
||||
Part-Filename|upload_file|filename
|
||||
Part-Sha1sum|upload_file|e75b73644afe9b234d70da9ff225229de68cdff8
|
Binary file not shown.
|
@ -0,0 +1,15 @@
|
|||
Request-Header|Accept-Encoding|gzip
|
||||
Request-Header|Connection|close
|
||||
Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundary1275gffetpxz8o0q
|
||||
Request-Header|Host|localhost:9090
|
||||
Request-Header|Transfer-Encoding|chunked
|
||||
Request-Header|User-Agent|Jetty/9.4.9.v20180320
|
||||
Request-Header|X-BrowserId|jetty-client
|
||||
Parts-Count|6
|
||||
Part-ContainsContents|pi|3.14159265358979323846264338327950288419716939937510
|
||||
Part-ContainsContents|company|bob & frank's shoe repair
|
||||
Part-ContainsContents|power|ꬵо𝗋ⲥ𝖾
|
||||
Part-ContainsContents|japanese|オープンソース
|
||||
Part-ContainsContents|hello|日食桟橋
|
||||
Part-Filename|upload_file|filename
|
||||
Part-Sha1sum|upload_file|e75b73644afe9b234d70da9ff225229de68cdff8
|
Binary file not shown.
|
@ -0,0 +1,8 @@
|
|||
Request-Header|Accept-Encoding|gzip,deflate
|
||||
Request-Header|Connection|keep-alive
|
||||
Request-Header|Content-Length|1815
|
||||
Request-Header|Content-Type|multipart/form-data; boundary=QW3F8Fg64P2J2dpfEKGKlX0Q9QF2a8SK_7YH
|
||||
Request-Header|Host|localhost:9090
|
||||
Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
|
||||
Request-Header|X-BrowserId|apache-httpcomp
|
||||
Parts-Count|10
|
Binary file not shown.
|
@ -0,0 +1,8 @@
|
|||
Request-Header|Accept-Encoding|gzip
|
||||
Request-Header|Connection|close
|
||||
Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundary14beb4to333d91v8
|
||||
Request-Header|Host|localhost:9090
|
||||
Request-Header|Transfer-Encoding|chunked
|
||||
Request-Header|User-Agent|Jetty/9.4.9.v20180320
|
||||
Request-Header|X-BrowserId|jetty-client
|
||||
Parts-Count|10
|
|
@ -0,0 +1,51 @@
|
|||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="pi"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
3.14159265358979323846264338327950288419716939937510
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="pi"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
3.14159
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="pi"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
3
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="pi"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
π
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="pi"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
π
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="pi"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
%CF%80
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="pi"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
π = C/d
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="π"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
3.14
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="%CF%80"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
Approximately 3.14
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="%FE%FF%03%C0"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
Approximately 3.14
|
||||
--JettyHttpClientBoundary14beb4to333d91v8--
|
|
@ -0,0 +1,11 @@
|
|||
Request-Header|Accept-Encoding|gzip,deflate
|
||||
Request-Header|Connection|keep-alive
|
||||
Request-Header|Content-Length|31148
|
||||
Request-Header|Content-Type|multipart/form-data; boundary=qqr2YBBR31U4xVib4vaVuIsrwNY1iw
|
||||
Request-Header|Host|localhost:9090
|
||||
Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
|
||||
Request-Header|X-BrowserId|apache-httpcomp
|
||||
Parts-Count|169
|
||||
Part-ContainsContents|count|168
|
||||
Part-ContainsContents|persian-UTF-8|برج بابل
|
||||
Part-ContainsContents|persian-CESU-8|برج بابل
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,11 @@
|
|||
Request-Header|Accept-Encoding|gzip
|
||||
Request-Header|Connection|close
|
||||
Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Request-Header|Host|localhost:9090
|
||||
Request-Header|Transfer-Encoding|chunked
|
||||
Request-Header|User-Agent|Jetty/9.4.9.v20180320
|
||||
Request-Header|X-BrowserId|jetty-client
|
||||
Parts-Count|169
|
||||
Part-ContainsContents|count|168
|
||||
Part-ContainsContents|persian-UTF-8|برج بابل
|
||||
Part-ContainsContents|persian-CESU-8|برج بابل
|
|
@ -0,0 +1,846 @@
|
|||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-Big5"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-Big5-HKSCS"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-CESU-8"
|
||||
Content-Type: text/plain
|
||||
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-EUC-JP"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-EUC-KR"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-GB18030"
|
||||
Content-Type: text/plain
|
||||
|
||||
<EFBFBD>1<EFBFBD>0<EFBFBD>1<EFBFBD>9<EFBFBD>1<EFBFBD>4 <20>1<EFBFBD>0<EFBFBD>1<EFBFBD>9<EFBFBD>1<EFBFBD>0<EFBFBD>1<EFBFBD>8
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-GB2312"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-GBK"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM-Thai"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM00858"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01140"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01141"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01142"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01143"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01144"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01145"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01146"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01147"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01148"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01149"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM037"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM1026"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM1047"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM273"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM277"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM278"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM280"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM284"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM285"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM290"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM297"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM420"
|
||||
Content-Type: text/plain
|
||||
|
||||
Xug@XVX<56>
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM424"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM437"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM500"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM775"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM850"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM852"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM855"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM857"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM860"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM861"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM862"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM863"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM864"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM865"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM866"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM868"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM869"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM870"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM871"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM918"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-2022-JP"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-2022-JP-2"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-2022-KR"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-1"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-13"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-15"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-2"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-3"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-4"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-5"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-6"
|
||||
Content-Type: text/plain
|
||||
|
||||
<EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-7"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-8"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-9"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-JIS_X0201"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-JIS_X0212-1990"
|
||||
Content-Type: text/plain
|
||||
|
||||
"D"D"D"D"D"D"D"D
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-KOI8-R"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-KOI8-U"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-Shift_JIS"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-TIS-620"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-US-ASCII"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-UTF-16"
|
||||
Content-Type: text/plain
|
||||
|
||||
<EFBFBD><EFBFBD>(1, |