Merge remote-tracking branch 'origin/jetty-9.4.x' into jetty-10.0.x
This commit is contained in:
commit
9390c2df3f
|
@ -3,7 +3,9 @@
|
|||
*.txt eol=lf
|
||||
*.properties eol=lf
|
||||
*.java eol=lf
|
||||
*.mod eol=lf
|
||||
*.adoc eol=lf
|
||||
*.xml eol=lf
|
||||
Jenkinsfile eol=lf
|
||||
*.js eol=lf
|
||||
*.raw binary
|
||||
*.raw binary
|
||||
|
|
|
@ -12,6 +12,7 @@ for (def os in oss) {
|
|||
|
||||
parallel builds
|
||||
|
||||
|
||||
def getFullBuild(jdk, os) {
|
||||
return {
|
||||
node(os) {
|
||||
|
@ -164,6 +165,7 @@ def getFullBuild(jdk, os) {
|
|||
notifyBuild("Compact3 Failure", jdk)
|
||||
throw e
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
#!groovy
|
||||
|
||||
|
||||
node("linux") {
|
||||
// System Dependent Locations
|
||||
def mvntool = tool name: 'maven3.5', type: 'hudson.tasks.Maven$MavenInstallation'
|
||||
def jdktool = tool name: "jdk8", type: 'hudson.model.JDK'
|
||||
def mvnName = 'maven3.5'
|
||||
def localRepo = "${env.JENKINS_HOME}/${env.EXECUTOR_NUMBER}"
|
||||
def settingsName = 'oss-settings.xml'
|
||||
def mavenOpts = '-Xms1g -Xmx4g -Djava.awt.headless=true'
|
||||
|
||||
// Environment
|
||||
List mvnEnv = ["PATH+MVN=${mvntool}/bin", "PATH+JDK=${jdktool}/bin", "JAVA_HOME=${jdktool}/", "MAVEN_HOME=${mvntool}"]
|
||||
mvnEnv.add("MAVEN_OPTS=$mavenOpts")
|
||||
|
||||
|
||||
stage("Checkout") {
|
||||
git url: 'https://github.com/eclipse/jetty.project.git', branch: 'jetty-9.4.x'
|
||||
}
|
||||
|
||||
stage("Compile") {
|
||||
withEnv(mvnEnv) {
|
||||
timeout(time: 15, unit: 'MINUTES') {
|
||||
withMaven(
|
||||
maven: mvnName,
|
||||
jdk: "jdk8",
|
||||
publisherStrategy: 'EXPLICIT',
|
||||
globalMavenSettingsConfig: settingsName,
|
||||
mavenOpts: mavenOpts,
|
||||
mavenLocalRepo: localRepo) {
|
||||
sh "mvn -V -B clean install -DskipTests -T6 -e"
|
||||
}
|
||||
|
||||
}
|
||||
stash name: 'perf-tests', includes: 'jetty-jmh/target/benchmarks.jar'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// jmh run
|
||||
|
||||
stage("jmh-run") {
|
||||
node( 'jmh-build-node' ) {
|
||||
timeout( time: 120, unit: 'MINUTES' ) {
|
||||
withEnv( ["JAVA_HOME=${tool "jdk8"}"] ) {
|
||||
unstash name: 'perf-tests'
|
||||
sh "${env.JAVA_HOME}/bin/java -jar jetty-jmh/target/benchmarks.jar -rff jetty-jmh/target/jmh_result.json -rf json"
|
||||
jmhReport 'jetty-jmh/target/jmh_result.json'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// vim: et:ts=2:sw=2:ft=groovy
|
26
VERSION.txt
26
VERSION.txt
|
@ -1,31 +1,5 @@
|
|||
jetty-10.0.0-SNAPSHOT
|
||||
|
||||
jetty-9.4.12.RC0 - 11 July 2018
|
||||
+ 901 Overriding SSL context KeyStoreType requires explicit override of
|
||||
TrustStoreType
|
||||
+ 2075 Deprecating MultiException
|
||||
+ 2342 File Descriptor Leak: Conscrypt: "Too many open files"
|
||||
+ 2349 HTTP/2 max streams enforcement
|
||||
+ 2398 MultiPartFormInputStream parsing should default to UTF-8, but allowed
|
||||
to be overridden by Request.setCharacterEncoding()
|
||||
+ 2468 EWYK concurrent produce can fail SSL connections
|
||||
+ 2501 Include accepting connections in connection limit
|
||||
+ 2530 Client waits forever for cancelled large uploads
|
||||
+ 2560 Review PathResource exception handling
|
||||
+ 2565 HashLoginService silently ignores file:/ config paths from 9.3.x
|
||||
+ 2631 IllegalArgumentException: Buffering capacity exceeded, from HttpClient
|
||||
HEAD Requests to resources referencing large body contents
|
||||
+ 2648 LdapLoginModule fails with forceBinding=true under Java 9
|
||||
+ 2655 WebSocketClient not removing closed WebSocket Session's from managed
|
||||
beans
|
||||
+ 2662 Remove unnecessary boxing conversions
|
||||
+ 2663 Guard Throwable.addSuppressed() calls
|
||||
+ 2675 Demo rewrite rules prevent URL Session tracking
|
||||
+ 2677 Decode URI before matching against "/favicon.ico"
|
||||
+ 2683 NPE in FrameFlusher toString()
|
||||
+ 2684 MimeTypes.getAssumedEncodings() does not work
|
||||
+ 2696 GcloudDataStore dependency generation broken
|
||||
|
||||
jetty-9.4.11.v20180605 - 05 June 2018
|
||||
+ 1785 Support for vhost@connectorname syntax of virtual hosts
|
||||
+ 2346 Revert stack trace logging for HTTPChannel.onException
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
|
||||
|
||||
[depend]
|
||||
alpn-impl/alpn-9
|
|
@ -47,7 +47,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 PARAM_PATTERN = Pattern.compile("([^=]+)=([^=]+)?");
|
||||
private static final Pattern PARAM_PATTERN = Pattern.compile("([^=]+)=(.*)");
|
||||
private static final Pattern TYPE_PATTERN = Pattern.compile("([^\\s]+)(\\s+(.*))?");
|
||||
private static final Pattern MULTIPLE_CHALLENGE_PATTERN = Pattern.compile("(.*?)\\s*,\\s*([^=\\s,]+(\\s+[^=\\s].*)?)");
|
||||
private static final Pattern BASE64_PATTERN = Pattern.compile("[\\+\\-\\.\\/\\dA-Z_a-z~]+=*");
|
||||
|
|
|
@ -732,4 +732,40 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
|
|||
Assert.assertTrue(headerInfos.get(1).getType().equalsIgnoreCase("Negotiate"));
|
||||
Assert.assertTrue(headerInfos.get(1).getBase64().equals("YIIJvwYGKwYBBQUCoIIJszCCCa+gJDAi="));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void testEqualsInParam()
|
||||
{
|
||||
AuthenticationProtocolHandler aph = new WWWAuthenticationProtocolHandler(client);
|
||||
HeaderInfo headerInfo;
|
||||
|
||||
headerInfo = aph.getHeaderInfo("Digest realm=\"=the=rmo=stat=\", qop=\"=a=u=t=h=\", nonce=\"=1523430383=\"").get(0);
|
||||
Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Digest"));
|
||||
Assert.assertTrue(headerInfo.getParameter("qop").equals("=a=u=t=h="));
|
||||
Assert.assertTrue(headerInfo.getParameter("realm").equals("=the=rmo=stat="));
|
||||
Assert.assertTrue(headerInfo.getParameter("nonce").equals("=1523430383="));
|
||||
|
||||
|
||||
// test multiple authentications
|
||||
List<HeaderInfo> headerInfoList = aph.getHeaderInfo("Digest qop=\"=au=th=\", realm=\"=ther=mostat=\", nonce=\"=152343=0383=\", "
|
||||
+ "Digest realm=\"=thermostat2\", qop=\"=auth2\", nonce=\"=4522530354\", "
|
||||
+ "Digest qop=\"auth3=\", nonce=\"9523570528=\", realm=\"thermostat3=\", ");
|
||||
|
||||
Assert.assertTrue(headerInfoList.get(0).getType().equalsIgnoreCase("Digest"));
|
||||
Assert.assertTrue(headerInfoList.get(0).getParameter("qop").equals("=au=th="));
|
||||
Assert.assertTrue(headerInfoList.get(0).getParameter("realm").equals("=ther=mostat="));
|
||||
Assert.assertTrue(headerInfoList.get(0).getParameter("nonce").equals("=152343=0383="));
|
||||
|
||||
Assert.assertTrue(headerInfoList.get(1).getType().equalsIgnoreCase("Digest"));
|
||||
Assert.assertTrue(headerInfoList.get(1).getParameter("qop").equals("=auth2"));
|
||||
Assert.assertTrue(headerInfoList.get(1).getParameter("realm").equals("=thermostat2"));
|
||||
Assert.assertTrue(headerInfoList.get(1).getParameter("nonce").equals("=4522530354"));
|
||||
|
||||
Assert.assertTrue(headerInfoList.get(2).getType().equalsIgnoreCase("Digest"));
|
||||
Assert.assertTrue(headerInfoList.get(2).getParameter("qop").equals("auth3="));
|
||||
Assert.assertTrue(headerInfoList.get(2).getParameter("realm").equals("thermostat3="));
|
||||
Assert.assertTrue(headerInfoList.get(2).getParameter("nonce").equals("9523570528="));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
package org.eclipse.jetty.http.spi.util;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* This is a sample task. Test cases uses this for testing purpose
|
||||
*
|
||||
*/
|
||||
|
|
|
@ -33,18 +33,6 @@
|
|||
<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>
|
||||
|
|
|
@ -257,7 +257,7 @@ public class HttpURI
|
|||
state=State.PATH;
|
||||
encoded=true;
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
mark=i;
|
||||
if (_scheme==null)
|
||||
|
@ -336,14 +336,14 @@ public class HttpURI
|
|||
path_mark=mark;
|
||||
state=State.PATH;
|
||||
break;
|
||||
|
||||
|
||||
case '.':
|
||||
// it is a path
|
||||
encoded=true;
|
||||
path_mark=mark;
|
||||
state=State.PATH;
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
// it is a path
|
||||
path_mark=mark;
|
||||
|
@ -425,7 +425,7 @@ public class HttpURI
|
|||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case PATH:
|
||||
{
|
||||
switch (c)
|
||||
|
@ -453,9 +453,9 @@ public class HttpURI
|
|||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
case PARAM:
|
||||
{
|
||||
|
@ -747,7 +747,7 @@ public class HttpURI
|
|||
_port=port;
|
||||
_uri=null;
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @param path the path
|
||||
|
@ -769,7 +769,7 @@ public class HttpURI
|
|||
_path=URIUtil.encodePath(path);
|
||||
_decodedPath=path;
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public void setPathQuery(String path)
|
||||
{
|
||||
|
@ -808,7 +808,7 @@ public class HttpURI
|
|||
{
|
||||
return _host!=null;
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public String getAuthority()
|
||||
{
|
||||
|
|
|
@ -163,17 +163,26 @@ public class MetaData implements Iterable<HttpField>
|
|||
|
||||
public Request(String method, HttpScheme scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields)
|
||||
{
|
||||
this(method, new HttpURI(scheme == null ? null : scheme.asString(), hostPort.getHost(), hostPort.getPort(), uri), version, fields);
|
||||
this(method, new HttpURI(scheme == null ? null : scheme.asString(),
|
||||
hostPort==null?null:hostPort.getHost(),
|
||||
hostPort==null?-1:hostPort.getPort(),
|
||||
uri), version, fields);
|
||||
}
|
||||
|
||||
public Request(String method, HttpScheme scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields, long contentLength)
|
||||
{
|
||||
this(method, new HttpURI(scheme == null ? null : scheme.asString(), hostPort.getHost(), hostPort.getPort(), uri), version, fields, contentLength);
|
||||
this(method, new HttpURI(scheme==null?null:scheme.asString(),
|
||||
hostPort==null?null:hostPort.getHost(),
|
||||
hostPort==null?-1:hostPort.getPort(),
|
||||
uri), version, fields, contentLength);
|
||||
}
|
||||
|
||||
public Request(String method, String scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields, long contentLength)
|
||||
{
|
||||
this(method, new HttpURI(scheme, hostPort.getHost(), hostPort.getPort(), uri), version, fields, contentLength);
|
||||
this(method, new HttpURI(scheme,
|
||||
hostPort==null?null:hostPort.getHost(),
|
||||
hostPort==null?-1:hostPort.getPort(),
|
||||
uri), version, fields, contentLength);
|
||||
}
|
||||
|
||||
public Request(Request request)
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.eclipse.jetty.alpn.client.ALPNClientConnectionFactory;
|
|||
import org.eclipse.jetty.http2.BufferingFlowControlStrategy;
|
||||
import org.eclipse.jetty.http2.FlowControlStrategy;
|
||||
import org.eclipse.jetty.http2.api.Session;
|
||||
import org.eclipse.jetty.http2.frames.Frame;
|
||||
import org.eclipse.jetty.http2.frames.SettingsFrame;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.ClientConnectionFactory;
|
||||
|
@ -130,6 +131,8 @@ public class HTTP2Client extends ContainerLifeCycle
|
|||
private List<String> protocols = Arrays.asList("h2", "h2-17", "h2-16", "h2-15", "h2-14");
|
||||
private int initialSessionRecvWindow = 16 * 1024 * 1024;
|
||||
private int initialStreamRecvWindow = 8 * 1024 * 1024;
|
||||
private int maxFrameLength = Frame.DEFAULT_MAX_LENGTH;
|
||||
private int maxConcurrentPushedStreams = 32;
|
||||
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
|
||||
private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F);
|
||||
|
||||
|
@ -336,6 +339,28 @@ public class HTTP2Client extends ContainerLifeCycle
|
|||
this.initialStreamRecvWindow = initialStreamRecvWindow;
|
||||
}
|
||||
|
||||
@ManagedAttribute("The max frame length in bytes")
|
||||
public int getMaxFrameLength()
|
||||
{
|
||||
return maxFrameLength;
|
||||
}
|
||||
|
||||
public void setMaxFrameLength(int maxFrameLength)
|
||||
{
|
||||
this.maxFrameLength = maxFrameLength;
|
||||
}
|
||||
|
||||
@ManagedAttribute("The max number of concurrent pushed streams")
|
||||
public int getMaxConcurrentPushedStreams()
|
||||
{
|
||||
return maxConcurrentPushedStreams;
|
||||
}
|
||||
|
||||
public void setMaxConcurrentPushedStreams(int maxConcurrentPushedStreams)
|
||||
{
|
||||
this.maxConcurrentPushedStreams = maxConcurrentPushedStreams;
|
||||
}
|
||||
|
||||
@ManagedAttribute("The max number of keys in all SETTINGS frames")
|
||||
public int getMaxSettingsKeys()
|
||||
{
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
package org.eclipse.jetty.http2.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
|
@ -53,7 +52,7 @@ public class HTTP2ClientConnectionFactory implements ClientConnectionFactory
|
|||
private final Connection.Listener connectionListener = new ConnectionListener();
|
||||
|
||||
@Override
|
||||
public Connection newConnection(EndPoint endPoint, Map<String, Object> context) throws IOException
|
||||
public Connection newConnection(EndPoint endPoint, Map<String, Object> context)
|
||||
{
|
||||
HTTP2Client client = (HTTP2Client)context.get(CLIENT_CONTEXT_KEY);
|
||||
ByteBufferPool byteBufferPool = (ByteBufferPool)context.get(BYTE_BUFFER_POOL_CONTEXT_KEY);
|
||||
|
@ -66,8 +65,10 @@ public class HTTP2ClientConnectionFactory implements ClientConnectionFactory
|
|||
Generator generator = new Generator(byteBufferPool);
|
||||
FlowControlStrategy flowControl = client.getFlowControlStrategyFactory().newFlowControlStrategy();
|
||||
HTTP2ClientSession session = new HTTP2ClientSession(scheduler, endPoint, generator, listener, flowControl);
|
||||
session.setMaxRemoteStreams(client.getMaxConcurrentPushedStreams());
|
||||
|
||||
Parser parser = new Parser(byteBufferPool, session, 4096, 8192);
|
||||
parser.setMaxFrameLength(client.getMaxFrameLength());
|
||||
parser.setMaxSettingsKeys(client.getMaxSettingsKeys());
|
||||
|
||||
HTTP2ClientConnection connection = new HTTP2ClientConnection(client, byteBufferPool, executor, endPoint,
|
||||
|
@ -111,6 +112,11 @@ public class HTTP2ClientConnectionFactory implements ClientConnectionFactory
|
|||
if (settings == null)
|
||||
settings = new HashMap<>();
|
||||
settings.computeIfAbsent(SettingsFrame.INITIAL_WINDOW_SIZE, k -> client.getInitialStreamRecvWindow());
|
||||
settings.computeIfAbsent(SettingsFrame.MAX_CONCURRENT_STREAMS, k -> client.getMaxConcurrentPushedStreams());
|
||||
|
||||
Integer maxFrameLength = settings.get(SettingsFrame.MAX_FRAME_SIZE);
|
||||
if (maxFrameLength != null)
|
||||
getParser().setMaxFrameLength(maxFrameLength);
|
||||
|
||||
PrefaceFrame prefaceFrame = new PrefaceFrame();
|
||||
SettingsFrame settingsFrame = new SettingsFrame(settings, false);
|
||||
|
|
|
@ -78,6 +78,7 @@ public class HTTP2ClientSession extends HTTP2Session
|
|||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Received {}", frame);
|
||||
|
||||
// HEADERS can be received for normal and pushed responses.
|
||||
int streamId = frame.getStreamId();
|
||||
IStream stream = getStream(streamId);
|
||||
if (stream != null)
|
||||
|
@ -96,7 +97,23 @@ public class HTTP2ClientSession extends HTTP2Session
|
|||
else
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Ignoring {}, stream #{} not found", frame, streamId);
|
||||
LOG.debug("Stream #{} not found", streamId);
|
||||
if (isClientStream(streamId))
|
||||
{
|
||||
// Normal stream.
|
||||
// Headers or trailers arriving after
|
||||
// the stream has been reset are ignored.
|
||||
if (!isLocalStreamClosed(streamId))
|
||||
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_headers_frame");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pushed stream.
|
||||
// Headers or trailers arriving after
|
||||
// the stream has been reset are ignored.
|
||||
if (!isRemoteStreamClosed(streamId))
|
||||
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_headers_frame");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,9 +134,12 @@ public class HTTP2ClientSession extends HTTP2Session
|
|||
else
|
||||
{
|
||||
IStream pushStream = createRemoteStream(pushStreamId);
|
||||
pushStream.process(frame, Callback.NOOP);
|
||||
Stream.Listener listener = notifyPush(stream, pushStream, frame);
|
||||
pushStream.setListener(listener);
|
||||
if (pushStream != null)
|
||||
{
|
||||
pushStream.process(frame, Callback.NOOP);
|
||||
Stream.Listener listener = notifyPush(stream, pushStream, frame);
|
||||
pushStream.setListener(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -734,7 +734,21 @@ public abstract class FlowControlStrategyTest
|
|||
public void testClientExceedingSessionWindow() throws Exception
|
||||
{
|
||||
// On server, we don't consume the data.
|
||||
start(new ServerSessionListener.Adapter());
|
||||
start(new ServerSessionListener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
|
||||
{
|
||||
return new Stream.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onData(Stream stream, DataFrame frame, Callback callback)
|
||||
{
|
||||
// Do not succeed the callback.
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
final CountDownLatch closeLatch = new CountDownLatch(1);
|
||||
Session session = newClient(new Session.Listener.Adapter()
|
||||
|
@ -805,6 +819,19 @@ public abstract class FlowControlStrategyTest
|
|||
((ISession)session).updateRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
|
||||
return super.onPreface(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
|
||||
{
|
||||
return new Stream.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onData(Stream stream, DataFrame frame, Callback callback)
|
||||
{
|
||||
// Do not succeed the callback.
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
final CountDownLatch closeLatch = new CountDownLatch(1);
|
||||
|
|
|
@ -46,12 +46,7 @@ import org.eclipse.jetty.http2.api.server.ServerSessionListener;
|
|||
import org.eclipse.jetty.http2.frames.DataFrame;
|
||||
import org.eclipse.jetty.http2.frames.GoAwayFrame;
|
||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.http2.frames.PingFrame;
|
||||
import org.eclipse.jetty.http2.frames.PriorityFrame;
|
||||
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
|
||||
import org.eclipse.jetty.http2.frames.ResetFrame;
|
||||
import org.eclipse.jetty.http2.frames.SettingsFrame;
|
||||
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
|
||||
import org.eclipse.jetty.http2.parser.ServerParser;
|
||||
import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
|
@ -752,7 +747,7 @@ public class HTTP2Test extends AbstractTest
|
|||
@Override
|
||||
protected ServerParser newServerParser(Connector connector, ServerParser.Listener listener)
|
||||
{
|
||||
return super.newServerParser(connector, new ServerParserListenerWrapper(listener)
|
||||
return super.newServerParser(connector, new ServerParser.Listener.Wrapper(listener)
|
||||
{
|
||||
@Override
|
||||
public void onGoAway(GoAwayFrame frame)
|
||||
|
@ -806,80 +801,4 @@ public class HTTP2Test extends AbstractTest
|
|||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServerParserListenerWrapper implements ServerParser.Listener
|
||||
{
|
||||
private final ServerParser.Listener listener;
|
||||
|
||||
private ServerParserListenerWrapper(ServerParser.Listener listener)
|
||||
{
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreface()
|
||||
{
|
||||
listener.onPreface();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onData(DataFrame frame)
|
||||
{
|
||||
listener.onData(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeaders(HeadersFrame frame)
|
||||
{
|
||||
listener.onHeaders(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPriority(PriorityFrame frame)
|
||||
{
|
||||
listener.onPriority(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReset(ResetFrame frame)
|
||||
{
|
||||
listener.onReset(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettings(SettingsFrame frame)
|
||||
{
|
||||
listener.onSettings(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPushPromise(PushPromiseFrame frame)
|
||||
{
|
||||
listener.onPushPromise(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPing(PingFrame frame)
|
||||
{
|
||||
listener.onPing(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGoAway(GoAwayFrame frame)
|
||||
{
|
||||
listener.onGoAway(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowUpdate(WindowUpdateFrame frame)
|
||||
{
|
||||
listener.onWindowUpdate(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionFailure(int error, String reason)
|
||||
{
|
||||
listener.onConnectionFailure(error, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.http2.client;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.BinaryOperator;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http2.ErrorCode;
|
||||
import org.eclipse.jetty.http2.HTTP2Session;
|
||||
import org.eclipse.jetty.http2.api.Session;
|
||||
import org.eclipse.jetty.http2.api.Stream;
|
||||
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
|
||||
import org.eclipse.jetty.http2.frames.DataFrame;
|
||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
|
||||
import org.eclipse.jetty.http2.frames.ResetFrame;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.FuturePromise;
|
||||
import org.eclipse.jetty.util.Promise;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class MaxPushedStreamsTest extends AbstractTest
|
||||
{
|
||||
@Test
|
||||
public void testMaxPushedStreams() throws Exception
|
||||
{
|
||||
int maxPushed = 2;
|
||||
|
||||
CountDownLatch resetLatch = new CountDownLatch(1);
|
||||
start(new ServerSessionListener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
|
||||
{
|
||||
// Trick the server into thinking it can push unlimited streams.
|
||||
((HTTP2Session)stream.getSession()).setMaxLocalStreams(-1);
|
||||
|
||||
BiFunction<List<Stream>, Stream, List<Stream>> add = (l, s) -> { l.add(s); return l; };
|
||||
BinaryOperator<List<Stream>> addAll = (l1, l2) -> { l1.addAll(l2); return l1; };
|
||||
CompletableFuture<List<Stream>> result = CompletableFuture.completedFuture(new ArrayList<>());
|
||||
// Push maxPushed resources...
|
||||
IntStream.range(0, maxPushed)
|
||||
.mapToObj(i -> new PushPromiseFrame(stream.getId(), 0, newRequest("GET", "/push_" + i, new HttpFields())))
|
||||
.map(pushFrame ->
|
||||
{
|
||||
Promise.Completable<Stream> promise = new Promise.Completable<>();
|
||||
stream.push(pushFrame, promise, new Stream.Listener.Adapter());
|
||||
return promise;
|
||||
})
|
||||
// ... wait for the pushed streams...
|
||||
.reduce(result, (cfList, cfStream) -> cfList.thenCombine(cfStream, add),
|
||||
(cfList1, cfList2) -> cfList1.thenCombine(cfList2, addAll))
|
||||
// ... then push one extra stream, the client must reject it...
|
||||
.thenApply(streams ->
|
||||
{
|
||||
PushPromiseFrame extraPushFrame = new PushPromiseFrame(stream.getId(), 0, newRequest("GET", "/push_extra", new HttpFields()));
|
||||
FuturePromise<Stream> extraPromise = new FuturePromise<>();
|
||||
stream.push(extraPushFrame, extraPromise, new Stream.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onReset(Stream stream, ResetFrame frame)
|
||||
{
|
||||
Assert.assertEquals(ErrorCode.REFUSED_STREAM_ERROR.code, frame.getError());
|
||||
resetLatch.countDown();
|
||||
}
|
||||
});
|
||||
return streams;
|
||||
})
|
||||
// ... then send the data for the valid pushed streams...
|
||||
.thenAccept(streams -> streams.forEach(pushedStream ->
|
||||
{
|
||||
DataFrame data = new DataFrame(pushedStream.getId(), BufferUtil.EMPTY_BUFFER, true);
|
||||
pushedStream.data(data, Callback.NOOP);
|
||||
}))
|
||||
// ... then send the response.
|
||||
.thenRun(() ->
|
||||
{
|
||||
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
|
||||
stream.headers(new HeadersFrame(stream.getId(), response, null, true), Callback.NOOP);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
});
|
||||
client.setMaxConcurrentPushedStreams(maxPushed);
|
||||
|
||||
Session session = newClient(new Session.Listener.Adapter());
|
||||
MetaData.Request request = newRequest("GET", new HttpFields());
|
||||
CountDownLatch responseLatch = new CountDownLatch(1);
|
||||
session.newStream(new HeadersFrame(request, null, true), new Promise.Adapter<>(), new Stream.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onHeaders(Stream stream, HeadersFrame frame)
|
||||
{
|
||||
if (frame.isEndStream())
|
||||
responseLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
Assert.assertTrue(resetLatch.await(5, TimeUnit.SECONDS));
|
||||
Assert.assertTrue(responseLatch.await(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
|
@ -26,13 +26,13 @@ import java.util.List;
|
|||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
|
@ -42,11 +42,13 @@ import org.eclipse.jetty.http2.api.server.ServerSessionListener;
|
|||
import org.eclipse.jetty.http2.frames.DataFrame;
|
||||
import org.eclipse.jetty.http2.frames.Frame;
|
||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.http2.frames.ResetFrame;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.FuturePromise;
|
||||
import org.eclipse.jetty.util.Promise;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
@ -106,7 +108,7 @@ public class TrailersTest extends AbstractTest
|
|||
start(new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
|
||||
{
|
||||
Request jettyRequest = (Request)request;
|
||||
// No trailers yet.
|
||||
|
@ -238,7 +240,7 @@ public class TrailersTest extends AbstractTest
|
|||
start(new EmptyHttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
|
||||
{
|
||||
Request jettyRequest = (Request)request;
|
||||
Response jettyResponse = jettyRequest.getResponse();
|
||||
|
@ -287,4 +289,63 @@ public class TrailersTest extends AbstractTest
|
|||
Assert.assertTrue(trailers.isEndStream());
|
||||
Assert.assertThat(trailers.getMetaData().getFields().get(trailerName), Matchers.equalTo(trailerValue));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestTrailerInvalidHpack() throws Exception
|
||||
{
|
||||
CountDownLatch serverLatch = new CountDownLatch(1);
|
||||
start(new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
|
||||
{
|
||||
try
|
||||
{
|
||||
// Read the content to read the trailers
|
||||
ServletInputStream input = request.getInputStream();
|
||||
while (true)
|
||||
{
|
||||
int read = input.read();
|
||||
if (read < 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
serverLatch.countDown();
|
||||
throw x;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CountDownLatch clientLatch = new CountDownLatch(1);
|
||||
Session session = newClient(new Session.Listener.Adapter());
|
||||
MetaData.Request request = newRequest("POST", new HttpFields());
|
||||
HeadersFrame requestFrame = new HeadersFrame(request, null, false);
|
||||
FuturePromise<Stream> promise = new FuturePromise<>();
|
||||
session.newStream(requestFrame, promise, new Stream.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onReset(Stream stream, ResetFrame frame)
|
||||
{
|
||||
clientLatch.countDown();
|
||||
}
|
||||
});
|
||||
Stream stream = promise.get(5, TimeUnit.SECONDS);
|
||||
ByteBuffer data = ByteBuffer.wrap(StringUtil.getUtf8Bytes("hello"));
|
||||
Callback.Completable completable = new Callback.Completable();
|
||||
stream.data(new DataFrame(stream.getId(), data, false), completable);
|
||||
completable.thenRun(() ->
|
||||
{
|
||||
// Invalid trailer: cannot contain pseudo headers.
|
||||
HttpFields trailerFields = new HttpFields();
|
||||
trailerFields.put(HttpHeader.C_METHOD, "GET");
|
||||
MetaData trailer = new MetaData(HttpVersion.HTTP_2, trailerFields);
|
||||
HeadersFrame trailerFrame = new HeadersFrame(stream.getId(), trailer, null, true);
|
||||
stream.headers(trailerFrame, Callback.NOOP);
|
||||
});
|
||||
|
||||
Assert.assertTrue(serverLatch.await(5, TimeUnit.SECONDS));
|
||||
Assert.assertTrue(clientLatch.await(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
package org.eclipse.jetty.http2;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
@ -96,6 +97,19 @@ public enum ErrorCode
|
|||
return Codes.codes.get(error);
|
||||
}
|
||||
|
||||
public static String toString(int error, String dft)
|
||||
{
|
||||
ErrorCode errorCode = from(error);
|
||||
String result;
|
||||
if (errorCode != null)
|
||||
result = errorCode.name().toLowerCase(Locale.ENGLISH);
|
||||
else if (dft == null)
|
||||
result = String.valueOf(error);
|
||||
else
|
||||
result = dft;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static class Codes
|
||||
{
|
||||
private static final Map<Integer, ErrorCode> codes = new HashMap<>();
|
||||
|
|
|
@ -27,14 +27,6 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.eclipse.jetty.http2.frames.DataFrame;
|
||||
import org.eclipse.jetty.http2.frames.GoAwayFrame;
|
||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.http2.frames.PingFrame;
|
||||
import org.eclipse.jetty.http2.frames.PriorityFrame;
|
||||
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
|
||||
import org.eclipse.jetty.http2.frames.ResetFrame;
|
||||
import org.eclipse.jetty.http2.frames.SettingsFrame;
|
||||
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
|
||||
import org.eclipse.jetty.http2.parser.Parser;
|
||||
import org.eclipse.jetty.io.AbstractConnection;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
|
@ -220,6 +212,7 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
|
|||
private final Callback fillableCallback = new FillableCallback();
|
||||
private NetworkBuffer buffer;
|
||||
private boolean shutdown;
|
||||
private boolean failed;
|
||||
|
||||
private void setInputBuffer(ByteBuffer byteBuffer)
|
||||
{
|
||||
|
@ -237,7 +230,7 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
|
|||
if (task != null)
|
||||
return task;
|
||||
|
||||
if (isFillInterested() || shutdown)
|
||||
if (isFillInterested() || shutdown || failed)
|
||||
return null;
|
||||
|
||||
if (buffer == null)
|
||||
|
@ -248,11 +241,22 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
|
|||
if (parse)
|
||||
{
|
||||
buffer.retain();
|
||||
|
||||
while (buffer.hasRemaining())
|
||||
parser.parse(buffer.buffer);
|
||||
|
||||
boolean released = buffer.tryRelease();
|
||||
boolean released;
|
||||
try
|
||||
{
|
||||
while (buffer.hasRemaining())
|
||||
{
|
||||
parser.parse(buffer.buffer);
|
||||
if (failed)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
released = buffer.release();
|
||||
if (failed && released)
|
||||
releaseNetworkBuffer();
|
||||
}
|
||||
|
||||
task = pollTask();
|
||||
if (LOG.isDebugEnabled())
|
||||
|
@ -272,6 +276,9 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
|
|||
}
|
||||
}
|
||||
|
||||
// Here we know that this.buffer is not retained:
|
||||
// either it has been released, or it's a new one.
|
||||
|
||||
int filled = fill(getEndPoint(), buffer.buffer);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Filled {} bytes in {}", filled, buffer);
|
||||
|
@ -307,14 +314,10 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
|
|||
|
||||
private void releaseNetworkBuffer()
|
||||
{
|
||||
if (!buffer.hasRemaining())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Released {}", buffer);
|
||||
buffer.release();
|
||||
byteBufferPool.release(buffer.buffer);
|
||||
buffer = null;
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Released {}", buffer);
|
||||
buffer.recycle();
|
||||
buffer = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -345,13 +348,11 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
|
|||
}
|
||||
}
|
||||
|
||||
private class ParserListener implements Parser.Listener
|
||||
private class ParserListener extends Parser.Listener.Wrapper
|
||||
{
|
||||
private final Parser.Listener listener;
|
||||
|
||||
private ParserListener(Parser.Listener listener)
|
||||
{
|
||||
this.listener = listener;
|
||||
super(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -363,58 +364,11 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
|
|||
session.onData(frame, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeaders(HeadersFrame frame)
|
||||
{
|
||||
listener.onHeaders(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPriority(PriorityFrame frame)
|
||||
{
|
||||
listener.onPriority(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReset(ResetFrame frame)
|
||||
{
|
||||
listener.onReset(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettings(SettingsFrame frame)
|
||||
{
|
||||
listener.onSettings(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPushPromise(PushPromiseFrame frame)
|
||||
{
|
||||
listener.onPushPromise(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPing(PingFrame frame)
|
||||
{
|
||||
listener.onPing(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGoAway(GoAwayFrame frame)
|
||||
{
|
||||
listener.onGoAway(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowUpdate(WindowUpdateFrame frame)
|
||||
{
|
||||
listener.onWindowUpdate(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionFailure(int error, String reason)
|
||||
{
|
||||
listener.onConnectionFailure(error, reason);
|
||||
producer.failed = true;
|
||||
super.onConnectionFailure(error, reason);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -444,16 +398,31 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
|
|||
refCount.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void succeeded()
|
||||
private boolean release()
|
||||
{
|
||||
release();
|
||||
return refCount.decrementAndGet() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable x)
|
||||
public void succeeded()
|
||||
{
|
||||
release();
|
||||
if (release())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Released retained {}", this);
|
||||
recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable failure)
|
||||
{
|
||||
if (release())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Released retained " + this, failure);
|
||||
recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -462,19 +431,9 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
|
|||
return InvocationType.NON_BLOCKING;
|
||||
}
|
||||
|
||||
private void release()
|
||||
private void recycle()
|
||||
{
|
||||
if (tryRelease())
|
||||
{
|
||||
byteBufferPool.release(buffer);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Released retained {}", this);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tryRelease()
|
||||
{
|
||||
return refCount.decrementAndGet() == 0;
|
||||
byteBufferPool.release(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.eclipse.jetty.http2.api.Session;
|
|||
import org.eclipse.jetty.http2.api.Stream;
|
||||
import org.eclipse.jetty.http2.frames.DataFrame;
|
||||
import org.eclipse.jetty.http2.frames.DisconnectFrame;
|
||||
import org.eclipse.jetty.http2.frames.FailureFrame;
|
||||
import org.eclipse.jetty.http2.frames.Frame;
|
||||
import org.eclipse.jetty.http2.frames.FrameType;
|
||||
import org.eclipse.jetty.http2.frames.GoAwayFrame;
|
||||
|
@ -72,8 +73,8 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
private static final Logger LOG = Log.getLogger(HTTP2Session.class);
|
||||
|
||||
private final ConcurrentMap<Integer, IStream> streams = new ConcurrentHashMap<>();
|
||||
private final AtomicInteger streamIds = new AtomicInteger();
|
||||
private final AtomicInteger lastStreamId = new AtomicInteger();
|
||||
private final AtomicInteger localStreamIds = new AtomicInteger();
|
||||
private final AtomicInteger lastRemoteStreamId = new AtomicInteger();
|
||||
private final AtomicInteger localStreamCount = new AtomicInteger();
|
||||
private final AtomicBiInteger remoteStreamCount = new AtomicBiInteger();
|
||||
private final AtomicInteger sendWindow = new AtomicInteger();
|
||||
|
@ -105,7 +106,8 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
this.flusher = new HTTP2Flusher(this);
|
||||
this.maxLocalStreams = -1;
|
||||
this.maxRemoteStreams = -1;
|
||||
this.streamIds.set(initialStreamId);
|
||||
this.localStreamIds.set(initialStreamId);
|
||||
this.lastRemoteStreamId.set(isClientStream(initialStreamId) ? 0 : -1);
|
||||
this.streamIdleTimeout = endPoint.getIdleTimeout();
|
||||
this.sendWindow.set(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
|
||||
this.recvWindow.set(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
|
||||
|
@ -229,35 +231,46 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
LOG.debug("Received {}", frame);
|
||||
|
||||
int streamId = frame.getStreamId();
|
||||
final IStream stream = getStream(streamId);
|
||||
IStream stream = getStream(streamId);
|
||||
|
||||
// SPEC: the session window must be updated even if the stream is null.
|
||||
// The flow control length includes the padding bytes.
|
||||
final int flowControlLength = frame.remaining() + frame.padding();
|
||||
int flowControlLength = frame.remaining() + frame.padding();
|
||||
flowControl.onDataReceived(this, stream, flowControlLength);
|
||||
|
||||
if (stream != null)
|
||||
{
|
||||
if (getRecvWindow() < 0)
|
||||
{
|
||||
close(ErrorCode.FLOW_CONTROL_ERROR.code, "session_window_exceeded", callback);
|
||||
}
|
||||
onConnectionFailure(ErrorCode.FLOW_CONTROL_ERROR.code, "session_window_exceeded", callback);
|
||||
else
|
||||
{
|
||||
stream.process(frame, new DataCallback(callback, stream, flowControlLength));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Ignoring {}, stream #{} not found", frame, streamId);
|
||||
LOG.debug("Stream #{} not found", streamId);
|
||||
// We must enlarge the session flow control window,
|
||||
// otherwise other requests will be stalled.
|
||||
flowControl.onDataConsumed(this, null, flowControlLength);
|
||||
callback.succeeded();
|
||||
boolean local = (streamId & 1) == (localStreamIds.get() & 1);
|
||||
boolean closed = local ? isLocalStreamClosed(streamId) : isRemoteStreamClosed(streamId);
|
||||
if (closed)
|
||||
reset(new ResetFrame(streamId, ErrorCode.STREAM_CLOSED_ERROR.code), callback);
|
||||
else
|
||||
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_data_frame", callback);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isLocalStreamClosed(int streamId)
|
||||
{
|
||||
return streamId <= localStreamIds.get();
|
||||
}
|
||||
|
||||
protected boolean isRemoteStreamClosed(int streamId)
|
||||
{
|
||||
return streamId <= getLastRemoteStreamId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract void onHeaders(HeadersFrame frame);
|
||||
|
||||
|
@ -274,11 +287,19 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Received {}", frame);
|
||||
|
||||
IStream stream = getStream(frame.getStreamId());
|
||||
int streamId = frame.getStreamId();
|
||||
IStream stream = getStream(streamId);
|
||||
if (stream != null)
|
||||
stream.process(frame, new ResetCallback());
|
||||
{
|
||||
stream.process(frame, new OnResetCallback());
|
||||
}
|
||||
else
|
||||
notifyReset(this, frame);
|
||||
{
|
||||
if (isRemoteStreamClosed(streamId))
|
||||
notifyReset(this, frame);
|
||||
else
|
||||
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_rst_stream_frame");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -306,54 +327,42 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
case SettingsFrame.HEADER_TABLE_SIZE:
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Update HPACK header table size to {} for {}", value, this);
|
||||
LOG.debug("Updating HPACK header table size to {} for {}", value, this);
|
||||
generator.setHeaderTableSize(value);
|
||||
break;
|
||||
}
|
||||
case SettingsFrame.ENABLE_PUSH:
|
||||
{
|
||||
// SPEC: check the value is sane.
|
||||
if (value != 0 && value != 1)
|
||||
{
|
||||
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_enable_push");
|
||||
return;
|
||||
}
|
||||
pushEnabled = value == 1;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("{} push for {}", pushEnabled ? "Enable" : "Disable", this);
|
||||
LOG.debug("{} push for {}", pushEnabled ? "Enabling" : "Disabling", this);
|
||||
pushEnabled = value == 1;
|
||||
break;
|
||||
}
|
||||
case SettingsFrame.MAX_CONCURRENT_STREAMS:
|
||||
{
|
||||
maxLocalStreams = value;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Update max local concurrent streams to {} for {}", maxLocalStreams, this);
|
||||
LOG.debug("Updating max local concurrent streams to {} for {}", maxLocalStreams, this);
|
||||
maxLocalStreams = value;
|
||||
break;
|
||||
}
|
||||
case SettingsFrame.INITIAL_WINDOW_SIZE:
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Update initial window size to {} for {}", value, this);
|
||||
LOG.debug("Updating initial window size to {} for {}", value, this);
|
||||
flowControl.updateInitialStreamWindow(this, value, false);
|
||||
break;
|
||||
}
|
||||
case SettingsFrame.MAX_FRAME_SIZE:
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Update max frame size to {} for {}", value, this);
|
||||
// SPEC: check the max frame size is sane.
|
||||
if (value < Frame.DEFAULT_MAX_LENGTH || value > Frame.MAX_MAX_LENGTH)
|
||||
{
|
||||
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_max_frame_size");
|
||||
return;
|
||||
}
|
||||
LOG.debug("Updating max frame size to {} for {}", value, this);
|
||||
generator.setMaxFrameSize(value);
|
||||
break;
|
||||
}
|
||||
case SettingsFrame.MAX_HEADER_LIST_SIZE:
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Update max header list size to {} for {}", value, this);
|
||||
LOG.debug("Updating max header list size to {} for {}", value, this);
|
||||
generator.setMaxHeaderListSize(value);
|
||||
break;
|
||||
}
|
||||
|
@ -448,25 +457,86 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
LOG.debug("Received {}", frame);
|
||||
|
||||
int streamId = frame.getStreamId();
|
||||
int windowDelta = frame.getWindowDelta();
|
||||
if (streamId > 0)
|
||||
{
|
||||
IStream stream = getStream(streamId);
|
||||
if (stream != null)
|
||||
if (windowDelta == 0)
|
||||
{
|
||||
stream.process(frame, Callback.NOOP);
|
||||
onWindowUpdate(stream, frame);
|
||||
reset(new ResetFrame(streamId, ErrorCode.PROTOCOL_ERROR.code), Callback.NOOP);
|
||||
}
|
||||
else
|
||||
{
|
||||
IStream stream = getStream(streamId);
|
||||
if (stream != null)
|
||||
{
|
||||
int streamSendWindow = stream.updateSendWindow(0);
|
||||
if (sumOverflows(streamSendWindow, windowDelta))
|
||||
{
|
||||
reset(new ResetFrame(streamId, ErrorCode.FLOW_CONTROL_ERROR.code), Callback.NOOP);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream.process(frame, Callback.NOOP);
|
||||
onWindowUpdate(stream, frame);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isRemoteStreamClosed(streamId))
|
||||
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_window_update_frame");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
onWindowUpdate(null, frame);
|
||||
if (windowDelta == 0)
|
||||
{
|
||||
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_window_update_frame");
|
||||
}
|
||||
else
|
||||
{
|
||||
int sessionSendWindow = updateSendWindow(0);
|
||||
if (sumOverflows(sessionSendWindow, windowDelta))
|
||||
onConnectionFailure(ErrorCode.FLOW_CONTROL_ERROR.code, "invalid_flow_control_window");
|
||||
else
|
||||
onWindowUpdate(null, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamFailure(int streamId, int error, String reason)
|
||||
{
|
||||
Callback callback = new ResetCallback(streamId, error, Callback.NOOP);
|
||||
IStream stream = getStream(streamId);
|
||||
if (stream != null)
|
||||
stream.process(new FailureFrame(error, reason), callback);
|
||||
else
|
||||
callback.succeeded();
|
||||
}
|
||||
|
||||
private boolean sumOverflows(int a, int b)
|
||||
{
|
||||
try
|
||||
{
|
||||
Math.addExact(a, b);
|
||||
return false;
|
||||
}
|
||||
catch (ArithmeticException x)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionFailure(int error, String reason)
|
||||
{
|
||||
notifyFailure(this, new IOException(String.format("%d/%s", error, reason)), new CloseCallback(error, reason));
|
||||
onConnectionFailure(error, reason, Callback.NOOP);
|
||||
}
|
||||
|
||||
protected void onConnectionFailure(int error, String reason, Callback callback)
|
||||
{
|
||||
notifyFailure(this, new IOException(String.format("%d/%s", error, reason)), new CloseCallback(error, reason, callback));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -482,7 +552,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
int streamId = frame.getStreamId();
|
||||
if (streamId <= 0)
|
||||
{
|
||||
streamId = streamIds.getAndAdd(2);
|
||||
streamId = localStreamIds.getAndAdd(2);
|
||||
PriorityFrame priority = frame.getPriority();
|
||||
priority = priority == null ? null : new PriorityFrame(streamId, priority.getParentStreamId(),
|
||||
priority.getWeight(), priority.isExclusive());
|
||||
|
@ -511,7 +581,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
IStream stream = streams.get(streamId);
|
||||
if (stream == null)
|
||||
{
|
||||
streamId = streamIds.getAndAdd(2);
|
||||
streamId = localStreamIds.getAndAdd(2);
|
||||
frame = new PriorityFrame(streamId, frame.getParentStreamId(),
|
||||
frame.getWeight(), frame.isExclusive());
|
||||
}
|
||||
|
@ -529,7 +599,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
boolean queued;
|
||||
synchronized (this)
|
||||
{
|
||||
int streamId = streamIds.getAndAdd(2);
|
||||
int streamId = localStreamIds.getAndAdd(2);
|
||||
frame = new PushPromiseFrame(frame.getStreamId(), streamId, frame.getMetaData());
|
||||
|
||||
IStream pushStream = createLocalStream(streamId);
|
||||
|
@ -629,7 +699,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
reason = reason.substring(0, Math.min(reason.length(), 32));
|
||||
payload = reason.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
return new GoAwayFrame(closeState, lastStreamId.get(), error, payload);
|
||||
return new GoAwayFrame(closeState, getLastRemoteStreamId(), error, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -736,7 +806,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
// SPEC: duplicate stream is treated as connection error.
|
||||
if (streams.putIfAbsent(streamId, stream) == null)
|
||||
{
|
||||
updateLastStreamId(streamId);
|
||||
updateLastRemoteStreamId(streamId);
|
||||
stream.setIdleTimeout(getStreamIdleTimeout());
|
||||
flowControl.onStreamCreated(stream);
|
||||
if (LOG.isDebugEnabled())
|
||||
|
@ -745,7 +815,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
}
|
||||
else
|
||||
{
|
||||
close(ErrorCode.PROTOCOL_ERROR.code, "duplicate_stream", Callback.NOOP);
|
||||
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "duplicate_stream");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -1014,9 +1084,14 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
return !endPoint.isOpen();
|
||||
}
|
||||
|
||||
private void updateLastStreamId(int streamId)
|
||||
protected int getLastRemoteStreamId()
|
||||
{
|
||||
Atomics.updateMax(lastStreamId, streamId);
|
||||
return lastRemoteStreamId.get();
|
||||
}
|
||||
|
||||
private void updateLastRemoteStreamId(int streamId)
|
||||
{
|
||||
Atomics.updateMax(lastRemoteStreamId, streamId);
|
||||
}
|
||||
|
||||
protected Stream.Listener notifyNewStream(Stream stream, HeadersFrame frame)
|
||||
|
@ -1120,6 +1195,12 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
}
|
||||
}
|
||||
|
||||
protected static boolean isClientStream(int streamId)
|
||||
{
|
||||
// Client-initiated stream ids are odd.
|
||||
return (streamId & 1) == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(Appendable out, String indent) throws IOException
|
||||
{
|
||||
|
@ -1448,7 +1529,37 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
}
|
||||
}
|
||||
|
||||
private class ResetCallback implements Callback
|
||||
private class ResetCallback extends Callback.Nested
|
||||
{
|
||||
private final int streamId;
|
||||
private final int error;
|
||||
|
||||
private ResetCallback(int streamId, int error, Callback callback)
|
||||
{
|
||||
super(callback);
|
||||
this.streamId = streamId;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void succeeded()
|
||||
{
|
||||
complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable x)
|
||||
{
|
||||
complete();
|
||||
}
|
||||
|
||||
private void complete()
|
||||
{
|
||||
reset(new ResetFrame(streamId, error), getCallback());
|
||||
}
|
||||
}
|
||||
|
||||
private class OnResetCallback implements Callback
|
||||
{
|
||||
@Override
|
||||
public void succeeded()
|
||||
|
@ -1474,13 +1585,14 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
}
|
||||
}
|
||||
|
||||
private class CloseCallback implements Callback
|
||||
private class CloseCallback extends Callback.Nested
|
||||
{
|
||||
private final int error;
|
||||
private final String reason;
|
||||
|
||||
private CloseCallback(int error, String reason)
|
||||
private CloseCallback(int error, String reason, Callback callback)
|
||||
{
|
||||
super(callback);
|
||||
this.error = error;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
@ -1497,15 +1609,9 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvocationType getInvocationType()
|
||||
{
|
||||
return InvocationType.NON_BLOCKING;
|
||||
}
|
||||
|
||||
private void complete()
|
||||
{
|
||||
close(error, reason, Callback.NOOP);
|
||||
close(error, reason, getCallback());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,8 +28,12 @@ import java.util.concurrent.TimeoutException;
|
|||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http2.api.Stream;
|
||||
import org.eclipse.jetty.http2.frames.DataFrame;
|
||||
import org.eclipse.jetty.http2.frames.FailureFrame;
|
||||
import org.eclipse.jetty.http2.frames.Frame;
|
||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
|
||||
|
@ -58,9 +62,10 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
|
|||
private final ISession session;
|
||||
private final int streamId;
|
||||
private final boolean local;
|
||||
private volatile Listener listener;
|
||||
private volatile boolean localReset;
|
||||
private volatile boolean remoteReset;
|
||||
private boolean localReset;
|
||||
private Listener listener;
|
||||
private boolean remoteReset;
|
||||
private long dataLength;
|
||||
|
||||
public HTTP2Stream(Scheduler scheduler, ISession session, int streamId, boolean local)
|
||||
{
|
||||
|
@ -68,6 +73,7 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
|
|||
this.session = session;
|
||||
this.streamId = streamId;
|
||||
this.local = local;
|
||||
this.dataLength = Long.MIN_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -255,6 +261,11 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
|
|||
onWindowUpdate((WindowUpdateFrame)frame, callback);
|
||||
break;
|
||||
}
|
||||
case FAILURE:
|
||||
{
|
||||
onFailure((FailureFrame)frame, callback);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
|
@ -266,6 +277,15 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
|
|||
{
|
||||
if (updateClose(frame.isEndStream(), CloseState.Event.RECEIVED))
|
||||
session.removeStream(this);
|
||||
MetaData metaData = frame.getMetaData();
|
||||
if (metaData.isRequest() || metaData.isResponse())
|
||||
{
|
||||
HttpFields fields = metaData.getFields();
|
||||
long length = -1;
|
||||
if (fields != null)
|
||||
length = fields.getLongField(HttpHeader.CONTENT_LENGTH.asString());
|
||||
dataLength = length >= 0 ? length : Long.MIN_VALUE;
|
||||
}
|
||||
callback.succeeded();
|
||||
}
|
||||
|
||||
|
@ -295,8 +315,20 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
|
|||
return;
|
||||
}
|
||||
|
||||
if (dataLength != Long.MIN_VALUE)
|
||||
{
|
||||
dataLength -= frame.remaining();
|
||||
if (frame.isEndStream() && dataLength != 0)
|
||||
{
|
||||
reset(new ResetFrame(streamId, ErrorCode.PROTOCOL_ERROR.code), Callback.NOOP);
|
||||
callback.failed(new IOException("invalid_data_length"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateClose(frame.isEndStream(), CloseState.Event.RECEIVED))
|
||||
session.removeStream(this);
|
||||
|
||||
notifyData(this, frame, callback);
|
||||
}
|
||||
|
||||
|
@ -321,6 +353,11 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
|
|||
callback.succeeded();
|
||||
}
|
||||
|
||||
private void onFailure(FailureFrame frame, Callback callback)
|
||||
{
|
||||
notifyFailure(this, frame, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateClose(boolean update, CloseState.Event event)
|
||||
{
|
||||
|
@ -498,31 +535,43 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
|
|||
|
||||
private void notifyData(Stream stream, DataFrame frame, Callback callback)
|
||||
{
|
||||
final Listener listener = this.listener;
|
||||
if (listener == null)
|
||||
return;
|
||||
try
|
||||
Listener listener = this.listener;
|
||||
if (listener != null)
|
||||
{
|
||||
listener.onData(stream, frame, callback);
|
||||
try
|
||||
{
|
||||
listener.onData(stream, frame, callback);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
LOG.info("Failure while notifying listener " + listener, x);
|
||||
callback.failed(x);
|
||||
}
|
||||
}
|
||||
catch (Throwable x)
|
||||
else
|
||||
{
|
||||
LOG.info("Failure while notifying listener " + listener, x);
|
||||
callback.succeeded();
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyReset(Stream stream, ResetFrame frame, Callback callback)
|
||||
{
|
||||
final Listener listener = this.listener;
|
||||
if (listener == null)
|
||||
return;
|
||||
try
|
||||
Listener listener = this.listener;
|
||||
if (listener != null)
|
||||
{
|
||||
listener.onReset(stream, frame, callback);
|
||||
try
|
||||
{
|
||||
listener.onReset(stream, frame, callback);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
LOG.info("Failure while notifying listener " + listener, x);
|
||||
callback.failed(x);
|
||||
}
|
||||
}
|
||||
catch (Throwable x)
|
||||
else
|
||||
{
|
||||
LOG.info("Failure while notifying listener " + listener, x);
|
||||
callback.succeeded();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -542,6 +591,27 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
|
|||
}
|
||||
}
|
||||
|
||||
private void notifyFailure(Stream stream, FailureFrame frame, Callback callback)
|
||||
{
|
||||
Listener listener = this.listener;
|
||||
if (listener != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
listener.onFailure(stream, frame.getError(), frame.getReason(), callback);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
LOG.info("Failure while notifying listener " + listener, x);
|
||||
callback.failed(x);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
callback.succeeded();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String dump()
|
||||
{
|
||||
|
|
|
@ -197,6 +197,11 @@ public interface Stream
|
|||
*/
|
||||
public boolean onIdleTimeout(Stream stream, Throwable x);
|
||||
|
||||
public default void onFailure(Stream stream, int error, String reason, Callback callback)
|
||||
{
|
||||
callback.succeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Empty implementation of {@link Listener}</p>
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.http2.frames;
|
||||
|
||||
public class FailureFrame extends Frame
|
||||
{
|
||||
private final int error;
|
||||
private final String reason;
|
||||
|
||||
public FailureFrame(int error, String reason)
|
||||
{
|
||||
super(FrameType.FAILURE);
|
||||
this.error = error;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public int getError()
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
public String getReason()
|
||||
{
|
||||
return reason;
|
||||
}
|
||||
}
|
|
@ -35,7 +35,8 @@ public enum FrameType
|
|||
CONTINUATION(9),
|
||||
// Synthetic frames only needed by the implementation.
|
||||
PREFACE(10),
|
||||
DISCONNECT(11);
|
||||
DISCONNECT(11),
|
||||
FAILURE(12);
|
||||
|
||||
public static FrameType from(int type)
|
||||
{
|
||||
|
|
|
@ -76,11 +76,10 @@ public class GoAwayFrame extends Frame
|
|||
@Override
|
||||
public String toString()
|
||||
{
|
||||
ErrorCode errorCode = ErrorCode.from(error);
|
||||
return String.format("%s,%d/%s/%s/%s",
|
||||
super.toString(),
|
||||
lastStreamId,
|
||||
errorCode != null ? errorCode.toString() : String.valueOf(error),
|
||||
ErrorCode.toString(error, null),
|
||||
tryConvertPayload(),
|
||||
closeState);
|
||||
}
|
||||
|
|
|
@ -83,6 +83,7 @@ public class HeadersFrame extends Frame
|
|||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s#%d{end=%b}", super.toString(), streamId, endStream);
|
||||
return String.format("%s#%d{end=%b}%s", super.toString(), streamId, endStream,
|
||||
priority == null ? "" : String.format("+%s", priority));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,6 @@
|
|||
|
||||
package org.eclipse.jetty.http2.frames;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.eclipse.jetty.http2.ErrorCode;
|
||||
|
||||
public class ResetFrame extends Frame
|
||||
|
@ -49,8 +47,6 @@ public class ResetFrame extends Frame
|
|||
@Override
|
||||
public String toString()
|
||||
{
|
||||
ErrorCode errorCode = ErrorCode.from(error);
|
||||
String reason = errorCode == null ? "error=" + error : errorCode.name().toLowerCase(Locale.ENGLISH);
|
||||
return String.format("%s#%d{%s}", super.toString(), streamId, reason);
|
||||
return String.format("%s#%d{%s}", super.toString(), streamId, ErrorCode.toString(error, null));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ public class GoAwayGenerator extends FrameGenerator
|
|||
public int generateGoAway(ByteBufferPool.Lease lease, int lastStreamId, int error, byte[] payload)
|
||||
{
|
||||
if (lastStreamId < 0)
|
||||
throw new IllegalArgumentException("Invalid last stream id: " + lastStreamId);
|
||||
lastStreamId = 0;
|
||||
|
||||
// The last streamId + the error code.
|
||||
int fixedLength = 4 + 4;
|
||||
|
|
|
@ -222,4 +222,21 @@ public abstract class BodyParser
|
|||
LOG.info("Failure while notifying listener " + listener, x);
|
||||
}
|
||||
}
|
||||
|
||||
protected void streamFailure(int streamId, int error, String reason)
|
||||
{
|
||||
notifyStreamFailure(streamId, error, reason);
|
||||
}
|
||||
|
||||
private void notifyStreamFailure(int streamId, int error, String reason)
|
||||
{
|
||||
try
|
||||
{
|
||||
listener.onStreamFailure(streamId, error, reason);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
LOG.info("Failure while notifying listener " + listener, x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ public class ContinuationBodyParser extends BodyParser
|
|||
headerBlockFragments.storeFragment(buffer, length, last);
|
||||
reset();
|
||||
if (last)
|
||||
onHeaders();
|
||||
return onHeaders();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -94,12 +94,17 @@ public class ContinuationBodyParser extends BodyParser
|
|||
return false;
|
||||
}
|
||||
|
||||
private void onHeaders()
|
||||
private boolean onHeaders()
|
||||
{
|
||||
ByteBuffer headerBlock = headerBlockFragments.complete();
|
||||
MetaData metaData = headerBlockParser.parse(headerBlock, headerBlock.remaining());
|
||||
if (metaData == HeaderBlockParser.SESSION_FAILURE)
|
||||
return false;
|
||||
if (metaData == null || metaData == HeaderBlockParser.STREAM_FAILURE)
|
||||
return true;
|
||||
HeadersFrame frame = new HeadersFrame(getStreamId(), metaData, headerBlockFragments.getPriorityFrame(), headerBlockFragments.isEndStream());
|
||||
notifyHeaders(frame);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void reset()
|
||||
|
|
|
@ -20,23 +20,46 @@ package org.eclipse.jetty.http2.parser;
|
|||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http2.ErrorCode;
|
||||
import org.eclipse.jetty.http2.hpack.HpackDecoder;
|
||||
import org.eclipse.jetty.http2.hpack.HpackException;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
public class HeaderBlockParser
|
||||
{
|
||||
public static final MetaData STREAM_FAILURE = new MetaData(HttpVersion.HTTP_2, null);
|
||||
public static final MetaData SESSION_FAILURE = new MetaData(HttpVersion.HTTP_2, null);
|
||||
private static final Logger LOG = Log.getLogger(HeaderBlockParser.class);
|
||||
|
||||
private final HeaderParser headerParser;
|
||||
private final ByteBufferPool byteBufferPool;
|
||||
private final HpackDecoder hpackDecoder;
|
||||
private final BodyParser notifier;
|
||||
private ByteBuffer blockBuffer;
|
||||
|
||||
public HeaderBlockParser(ByteBufferPool byteBufferPool, HpackDecoder hpackDecoder)
|
||||
public HeaderBlockParser(HeaderParser headerParser, ByteBufferPool byteBufferPool, HpackDecoder hpackDecoder, BodyParser notifier)
|
||||
{
|
||||
this.headerParser = headerParser;
|
||||
this.byteBufferPool = byteBufferPool;
|
||||
this.hpackDecoder = hpackDecoder;
|
||||
this.notifier = notifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses @{code blockLength} HPACK bytes from the given {@code buffer}.
|
||||
*
|
||||
* @param buffer the buffer to parse
|
||||
* @param blockLength the length of the HPACK block
|
||||
* @return null, if the buffer contains less than {@code blockLength} bytes;
|
||||
* {@link #STREAM_FAILURE} if parsing the HPACK block produced a stream failure;
|
||||
* {@link #SESSION_FAILURE} if parsing the HPACK block produced a session failure;
|
||||
* a valid MetaData object if the parsing was successful.
|
||||
*/
|
||||
public MetaData parse(ByteBuffer buffer, int blockLength)
|
||||
{
|
||||
// We must wait for the all the bytes of the header block to arrive.
|
||||
|
@ -72,17 +95,41 @@ public class HeaderBlockParser
|
|||
toDecode = buffer;
|
||||
}
|
||||
|
||||
MetaData result = hpackDecoder.decode(toDecode);
|
||||
|
||||
buffer.limit(limit);
|
||||
|
||||
if (blockBuffer != null)
|
||||
try
|
||||
{
|
||||
byteBufferPool.release(blockBuffer);
|
||||
blockBuffer = null;
|
||||
return hpackDecoder.decode(toDecode);
|
||||
}
|
||||
catch (HpackException.StreamException x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug(x);
|
||||
notifier.streamFailure(headerParser.getStreamId(), ErrorCode.PROTOCOL_ERROR.code, "invalid_hpack_block");
|
||||
return STREAM_FAILURE;
|
||||
}
|
||||
catch (HpackException.CompressionException x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug(x);
|
||||
notifier.connectionFailure(buffer, ErrorCode.COMPRESSION_ERROR.code, "invalid_hpack_block");
|
||||
return SESSION_FAILURE;
|
||||
}
|
||||
catch (HpackException.SessionException x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug(x);
|
||||
notifier.connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_hpack_block");
|
||||
return SESSION_FAILURE;
|
||||
}
|
||||
finally
|
||||
{
|
||||
buffer.limit(limit);
|
||||
|
||||
return result;
|
||||
if (blockBuffer != null)
|
||||
{
|
||||
byteBufferPool.release(blockBuffer);
|
||||
blockBuffer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ package org.eclipse.jetty.http2.parser;
|
|||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.eclipse.jetty.http2.frames.Frame;
|
||||
import org.eclipse.jetty.http2.frames.FrameType;
|
||||
|
||||
/**
|
||||
* <p>The parser for the frame header of HTTP/2 frames.</p>
|
||||
|
@ -144,6 +145,12 @@ public class HeaderParser
|
|||
return streamId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("[%s|%d|%d|%d]", FrameType.from(getFrameType()), getLength(), flags, getStreamId());
|
||||
}
|
||||
|
||||
private enum State
|
||||
{
|
||||
LENGTH, TYPE, FLAGS, STREAM_ID, STREAM_ID_BYTES
|
||||
|
|
|
@ -92,17 +92,11 @@ public class HeadersBodyParser extends BodyParser
|
|||
length = getBodyLength();
|
||||
|
||||
if (isPadding())
|
||||
{
|
||||
state = State.PADDING_LENGTH;
|
||||
}
|
||||
else if (hasFlag(Flags.PRIORITY))
|
||||
{
|
||||
state = State.EXCLUSIVE;
|
||||
}
|
||||
else
|
||||
{
|
||||
state = State.HEADERS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PADDING_LENGTH:
|
||||
|
@ -162,6 +156,9 @@ public class HeadersBodyParser extends BodyParser
|
|||
}
|
||||
case WEIGHT:
|
||||
{
|
||||
// SPEC: stream cannot depend on itself.
|
||||
if (getStreamId() == parentStreamId)
|
||||
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_priority_frame");
|
||||
weight = (buffer.get() & 0xFF) + 1;
|
||||
--length;
|
||||
state = State.HEADERS;
|
||||
|
@ -173,13 +170,16 @@ public class HeadersBodyParser extends BodyParser
|
|||
if (hasFlag(Flags.END_HEADERS))
|
||||
{
|
||||
MetaData metaData = headerBlockParser.parse(buffer, length);
|
||||
if (metaData == HeaderBlockParser.SESSION_FAILURE)
|
||||
return false;
|
||||
if (metaData != null)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Parsed {} frame hpack from {}", FrameType.HEADERS, buffer);
|
||||
state = State.PADDING;
|
||||
loop = paddingLength == 0;
|
||||
onHeaders(parentStreamId, weight, exclusive, metaData);
|
||||
if (metaData != HeaderBlockParser.STREAM_FAILURE)
|
||||
onHeaders(parentStreamId, weight, exclusive, metaData);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
@ -24,6 +24,7 @@ import java.util.function.UnaryOperator;
|
|||
import org.eclipse.jetty.http2.ErrorCode;
|
||||
import org.eclipse.jetty.http2.Flags;
|
||||
import org.eclipse.jetty.http2.frames.DataFrame;
|
||||
import org.eclipse.jetty.http2.frames.Frame;
|
||||
import org.eclipse.jetty.http2.frames.FrameType;
|
||||
import org.eclipse.jetty.http2.frames.GoAwayFrame;
|
||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||
|
@ -35,7 +36,6 @@ import org.eclipse.jetty.http2.frames.SettingsFrame;
|
|||
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
|
||||
import org.eclipse.jetty.http2.hpack.HpackDecoder;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
|
@ -52,6 +52,8 @@ public class Parser
|
|||
private final HeaderParser headerParser;
|
||||
private final HeaderBlockParser headerBlockParser;
|
||||
private final BodyParser[] bodyParsers;
|
||||
private final UnknownBodyParser unknownBodyParser;
|
||||
private int maxFrameLength;
|
||||
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
|
||||
private boolean continuation;
|
||||
private State state = State.HEADER;
|
||||
|
@ -60,7 +62,9 @@ public class Parser
|
|||
{
|
||||
this.listener = listener;
|
||||
this.headerParser = new HeaderParser();
|
||||
this.headerBlockParser = new HeaderBlockParser(byteBufferPool, new HpackDecoder(maxDynamicTableSize, maxHeaderSize));
|
||||
this.unknownBodyParser = new UnknownBodyParser(headerParser, listener);
|
||||
this.headerBlockParser = new HeaderBlockParser(headerParser, byteBufferPool, new HpackDecoder(maxDynamicTableSize, maxHeaderSize), unknownBodyParser);
|
||||
this.maxFrameLength = Frame.DEFAULT_MAX_LENGTH;
|
||||
this.bodyParsers = new BodyParser[FrameType.values().length];
|
||||
}
|
||||
|
||||
|
@ -128,8 +132,7 @@ public class Parser
|
|||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug(x);
|
||||
BufferUtil.clear(buffer);
|
||||
notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "parser_error");
|
||||
connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR, "parser_error");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,31 +141,27 @@ public class Parser
|
|||
if (!headerParser.parse(buffer))
|
||||
return false;
|
||||
|
||||
FrameType frameType = FrameType.from(getFrameType());
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Parsed {} frame header from {}", frameType, buffer);
|
||||
LOG.debug("Parsed {} frame header from {}", headerParser, buffer);
|
||||
|
||||
if (headerParser.getLength() > getMaxFrameLength())
|
||||
return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR, "invalid_frame_length");
|
||||
|
||||
FrameType frameType = FrameType.from(getFrameType());
|
||||
if (continuation)
|
||||
{
|
||||
// SPEC: CONTINUATION frames must be consecutive.
|
||||
if (frameType != FrameType.CONTINUATION)
|
||||
{
|
||||
// SPEC: CONTINUATION frames must be consecutive.
|
||||
BufferUtil.clear(buffer);
|
||||
notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "continuation_frame_expected");
|
||||
return false;
|
||||
}
|
||||
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR, "expected_continuation_frame");
|
||||
if (headerParser.hasFlag(Flags.END_HEADERS))
|
||||
{
|
||||
continuation = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (frameType == FrameType.HEADERS &&
|
||||
!headerParser.hasFlag(Flags.END_HEADERS))
|
||||
{
|
||||
continuation = true;
|
||||
}
|
||||
if (frameType == FrameType.HEADERS)
|
||||
continuation = !headerParser.hasFlag(Flags.END_HEADERS);
|
||||
else if (frameType == FrameType.CONTINUATION)
|
||||
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR, "unexpected_continuation_frame");
|
||||
}
|
||||
state = State.BODY;
|
||||
return true;
|
||||
|
@ -173,9 +172,13 @@ public class Parser
|
|||
int type = getFrameType();
|
||||
if (type < 0 || type >= bodyParsers.length)
|
||||
{
|
||||
BufferUtil.clear(buffer);
|
||||
notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unknown_frame_type_" + type);
|
||||
// Unknown frame types must be ignored.
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Ignoring unknown frame type {}", Integer.toHexString(type));
|
||||
if (!unknownBodyParser.parse(buffer))
|
||||
return false;
|
||||
reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
BodyParser bodyParser = bodyParsers[type];
|
||||
|
@ -194,6 +197,11 @@ public class Parser
|
|||
return true;
|
||||
}
|
||||
|
||||
private boolean connectionFailure(ByteBuffer buffer, ErrorCode error, String reason)
|
||||
{
|
||||
return unknownBodyParser.connectionFailure(buffer, error.code, reason);
|
||||
}
|
||||
|
||||
protected int getFrameType()
|
||||
{
|
||||
return headerParser.getFrameType();
|
||||
|
@ -204,6 +212,16 @@ public class Parser
|
|||
return headerParser.hasFlag(bit);
|
||||
}
|
||||
|
||||
public int getMaxFrameLength()
|
||||
{
|
||||
return maxFrameLength;
|
||||
}
|
||||
|
||||
public void setMaxFrameLength(int maxFrameLength)
|
||||
{
|
||||
this.maxFrameLength = maxFrameLength;
|
||||
}
|
||||
|
||||
public int getMaxSettingsKeys()
|
||||
{
|
||||
return maxSettingsKeys;
|
||||
|
@ -246,6 +264,8 @@ public class Parser
|
|||
|
||||
public void onWindowUpdate(WindowUpdateFrame frame);
|
||||
|
||||
public void onStreamFailure(int streamId, int error, String reason);
|
||||
|
||||
public void onConnectionFailure(int error, String reason);
|
||||
|
||||
public static class Adapter implements Listener
|
||||
|
@ -295,12 +315,98 @@ public class Parser
|
|||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamFailure(int streamId, int error, String reason)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionFailure(int error, String reason)
|
||||
{
|
||||
LOG.warn("Connection failure: {}/{}", error, reason);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Wrapper implements Listener
|
||||
{
|
||||
private final Parser.Listener listener;
|
||||
|
||||
public Wrapper(Parser.Listener listener)
|
||||
{
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public Listener getParserListener()
|
||||
{
|
||||
return listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onData(DataFrame frame)
|
||||
{
|
||||
listener.onData(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeaders(HeadersFrame frame)
|
||||
{
|
||||
listener.onHeaders(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPriority(PriorityFrame frame)
|
||||
{
|
||||
listener.onPriority(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReset(ResetFrame frame)
|
||||
{
|
||||
listener.onReset(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettings(SettingsFrame frame)
|
||||
{
|
||||
listener.onSettings(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPushPromise(PushPromiseFrame frame)
|
||||
{
|
||||
listener.onPushPromise(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPing(PingFrame frame)
|
||||
{
|
||||
listener.onPing(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGoAway(GoAwayFrame frame)
|
||||
{
|
||||
listener.onGoAway(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowUpdate(WindowUpdateFrame frame)
|
||||
{
|
||||
listener.onWindowUpdate(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamFailure(int streamId, int error, String reason)
|
||||
{
|
||||
listener.onStreamFailure(streamId, error, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionFailure(int error, String reason)
|
||||
{
|
||||
listener.onConnectionFailure(error, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum State
|
||||
|
|
|
@ -102,7 +102,6 @@ public class PriorityBodyParser extends BodyParser
|
|||
// SPEC: stream cannot depend on itself.
|
||||
if (getStreamId() == parentStreamId)
|
||||
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_priority_frame");
|
||||
|
||||
int weight = (buffer.get() & 0xFF) + 1;
|
||||
return onPriority(parentStreamId, weight, exclusive);
|
||||
}
|
||||
|
|
|
@ -125,11 +125,14 @@ public class PushPromiseBodyParser extends BodyParser
|
|||
case HEADERS:
|
||||
{
|
||||
MetaData metaData = headerBlockParser.parse(buffer, length);
|
||||
if (metaData == HeaderBlockParser.SESSION_FAILURE)
|
||||
return false;
|
||||
if (metaData != null)
|
||||
{
|
||||
state = State.PADDING;
|
||||
loop = paddingLength == 0;
|
||||
onPushPromise(streamId, metaData);
|
||||
if (metaData != HeaderBlockParser.STREAM_FAILURE)
|
||||
onPushPromise(streamId, metaData);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -158,6 +158,26 @@ public class ServerParser extends Parser
|
|||
{
|
||||
}
|
||||
}
|
||||
|
||||
public static class Wrapper extends Parser.Listener.Wrapper implements Listener
|
||||
{
|
||||
public Wrapper(ServerParser.Listener listener)
|
||||
{
|
||||
super(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerParser.Listener getParserListener()
|
||||
{
|
||||
return (Listener)super.getParserListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreface()
|
||||
{
|
||||
getParserListener().onPreface();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum State
|
||||
|
|
|
@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||
|
||||
import org.eclipse.jetty.http2.ErrorCode;
|
||||
import org.eclipse.jetty.http2.Flags;
|
||||
import org.eclipse.jetty.http2.frames.Frame;
|
||||
import org.eclipse.jetty.http2.frames.SettingsFrame;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
@ -71,7 +72,7 @@ public class SettingsBodyParser extends BodyParser
|
|||
@Override
|
||||
protected void emptyBody(ByteBuffer buffer)
|
||||
{
|
||||
onSettings(new HashMap<>());
|
||||
onSettings(buffer, new HashMap<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -135,7 +136,7 @@ public class SettingsBodyParser extends BodyParser
|
|||
state = State.SETTING_ID;
|
||||
length -= 4;
|
||||
if (length == 0)
|
||||
return onSettings(settings);
|
||||
return onSettings(buffer, settings);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -161,7 +162,7 @@ public class SettingsBodyParser extends BodyParser
|
|||
return false;
|
||||
state = State.SETTING_ID;
|
||||
if (length == 0)
|
||||
return onSettings(settings);
|
||||
return onSettings(buffer, settings);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -183,8 +184,21 @@ public class SettingsBodyParser extends BodyParser
|
|||
return true;
|
||||
}
|
||||
|
||||
protected boolean onSettings(Map<Integer, Integer> settings)
|
||||
protected boolean onSettings(ByteBuffer buffer, Map<Integer, Integer> settings)
|
||||
{
|
||||
Integer enablePush = settings.get(SettingsFrame.ENABLE_PUSH);
|
||||
if (enablePush != null && enablePush != 0 && enablePush != 1)
|
||||
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_enable_push");
|
||||
|
||||
Integer initialWindowSize = settings.get(SettingsFrame.INITIAL_WINDOW_SIZE);
|
||||
// Values greater than Integer.MAX_VALUE will overflow to negative.
|
||||
if (initialWindowSize != null && initialWindowSize < 0)
|
||||
return connectionFailure(buffer, ErrorCode.FLOW_CONTROL_ERROR.code, "invalid_settings_initial_window_size");
|
||||
|
||||
Integer maxFrameLength = settings.get(SettingsFrame.MAX_FRAME_SIZE);
|
||||
if (maxFrameLength != null && (maxFrameLength < Frame.DEFAULT_MAX_LENGTH || maxFrameLength > Frame.MAX_MAX_LENGTH))
|
||||
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_max_frame_size");
|
||||
|
||||
SettingsFrame frame = new SettingsFrame(settings, hasFlag(Flags.ACK));
|
||||
reset();
|
||||
notifySettings(frame);
|
||||
|
@ -210,7 +224,7 @@ public class SettingsBodyParser extends BodyParser
|
|||
}
|
||||
|
||||
@Override
|
||||
protected boolean onSettings(Map<Integer, Integer> settings)
|
||||
protected boolean onSettings(ByteBuffer buffer, Map<Integer, Integer> settings)
|
||||
{
|
||||
frameRef.set(new SettingsFrame(settings, false));
|
||||
return true;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.http2.parser;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class UnknownBodyParser extends BodyParser
|
||||
{
|
||||
private int cursor;
|
||||
|
||||
public UnknownBodyParser(HeaderParser headerParser, Parser.Listener listener)
|
||||
{
|
||||
super(headerParser, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean parse(ByteBuffer buffer)
|
||||
{
|
||||
int length = cursor == 0 ? getBodyLength() : cursor;
|
||||
cursor = consume(buffer, length);
|
||||
return cursor == 0;
|
||||
}
|
||||
|
||||
private int consume(ByteBuffer buffer, int length)
|
||||
{
|
||||
int remaining = buffer.remaining();
|
||||
if (remaining >= length)
|
||||
{
|
||||
buffer.position(buffer.position() + length);
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.position(buffer.limit());
|
||||
return length - remaining;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.http2.frames;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import org.eclipse.jetty.http2.ErrorCode;
|
||||
import org.eclipse.jetty.http2.parser.Parser;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.MappedByteBufferPool;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class MaxFrameSizeParseTest
|
||||
{
|
||||
private final ByteBufferPool byteBufferPool = new MappedByteBufferPool();
|
||||
|
||||
@Test
|
||||
public void testMaxFrameSize()
|
||||
{
|
||||
int maxFrameLength = Frame.DEFAULT_MAX_LENGTH + 16;
|
||||
|
||||
AtomicInteger failure = new AtomicInteger();
|
||||
Parser parser = new Parser(byteBufferPool, new Parser.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onConnectionFailure(int error, String reason)
|
||||
{
|
||||
failure.set(error);
|
||||
}
|
||||
}, 4096, 8192);
|
||||
parser.setMaxFrameLength(maxFrameLength);
|
||||
parser.init(UnaryOperator.identity());
|
||||
|
||||
// Iterate a few times to be sure the parser is properly reset.
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
byte[] bytes = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0};
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bytes);
|
||||
buffer.putInt(0, maxFrameLength + 1);
|
||||
buffer.position(1);
|
||||
while (buffer.hasRemaining())
|
||||
parser.parse(buffer);
|
||||
}
|
||||
|
||||
Assert.assertEquals(ErrorCode.FRAME_SIZE_ERROR.code, failure.get());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.http2.frames;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import org.eclipse.jetty.http2.parser.Parser;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.MappedByteBufferPool;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class UnknownParseTest
|
||||
{
|
||||
private final ByteBufferPool byteBufferPool = new MappedByteBufferPool();
|
||||
|
||||
@Test
|
||||
public void testParse()
|
||||
{
|
||||
testParse(Function.identity());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseOneByteAtATime()
|
||||
{
|
||||
testParse(buffer -> ByteBuffer.wrap(new byte[]{buffer.get()}));
|
||||
}
|
||||
|
||||
private void testParse(Function<ByteBuffer, ByteBuffer> fn)
|
||||
{
|
||||
AtomicBoolean failure = new AtomicBoolean();
|
||||
Parser parser = new Parser(byteBufferPool, new Parser.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onConnectionFailure(int error, String reason)
|
||||
{
|
||||
failure.set(true);
|
||||
}
|
||||
}, 4096, 8192);
|
||||
parser.init(UnaryOperator.identity());
|
||||
|
||||
// Iterate a few times to be sure the parser is properly reset.
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
byte[] bytes = new byte[]{0, 0, 4, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bytes);
|
||||
while (buffer.hasRemaining())
|
||||
parser.parse(fn.apply(buffer));
|
||||
}
|
||||
|
||||
Assert.assertFalse(failure.get());
|
||||
}
|
||||
}
|
|
@ -21,10 +21,8 @@ package org.eclipse.jetty.http2.hpack;
|
|||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.eclipse.jetty.http.BadMessageException;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http2.hpack.HpackContext.Entry;
|
||||
import org.eclipse.jetty.util.TypeUtil;
|
||||
|
@ -66,14 +64,16 @@ public class HpackDecoder
|
|||
_localMaxDynamicTableSize=localMaxdynamciTableSize;
|
||||
}
|
||||
|
||||
public MetaData decode(ByteBuffer buffer)
|
||||
public MetaData decode(ByteBuffer buffer) throws HpackException.SessionException, HpackException.StreamException
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug(String.format("CtxTbl[%x] decoding %d octets",_context.hashCode(),buffer.remaining()));
|
||||
|
||||
// If the buffer is big, don't even think about decoding it
|
||||
if (buffer.remaining()>_builder.getMaxSize())
|
||||
throw new BadMessageException(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431,"Header frame size "+buffer.remaining()+">"+_builder.getMaxSize());
|
||||
throw new HpackException.SessionException("431 Request Header Fields too large");
|
||||
|
||||
boolean emitted = false;
|
||||
|
||||
while(buffer.hasRemaining())
|
||||
{
|
||||
|
@ -92,14 +92,14 @@ public class HpackDecoder
|
|||
int index = NBitInteger.decode(buffer,7);
|
||||
Entry entry=_context.get(index);
|
||||
if (entry==null)
|
||||
{
|
||||
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Unknown index "+index);
|
||||
}
|
||||
else if (entry.isStatic())
|
||||
throw new HpackException.SessionException("Unknown index %d",index);
|
||||
|
||||
if (entry.isStatic())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("decode IdxStatic {}",entry);
|
||||
// emit field
|
||||
emitted = true;
|
||||
_builder.emit(entry.getHttpField());
|
||||
|
||||
// TODO copy and add to reference set if there is room
|
||||
|
@ -110,6 +110,7 @@ public class HpackDecoder
|
|||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("decode Idx {}",entry);
|
||||
// emit
|
||||
emitted = true;
|
||||
_builder.emit(entry.getHttpField());
|
||||
}
|
||||
}
|
||||
|
@ -134,6 +135,8 @@ public class HpackDecoder
|
|||
LOG.debug("decode resize="+size);
|
||||
if (size>_localMaxDynamicTableSize)
|
||||
throw new IllegalArgumentException();
|
||||
if (emitted)
|
||||
throw new HpackException.CompressionException("Dynamic table resize after fields");
|
||||
_context.resize(size);
|
||||
continue;
|
||||
|
||||
|
@ -178,7 +181,8 @@ public class HpackDecoder
|
|||
char c=name.charAt(i);
|
||||
if (c>='A'&&c<='Z')
|
||||
{
|
||||
throw new BadMessageException(400,"Uppercase header name");
|
||||
_builder.streamException("Uppercase header name %s",name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
header=HttpHeader.CACHE.get(name);
|
||||
|
@ -240,6 +244,7 @@ public class HpackDecoder
|
|||
}
|
||||
|
||||
// emit the field
|
||||
emitted = true;
|
||||
_builder.emit(field);
|
||||
|
||||
// if indexed add to dynamic table
|
||||
|
|
|
@ -21,8 +21,11 @@ package org.eclipse.jetty.http2.hpack;
|
|||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpScheme;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
|
@ -31,6 +34,9 @@ import org.eclipse.jetty.http.MetaData;
|
|||
import org.eclipse.jetty.http.PreEncodedHttpField;
|
||||
import org.eclipse.jetty.http2.hpack.HpackContext.Entry;
|
||||
import org.eclipse.jetty.http2.hpack.HpackContext.StaticEntry;
|
||||
import org.eclipse.jetty.util.ArrayTrie;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.Trie;
|
||||
import org.eclipse.jetty.util.TypeUtil;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
@ -38,17 +44,13 @@ import org.eclipse.jetty.util.log.Logger;
|
|||
public class HpackEncoder
|
||||
{
|
||||
public static final Logger LOG = Log.getLogger(HpackEncoder.class);
|
||||
|
||||
private final static HttpField[] __status= new HttpField[599];
|
||||
|
||||
|
||||
final static EnumSet<HttpHeader> __DO_NOT_HUFFMAN =
|
||||
EnumSet.of(
|
||||
HttpHeader.AUTHORIZATION,
|
||||
HttpHeader.CONTENT_MD5,
|
||||
HttpHeader.PROXY_AUTHENTICATE,
|
||||
HttpHeader.PROXY_AUTHORIZATION);
|
||||
|
||||
final static EnumSet<HttpHeader> __DO_NOT_INDEX =
|
||||
EnumSet.of(
|
||||
// HttpHeader.C_PATH, // TODO more data needed
|
||||
|
@ -69,18 +71,21 @@ public class HpackEncoder
|
|||
HttpHeader.LAST_MODIFIED,
|
||||
HttpHeader.SET_COOKIE,
|
||||
HttpHeader.SET_COOKIE2);
|
||||
|
||||
|
||||
final static EnumSet<HttpHeader> __NEVER_INDEX =
|
||||
EnumSet.of(
|
||||
HttpHeader.AUTHORIZATION,
|
||||
HttpHeader.SET_COOKIE,
|
||||
HttpHeader.SET_COOKIE2);
|
||||
private static final PreEncodedHttpField CONNECTION_TE = new PreEncodedHttpField(HttpHeader.CONNECTION, "te");
|
||||
private static final PreEncodedHttpField TE_TRAILERS = new PreEncodedHttpField(HttpHeader.TE, "trailers");
|
||||
private static final Trie<Boolean> specialHopHeaders = new ArrayTrie<>(6);
|
||||
|
||||
static
|
||||
{
|
||||
for (HttpStatus.Code code : HttpStatus.Code.values())
|
||||
__status[code.getCode()]=new PreEncodedHttpField(HttpHeader.C_STATUS,Integer.toString(code.getCode()));
|
||||
specialHopHeaders.put("close", true);
|
||||
specialHopHeaders.put("te", true);
|
||||
}
|
||||
|
||||
private final HpackContext _context;
|
||||
|
@ -174,9 +179,30 @@ public class HpackEncoder
|
|||
encode(buffer,status);
|
||||
}
|
||||
|
||||
// Add all the other fields
|
||||
for (HttpField field : metadata)
|
||||
encode(buffer,field);
|
||||
// Add all non-connection fields.
|
||||
HttpFields fields = metadata.getFields();
|
||||
if (fields != null)
|
||||
{
|
||||
Set<String> hopHeaders = fields.getCSV(HttpHeader.CONNECTION, false).stream()
|
||||
.filter(v -> specialHopHeaders.get(v) == Boolean.TRUE)
|
||||
.map(StringUtil::asciiToLowerCase)
|
||||
.collect(Collectors.toSet());
|
||||
for (HttpField field : fields)
|
||||
{
|
||||
if (field.getHeader() == HttpHeader.CONNECTION)
|
||||
continue;
|
||||
if (!hopHeaders.isEmpty() && hopHeaders.contains(StringUtil.asciiToLowerCase(field.getName())))
|
||||
continue;
|
||||
if (field.getHeader() == HttpHeader.TE)
|
||||
{
|
||||
if (!field.contains("trailers"))
|
||||
continue;
|
||||
encode(buffer, CONNECTION_TE);
|
||||
encode(buffer, TE_TRAILERS);
|
||||
}
|
||||
encode(buffer,field);
|
||||
}
|
||||
}
|
||||
|
||||
// Check size
|
||||
if (_maxHeaderListSize>0 && _headerListSize>_maxHeaderListSize)
|
||||
|
@ -305,7 +331,7 @@ public class HpackEncoder
|
|||
encoding="Lit"+
|
||||
((name==null)?"HuffN":("IdxN"+(name.isStatic()?"S":"")+(1+NBitInteger.octectsNeeded(4,_context.index(name)))))+
|
||||
(huffman?"HuffV":"LitV")+
|
||||
(indexed?"Idx":(never_index?"!!Idx":"!Idx"));
|
||||
(never_index?"!!Idx":"!Idx");
|
||||
}
|
||||
else if (field_size>=_context.getMaxDynamicTableSize() || header==HttpHeader.CONTENT_LENGTH && field.getValue().length()>2)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.http2.hpack;
|
||||
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public abstract class HpackException extends Exception
|
||||
{
|
||||
HpackException(String messageFormat, Object... args)
|
||||
{
|
||||
super(String.format(messageFormat, args));
|
||||
}
|
||||
|
||||
/**
|
||||
* A Stream HPACK exception.
|
||||
* <p>Stream exceptions are not fatal to the connection and the
|
||||
* hpack state is complete and able to continue handling other
|
||||
* decoding/encoding for the session.
|
||||
* </p>
|
||||
*/
|
||||
public static class StreamException extends HpackException
|
||||
{
|
||||
StreamException(String messageFormat, Object... args)
|
||||
{
|
||||
super(messageFormat,args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Session HPACK Exception.
|
||||
* <p>Session exceptions are fatal for the stream and the HPACK
|
||||
* state is unable to decode/encode further. </p>
|
||||
*/
|
||||
public static class SessionException extends HpackException
|
||||
{
|
||||
SessionException(String messageFormat, Object... args)
|
||||
{
|
||||
super(messageFormat,args);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CompressionException extends SessionException
|
||||
{
|
||||
public CompressionException(String messageFormat, Object... args)
|
||||
{
|
||||
super(messageFormat,args);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -287,6 +287,7 @@ public class Huffman
|
|||
};
|
||||
|
||||
static final int[][] LCCODES = new int[CODES.length][];
|
||||
static final char EOS = 256;
|
||||
|
||||
// Huffman decode tree stored in a flattened char array for good
|
||||
// locality of reference.
|
||||
|
@ -344,24 +345,25 @@ public class Huffman
|
|||
}
|
||||
}
|
||||
|
||||
public static String decode(ByteBuffer buffer)
|
||||
public static String decode(ByteBuffer buffer) throws HpackException.CompressionException
|
||||
{
|
||||
return decode(buffer,buffer.remaining());
|
||||
}
|
||||
|
||||
public static String decode(ByteBuffer buffer,int length)
|
||||
public static String decode(ByteBuffer buffer,int length) throws HpackException.CompressionException
|
||||
{
|
||||
StringBuilder out = new StringBuilder(length*2);
|
||||
int node = 0;
|
||||
int current = 0;
|
||||
int bits = 0;
|
||||
|
||||
|
||||
byte[] array = buffer.array();
|
||||
int position=buffer.position();
|
||||
int start=buffer.arrayOffset()+position;
|
||||
int end=start+length;
|
||||
buffer.position(position+length);
|
||||
|
||||
|
||||
|
||||
for (int i=start; i<end; i++)
|
||||
{
|
||||
int b = array[i]&0xFF;
|
||||
|
@ -373,6 +375,9 @@ public class Huffman
|
|||
node = tree[node*256+c];
|
||||
if (rowbits[node]!=0)
|
||||
{
|
||||
if(rowsym[node] == EOS)
|
||||
throw new HpackException.CompressionException("EOS in content");
|
||||
|
||||
// terminal node
|
||||
out.append(rowsym[node]);
|
||||
bits -= rowbits[node];
|
||||
|
@ -389,18 +394,30 @@ public class Huffman
|
|||
while (bits > 0)
|
||||
{
|
||||
int c = (current << (8 - bits)) & 0xFF;
|
||||
int lastNode = node;
|
||||
node = tree[node*256+c];
|
||||
if (rowbits[node]==0 || rowbits[node] > bits)
|
||||
|
||||
if (rowbits[node]==0 || rowbits[node] > bits)
|
||||
{
|
||||
int requiredPadding = 0;
|
||||
for(int i=0; i<bits; i++)
|
||||
requiredPadding = (requiredPadding << 1) | 1;
|
||||
|
||||
if((c>>(8-bits)) != requiredPadding)
|
||||
throw new HpackException.CompressionException("Incorrect padding");
|
||||
|
||||
node = lastNode;
|
||||
break;
|
||||
|
||||
if (rowbits[node]==0)
|
||||
throw new IllegalStateException();
|
||||
|
||||
}
|
||||
|
||||
out.append(rowsym[node]);
|
||||
bits -= rowbits[node];
|
||||
node = 0;
|
||||
}
|
||||
|
||||
if(node != 0)
|
||||
throw new HpackException.CompressionException("Bad termination");
|
||||
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
|
|
|
@ -20,27 +20,29 @@
|
|||
package org.eclipse.jetty.http2.hpack;
|
||||
|
||||
|
||||
import org.eclipse.jetty.http.BadMessageException;
|
||||
import org.eclipse.jetty.http.HostPortHttpField;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpScheme;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http2.hpack.HpackException.SessionException;
|
||||
|
||||
public class MetaDataBuilder
|
||||
{
|
||||
private final int _maxSize;
|
||||
private int _size;
|
||||
private int _status;
|
||||
private int _status=-1;
|
||||
private String _method;
|
||||
private HttpScheme _scheme;
|
||||
private HostPortHttpField _authority;
|
||||
private String _path;
|
||||
private long _contentLength=Long.MIN_VALUE;
|
||||
private HttpFields _fields = new HttpFields(10);
|
||||
private HpackException.StreamException _streamException;
|
||||
private boolean _request;
|
||||
private boolean _response;
|
||||
|
||||
/**
|
||||
* @param maxHeadersSize The maximum size of the headers, expressed as total name and value characters.
|
||||
|
@ -66,7 +68,7 @@ public class MetaDataBuilder
|
|||
return _size;
|
||||
}
|
||||
|
||||
public void emit(HttpField field)
|
||||
public void emit(HttpField field) throws HpackException.SessionException
|
||||
{
|
||||
HttpHeader header = field.getHeader();
|
||||
String name = field.getName();
|
||||
|
@ -74,7 +76,7 @@ public class MetaDataBuilder
|
|||
int field_size = name.length() + (value == null ? 0 : value.length());
|
||||
_size+=field_size+32;
|
||||
if (_size>_maxSize)
|
||||
throw new BadMessageException(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431,"Header size "+_size+">"+_maxSize);
|
||||
throw new HpackException.SessionException("Header Size %d > %d",_size,_maxSize);
|
||||
|
||||
if (field instanceof StaticTableHttpField)
|
||||
{
|
||||
|
@ -82,15 +84,21 @@ public class MetaDataBuilder
|
|||
switch(header)
|
||||
{
|
||||
case C_STATUS:
|
||||
_status=(Integer)staticField.getStaticValue();
|
||||
if(checkHeader(header, _status))
|
||||
_status = (Integer)staticField.getStaticValue();
|
||||
_response = true;
|
||||
break;
|
||||
|
||||
case C_METHOD:
|
||||
_method=value;
|
||||
if(checkPseudoHeader(header, _method))
|
||||
_method = value;
|
||||
_request = true;
|
||||
break;
|
||||
|
||||
case C_SCHEME:
|
||||
_scheme = (HttpScheme)staticField.getStaticValue();
|
||||
if(checkPseudoHeader(header, _scheme))
|
||||
_scheme = (HttpScheme)staticField.getStaticValue();
|
||||
_request = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -102,23 +110,32 @@ public class MetaDataBuilder
|
|||
switch(header)
|
||||
{
|
||||
case C_STATUS:
|
||||
_status=field.getIntValue();
|
||||
if(checkHeader(header, _status))
|
||||
_status = field.getIntValue();
|
||||
_response = true;
|
||||
break;
|
||||
|
||||
case C_METHOD:
|
||||
_method=value;
|
||||
if(checkPseudoHeader(header, _method))
|
||||
_method = value;
|
||||
_request = true;
|
||||
break;
|
||||
|
||||
case C_SCHEME:
|
||||
if (value != null)
|
||||
if(checkPseudoHeader(header, _scheme) && value != null)
|
||||
_scheme = HttpScheme.CACHE.get(value);
|
||||
_request = true;
|
||||
break;
|
||||
|
||||
case C_AUTHORITY:
|
||||
if (field instanceof HostPortHttpField)
|
||||
_authority = (HostPortHttpField)field;
|
||||
else if (value != null)
|
||||
_authority = new AuthorityHttpField(value);
|
||||
if(checkPseudoHeader(header, _authority))
|
||||
{
|
||||
if (field instanceof HostPortHttpField)
|
||||
_authority = (HostPortHttpField)field;
|
||||
else if (value != null)
|
||||
_authority = new AuthorityHttpField(value);
|
||||
}
|
||||
_request = true;
|
||||
break;
|
||||
|
||||
case HOST:
|
||||
|
@ -134,54 +151,129 @@ public class MetaDataBuilder
|
|||
break;
|
||||
|
||||
case C_PATH:
|
||||
_path = value;
|
||||
if(checkPseudoHeader(header, _path))
|
||||
{
|
||||
if (value!=null && value.length()>0)
|
||||
_path = value;
|
||||
else
|
||||
streamException("No Path");
|
||||
}
|
||||
_request = true;
|
||||
break;
|
||||
|
||||
case CONTENT_LENGTH:
|
||||
_contentLength = field.getLongValue();
|
||||
_fields.add(field);
|
||||
break;
|
||||
|
||||
case TE:
|
||||
if ("trailers".equalsIgnoreCase(value))
|
||||
_fields.add(field);
|
||||
else
|
||||
streamException("Unsupported TE value '%s'", value);
|
||||
break;
|
||||
|
||||
case CONNECTION:
|
||||
if ("TE".equalsIgnoreCase(value))
|
||||
_fields.add(field);
|
||||
else
|
||||
streamException("Connection specific field '%s'", header);
|
||||
break;
|
||||
|
||||
default:
|
||||
if (name.charAt(0)!=':')
|
||||
default:
|
||||
if (name.charAt(0)==':')
|
||||
streamException("Unknown pseudo header '%s'", name);
|
||||
else
|
||||
_fields.add(field);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (name.charAt(0)!=':')
|
||||
if (name.charAt(0)==':')
|
||||
streamException("Unknown pseudo header '%s'",name);
|
||||
else
|
||||
_fields.add(field);
|
||||
}
|
||||
}
|
||||
|
||||
public MetaData build()
|
||||
void streamException(String messageFormat, Object... args)
|
||||
{
|
||||
HpackException.StreamException stream = new HpackException.StreamException(messageFormat, args);
|
||||
if (_streamException==null)
|
||||
_streamException = stream;
|
||||
else
|
||||
_streamException.addSuppressed(stream);
|
||||
}
|
||||
|
||||
private boolean checkHeader(HttpHeader header, int value)
|
||||
{
|
||||
if (_fields.size()>0)
|
||||
{
|
||||
streamException("Pseudo header %s after fields", header.asString());
|
||||
return false;
|
||||
}
|
||||
if (value==-1)
|
||||
return true;
|
||||
streamException("Duplicate pseudo header %s", header.asString());
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean checkPseudoHeader(HttpHeader header, Object value)
|
||||
{
|
||||
if (_fields.size()>0)
|
||||
{
|
||||
streamException("Pseudo header %s after fields", header.asString());
|
||||
return false;
|
||||
}
|
||||
if (value==null)
|
||||
return true;
|
||||
streamException("Duplicate pseudo header %s", header.asString());
|
||||
return false;
|
||||
}
|
||||
|
||||
public MetaData build() throws HpackException.StreamException
|
||||
{
|
||||
if (_streamException!=null)
|
||||
{
|
||||
_streamException.addSuppressed(new Throwable());
|
||||
throw _streamException;
|
||||
}
|
||||
|
||||
if (_request && _response)
|
||||
throw new HpackException.StreamException("Request and Response headers");
|
||||
|
||||
|
||||
HttpFields fields = _fields;
|
||||
try
|
||||
{
|
||||
HttpFields fields = _fields;
|
||||
_fields = new HttpFields(Math.max(10,fields.size()+5));
|
||||
|
||||
if (_method!=null)
|
||||
if (_request)
|
||||
{
|
||||
if (_method==null)
|
||||
throw new HpackException.StreamException("No Method");
|
||||
if (_scheme==null)
|
||||
throw new HpackException.StreamException("No Scheme");
|
||||
if (_path==null)
|
||||
throw new HpackException.StreamException("No Path");
|
||||
return new MetaData.Request(_method,_scheme,_authority,_path,HttpVersion.HTTP_2,fields,_contentLength);
|
||||
if (_status!=0)
|
||||
}
|
||||
if (_response)
|
||||
return new MetaData.Response(HttpVersion.HTTP_2,_status,fields,_contentLength);
|
||||
if (_path!=null)
|
||||
fields.put(HttpHeader.C_PATH,_path);
|
||||
if (_authority!=null)
|
||||
fields.put(HttpHeader.HOST,_authority.getValue());
|
||||
|
||||
return new MetaData(HttpVersion.HTTP_2,fields,_contentLength);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_status=0;
|
||||
_fields = new HttpFields(Math.max(10,fields.size()+5));
|
||||
_request=false;
|
||||
_response=false;
|
||||
_status=-1;
|
||||
_method=null;
|
||||
_scheme=null;
|
||||
_authority=null;
|
||||
_path=null;
|
||||
_size=0;
|
||||
_contentLength=Long.MIN_VALUE;
|
||||
_contentLength=Long.MIN_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -189,13 +281,14 @@ public class MetaDataBuilder
|
|||
* Check that the max size will not be exceeded.
|
||||
* @param length the length
|
||||
* @param huffman the huffman name
|
||||
* @throws SessionException in case of size errors
|
||||
*/
|
||||
public void checkSize(int length, boolean huffman)
|
||||
public void checkSize(int length, boolean huffman) throws SessionException
|
||||
{
|
||||
// Apply a huffman fudge factor
|
||||
if (huffman)
|
||||
length=(length*4)/3;
|
||||
if ((_size+length)>_maxSize)
|
||||
throw new BadMessageException(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431,"Header size "+(_size+length)+">"+_maxSize);
|
||||
throw new HpackException.SessionException("Header too large %d > %d", _size+length, _maxSize);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -411,7 +411,7 @@ public class HpackContextTest
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testStaticHuffmanValues()
|
||||
public void testStaticHuffmanValues() throws Exception
|
||||
{
|
||||
HpackContext ctx = new HpackContext(4096);
|
||||
for (int i=2;i<=14;i++)
|
||||
|
|
|
@ -22,22 +22,17 @@ package org.eclipse.jetty.http2.hpack;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.eclipse.jetty.http.BadMessageException;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpScheme;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.http2.hpack.HpackException.CompressionException;
|
||||
import org.eclipse.jetty.http2.hpack.HpackException.StreamException;
|
||||
import org.eclipse.jetty.util.TypeUtil;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
@ -47,7 +42,7 @@ import static org.junit.Assert.assertTrue;
|
|||
public class HpackDecoderTest
|
||||
{
|
||||
@Test
|
||||
public void testDecodeD_3()
|
||||
public void testDecodeD_3() throws Exception
|
||||
{
|
||||
HpackDecoder decoder = new HpackDecoder(4096,8192);
|
||||
|
||||
|
@ -95,7 +90,7 @@ public class HpackDecoderTest
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeD_4()
|
||||
public void testDecodeD_4() throws Exception
|
||||
{
|
||||
HpackDecoder decoder = new HpackDecoder(4096,8192);
|
||||
|
||||
|
@ -128,7 +123,7 @@ public class HpackDecoderTest
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeWithArrayOffset()
|
||||
public void testDecodeWithArrayOffset() throws Exception
|
||||
{
|
||||
String value = "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==";
|
||||
|
||||
|
@ -152,7 +147,7 @@ public class HpackDecoderTest
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeHuffmanWithArrayOffset()
|
||||
public void testDecodeHuffmanWithArrayOffset() throws Exception
|
||||
{
|
||||
HpackDecoder decoder = new HpackDecoder(4096,8192);
|
||||
|
||||
|
@ -172,7 +167,7 @@ public class HpackDecoderTest
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testNghttpx()
|
||||
public void testNghttpx() throws Exception
|
||||
{
|
||||
// Response encoded by nghttpx
|
||||
String encoded="886196C361Be940b6a65B6850400B8A00571972e080a62D1Bf5f87497cA589D34d1f9a0f0d0234327690Aa69D29aFcA954D3A5358980Ae112e0f7c880aE152A9A74a6bF3";
|
||||
|
@ -194,30 +189,59 @@ public class HpackDecoderTest
|
|||
@Test
|
||||
public void testResize() throws Exception
|
||||
{
|
||||
String encoded = "3f6166871e33A13a47497f205f8841E92b043d492d49";
|
||||
String encoded = "203f136687A0E41d139d090760881c6490B2Cd39Ba7f";
|
||||
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
|
||||
HpackDecoder decoder = new HpackDecoder(4096, 8192);
|
||||
MetaData metaData = decoder.decode(buffer);
|
||||
assertThat(metaData.getFields().get(HttpHeader.HOST),is("aHostName"));
|
||||
assertThat(metaData.getFields().get(HttpHeader.CONTENT_TYPE),is("some/content"));
|
||||
assertThat(decoder.getHpackContext().getDynamicTableSize(),is(0));
|
||||
assertThat(metaData.getFields().get(HttpHeader.HOST),is( "localhost0"));
|
||||
assertThat(metaData.getFields().get(HttpHeader.COOKIE),is("abcdefghij"));
|
||||
assertThat(decoder.getHpackContext().getMaxDynamicTableSize(),is(50));
|
||||
assertThat(decoder.getHpackContext().size(),is(1));
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadResize() throws Exception
|
||||
{
|
||||
/*
|
||||
4. Dynamic Table Management
|
||||
4.2. Maximum Table Size
|
||||
× 1: Sends a dynamic table size update at the end of header block
|
||||
-> The endpoint MUST treat this as a decoding error.
|
||||
Expected: GOAWAY Frame (Error Code: COMPRESSION_ERROR)
|
||||
Connection closed
|
||||
*/
|
||||
|
||||
String encoded = "203f136687A0E41d139d090760881c6490B2Cd39Ba7f20";
|
||||
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
|
||||
HpackDecoder decoder = new HpackDecoder(4096, 8192);
|
||||
try
|
||||
{
|
||||
decoder.decode(buffer);
|
||||
Assert.fail();
|
||||
}
|
||||
catch(CompressionException e)
|
||||
{
|
||||
Assert.assertThat(e.getMessage(),Matchers.containsString("Dynamic table resize after fields"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTooBigToIndex()
|
||||
public void testTooBigToIndex() throws Exception
|
||||
{
|
||||
String encoded = "44FfEc02Df3990A190A0D4Ee5b3d2940Ec98Aa4a62D127D29e273a0aA20dEcAa190a503b262d8a2671D4A2672a927aA874988a2471D05510750c951139EdA2452a3a548cAa1aA90bE4B228342864A9E0D450A5474a92992a1aA513395448E3A0Aa17B96cFe3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f14E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F353F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F54f";
|
||||
String encoded = "3f610f17FfEc02Df3990A190A0D4Ee5b3d2940Ec98Aa4a62D127D29e273a0aA20dEcAa190a503b262d8a2671D4A2672a927aA874988a2471D05510750c951139EdA2452a3a548cAa1aA90bE4B228342864A9E0D450A5474a92992a1aA513395448E3A0Aa17B96cFe3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f14E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F353F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F54f";
|
||||
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
|
||||
|
||||
HpackDecoder decoder = new HpackDecoder(128,8192);
|
||||
MetaData metaData = decoder.decode(buffer);
|
||||
|
||||
assertThat(decoder.getHpackContext().getDynamicTableSize(),is(0));
|
||||
assertThat(metaData.getFields().get(HttpHeader.C_PATH),Matchers.startsWith("This is a very large field"));
|
||||
assertThat(metaData.getFields().get("host"),Matchers.startsWith("This is a very large field"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnknownIndex()
|
||||
public void testUnknownIndex() throws Exception
|
||||
{
|
||||
String encoded = "BE";
|
||||
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
|
||||
|
@ -228,11 +252,334 @@ public class HpackDecoderTest
|
|||
decoder.decode(buffer);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (BadMessageException e)
|
||||
catch (HpackException.SessionException e)
|
||||
{
|
||||
assertThat(e.getCode(),equalTo(HttpStatus.BAD_REQUEST_400));
|
||||
assertThat(e.getReason(),Matchers.startsWith("Unknown index"));
|
||||
assertThat(e.getMessage(),Matchers.startsWith("Unknown index"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* 8.1.2.1. Pseudo-Header Fields */
|
||||
@Test()
|
||||
public void test8_1_2_1_PsuedoHeaderFields() throws Exception
|
||||
{
|
||||
// 1:Sends a HEADERS frame that contains a unknown pseudo-header field
|
||||
MetaDataBuilder mdb = new MetaDataBuilder(4096);
|
||||
mdb.emit(new HttpField(":unknown","value"));
|
||||
try
|
||||
{
|
||||
mdb.build();
|
||||
Assert.fail();
|
||||
}
|
||||
catch(StreamException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(),Matchers.containsString("Unknown pseudo header"));
|
||||
}
|
||||
|
||||
// 2: Sends a HEADERS frame that contains the pseudo-header field defined for response
|
||||
mdb = new MetaDataBuilder(4096);
|
||||
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_PATH,"/path"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_STATUS,"100"));
|
||||
try
|
||||
{
|
||||
mdb.build();
|
||||
Assert.fail();
|
||||
}
|
||||
catch(StreamException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(),Matchers.containsString("Request and Response headers"));
|
||||
}
|
||||
|
||||
// 3: Sends a HEADERS frame that contains a pseudo-header field as trailers
|
||||
|
||||
// 4: Sends a HEADERS frame that contains a pseudo-header field that appears in a header block after a regular header field
|
||||
mdb = new MetaDataBuilder(4096);
|
||||
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_PATH,"/path"));
|
||||
mdb.emit(new HttpField("Accept","No Compromise"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost"));
|
||||
try
|
||||
{
|
||||
mdb.build();
|
||||
Assert.fail();
|
||||
}
|
||||
catch(StreamException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(),Matchers.containsString("Pseudo header :authority after fields"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test()
|
||||
public void test8_1_2_2_ConnectionSpecificHeaderFields() throws Exception
|
||||
{
|
||||
MetaDataBuilder mdb;
|
||||
|
||||
// 1: Sends a HEADERS frame that contains the connection-specific header field
|
||||
mdb = new MetaDataBuilder(4096);
|
||||
mdb.emit(new HttpField(HttpHeader.CONNECTION,"value"));
|
||||
try
|
||||
{
|
||||
mdb.build();
|
||||
Assert.fail();
|
||||
}
|
||||
catch(StreamException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(),Matchers.containsString("Connection specific field 'Connection'"));
|
||||
}
|
||||
|
||||
// 2: Sends a HEADERS frame that contains the TE header field with any value other than "trailers"
|
||||
mdb = new MetaDataBuilder(4096);
|
||||
mdb.emit(new HttpField(HttpHeader.TE,"not_trailers"));
|
||||
try
|
||||
{
|
||||
mdb.build();
|
||||
Assert.fail();
|
||||
}
|
||||
catch(StreamException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(),Matchers.containsString("Unsupported TE value 'not_trailers'"));
|
||||
}
|
||||
|
||||
|
||||
mdb = new MetaDataBuilder(4096);
|
||||
mdb.emit(new HttpField(HttpHeader.CONNECTION,"TE"));
|
||||
mdb.emit(new HttpField(HttpHeader.TE,"trailers"));
|
||||
Assert.assertNotNull(mdb.build());
|
||||
}
|
||||
|
||||
|
||||
@Test()
|
||||
public void test8_1_2_3_RequestPseudoHeaderFields() throws Exception
|
||||
{
|
||||
MetaDataBuilder mdb;
|
||||
|
||||
mdb = new MetaDataBuilder(4096);
|
||||
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost:8080"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_PATH,"/"));
|
||||
Assert.assertThat(mdb.build(),Matchers.instanceOf(MetaData.Request.class));
|
||||
|
||||
|
||||
// 1: Sends a HEADERS frame with empty ":path" pseudo-header field
|
||||
mdb = new MetaDataBuilder(4096);
|
||||
mdb = new MetaDataBuilder(4096);
|
||||
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost:8080"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_PATH,""));
|
||||
try
|
||||
{
|
||||
mdb.build();
|
||||
Assert.fail();
|
||||
}
|
||||
catch(StreamException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(),Matchers.containsString("No Path"));
|
||||
}
|
||||
|
||||
// 2: Sends a HEADERS frame that omits ":method" pseudo-header field
|
||||
mdb = new MetaDataBuilder(4096);
|
||||
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost:8080"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_PATH,"/"));
|
||||
try
|
||||
{
|
||||
mdb.build();
|
||||
Assert.fail();
|
||||
}
|
||||
catch(StreamException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(),Matchers.containsString("No Method"));
|
||||
}
|
||||
|
||||
|
||||
// 3: Sends a HEADERS frame that omits ":scheme" pseudo-header field
|
||||
mdb = new MetaDataBuilder(4096);
|
||||
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost:8080"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_PATH,"/"));
|
||||
try
|
||||
{
|
||||
mdb.build();
|
||||
Assert.fail();
|
||||
}
|
||||
catch(StreamException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(),Matchers.containsString("No Scheme"));
|
||||
}
|
||||
|
||||
// 4: Sends a HEADERS frame that omits ":path" pseudo-header field
|
||||
mdb = new MetaDataBuilder(4096);
|
||||
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost:8080"));
|
||||
try
|
||||
{
|
||||
mdb.build();
|
||||
Assert.fail();
|
||||
}
|
||||
catch(StreamException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(),Matchers.containsString("No Path"));
|
||||
}
|
||||
|
||||
// 5: Sends a HEADERS frame with duplicated ":method" pseudo-header field
|
||||
mdb = new MetaDataBuilder(4096);
|
||||
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost:8080"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_PATH,"/"));
|
||||
try
|
||||
{
|
||||
mdb.build();
|
||||
Assert.fail();
|
||||
}
|
||||
catch(StreamException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(),Matchers.containsString("Duplicate"));
|
||||
}
|
||||
|
||||
// 6: Sends a HEADERS frame with duplicated ":scheme" pseudo-header field
|
||||
mdb = new MetaDataBuilder(4096);
|
||||
mdb.emit(new HttpField(HttpHeader.C_METHOD,"GET"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_SCHEME,"http"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY,"localhost:8080"));
|
||||
mdb.emit(new HttpField(HttpHeader.C_PATH,"/"));
|
||||
try
|
||||
{
|
||||
mdb.build();
|
||||
Assert.fail();
|
||||
}
|
||||
catch(StreamException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(),Matchers.containsString("Duplicate"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test()
|
||||
public void testHuffmanEncodedStandard() throws Exception
|
||||
{
|
||||
HpackDecoder decoder = new HpackDecoder(4096, 8192);
|
||||
|
||||
String encoded = "82868441" + "83" + "49509F";
|
||||
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
|
||||
|
||||
MetaData.Request request = (MetaData.Request)decoder.decode(buffer);
|
||||
|
||||
assertEquals("GET", request.getMethod());
|
||||
assertEquals(HttpScheme.HTTP.asString(), request.getURI().getScheme());
|
||||
assertEquals("/", request.getURI().getPath());
|
||||
assertEquals("test", request.getURI().getHost());
|
||||
assertFalse(request.iterator().hasNext());
|
||||
}
|
||||
|
||||
|
||||
/* 5.2.1: Sends a Huffman-encoded string literal representation with padding longer than 7 bits */
|
||||
@Test()
|
||||
public void testHuffmanEncodedExtraPadding() throws Exception
|
||||
{
|
||||
HpackDecoder decoder = new HpackDecoder(4096, 8192);
|
||||
|
||||
String encoded = "82868441" + "84" + "49509FFF";
|
||||
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
|
||||
|
||||
try
|
||||
{
|
||||
decoder.decode(buffer);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (CompressionException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(), Matchers.containsString("Bad termination"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 5.2.2: Sends a Huffman-encoded string literal representation padded by zero */
|
||||
@Test()
|
||||
public void testHuffmanEncodedZeroPadding() throws Exception
|
||||
{
|
||||
HpackDecoder decoder = new HpackDecoder(4096, 8192);
|
||||
|
||||
String encoded = "82868441" + "83" + "495090";
|
||||
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
|
||||
|
||||
try
|
||||
{
|
||||
decoder.decode(buffer);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (CompressionException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(), Matchers.containsString("Incorrect padding"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 5.2.3: Sends a Huffman-encoded string literal representation containing the EOS symbol */
|
||||
@Test()
|
||||
public void testHuffmanEncodedWithEOS() throws Exception
|
||||
{
|
||||
HpackDecoder decoder = new HpackDecoder(4096, 8192);
|
||||
|
||||
String encoded = "82868441" + "87" + "497FFFFFFF427F";
|
||||
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
|
||||
|
||||
try
|
||||
{
|
||||
decoder.decode(buffer);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (CompressionException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(), Matchers.containsString("EOS in content"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test()
|
||||
public void testHuffmanEncodedOneIncompleteOctet() throws Exception
|
||||
{
|
||||
HpackDecoder decoder = new HpackDecoder(4096, 8192);
|
||||
|
||||
String encoded = "82868441" + "81" + "FE";
|
||||
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
|
||||
|
||||
try
|
||||
{
|
||||
decoder.decode(buffer);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (CompressionException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(), Matchers.containsString("Bad termination"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test()
|
||||
public void testHuffmanEncodedTwoIncompleteOctet() throws Exception
|
||||
{
|
||||
HpackDecoder decoder = new HpackDecoder(4096, 8192);
|
||||
|
||||
String encoded = "82868441" + "82" + "FFFE";
|
||||
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
|
||||
|
||||
try
|
||||
{
|
||||
decoder.decode(buffer);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (CompressionException ex)
|
||||
{
|
||||
Assert.assertThat(ex.getMessage(), Matchers.containsString("Bad termination"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -249,4 +249,28 @@ public class HpackEncoderTest
|
|||
context.get(HpackContext.STATIC_SIZE+1).getSize()+context.get(HpackContext.STATIC_SIZE+2).getSize()));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResize()
|
||||
{
|
||||
HttpFields fields = new HttpFields();
|
||||
fields.add("host", "localhost0");
|
||||
fields.add("cookie","abcdefghij");
|
||||
|
||||
HpackEncoder encoder = new HpackEncoder(4096);
|
||||
|
||||
ByteBuffer buffer = BufferUtil.allocate(4096);
|
||||
int pos = BufferUtil.flipToFill(buffer);
|
||||
encoder.encodeMaxDynamicTableSize(buffer,0);
|
||||
encoder.setRemoteMaxDynamicTableSize(50);
|
||||
encoder.encode(buffer,new MetaData(HttpVersion.HTTP_2,fields));
|
||||
BufferUtil.flipToFlush(buffer,pos);
|
||||
|
||||
HpackContext context = encoder.getHpackContext();
|
||||
|
||||
Assert.assertThat(context.getMaxDynamicTableSize(),Matchers.is(50));
|
||||
Assert.assertThat(context.size(),Matchers.is(1));
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,15 +18,18 @@
|
|||
|
||||
package org.eclipse.jetty.http2.hpack;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.http.BadMessageException;
|
||||
import org.eclipse.jetty.http.DateGenerator;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http.MetaData.Response;
|
||||
|
@ -35,10 +38,6 @@ import org.eclipse.jetty.util.BufferUtil;
|
|||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
public class HpackTest
|
||||
{
|
||||
final static HttpField ServerJetty = new PreEncodedHttpField(HttpHeader.SERVER,"jetty");
|
||||
|
@ -46,7 +45,7 @@ public class HpackTest
|
|||
final static HttpField Date = new PreEncodedHttpField(HttpHeader.DATE,DateGenerator.formatDate(TimeUnit.NANOSECONDS.toMillis(System.nanoTime())));
|
||||
|
||||
@Test
|
||||
public void encodeDecodeResponseTest()
|
||||
public void encodeDecodeResponseTest() throws Exception
|
||||
{
|
||||
HpackEncoder encoder = new HpackEncoder();
|
||||
HpackDecoder decoder = new HpackDecoder(4096,8192);
|
||||
|
@ -99,7 +98,7 @@ public class HpackTest
|
|||
}
|
||||
|
||||
@Test
|
||||
public void encodeDecodeTooLargeTest()
|
||||
public void encodeDecodeTooLargeTest() throws Exception
|
||||
{
|
||||
HpackEncoder encoder = new HpackEncoder();
|
||||
HpackDecoder decoder = new HpackDecoder(4096,164);
|
||||
|
@ -131,14 +130,14 @@ public class HpackTest
|
|||
decoder.decode(buffer);
|
||||
Assert.fail();
|
||||
}
|
||||
catch(BadMessageException e)
|
||||
catch(HpackException.SessionException e)
|
||||
{
|
||||
assertEquals(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431,e.getCode());
|
||||
assertThat(e.getMessage(),containsString("Header too large"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evictReferencedFieldTest()
|
||||
public void evictReferencedFieldTest() throws Exception
|
||||
{
|
||||
HpackEncoder encoder = new HpackEncoder(200,200);
|
||||
HpackDecoder decoder = new HpackDecoder(200,1024);
|
||||
|
|
|
@ -50,17 +50,6 @@ public class HuffmanTest
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeTrailingFF() throws Exception
|
||||
{
|
||||
for (String[] test:tests)
|
||||
{
|
||||
byte[] encoded=TypeUtil.fromHexString(test[1]+"FF");
|
||||
String decoded=Huffman.decode(ByteBuffer.wrap(encoded));
|
||||
Assert.assertEquals(test[0],test[2],decoded);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncode() throws Exception
|
||||
{
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
|
||||
org.eclipse.jetty.http2.LEVEL=INFO
|
||||
org.eclipse.jetty.http2.hpack.LEVEL=INFO
|
||||
#org.eclipse.jetty.http2.LEVEL=DEBUG
|
||||
#org.eclipse.jetty.http2.hpack.LEVEL=DEBUG
|
||||
|
|
|
@ -23,7 +23,6 @@ import java.nio.ByteBuffer;
|
|||
import java.util.ArrayDeque;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Queue;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
|
@ -165,10 +164,8 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements Stream.Listen
|
|||
HttpExchange exchange = getHttpExchange();
|
||||
if (exchange == null)
|
||||
return;
|
||||
|
||||
ErrorCode error = ErrorCode.from(frame.getError());
|
||||
String reason = error == null ? "reset" : error.name().toLowerCase(Locale.ENGLISH);
|
||||
exchange.getRequest().abort(new IOException(reason));
|
||||
int error = frame.getError();
|
||||
exchange.getRequest().abort(new IOException(ErrorCode.toString(error, "reset_code_" + error)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -178,6 +175,13 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements Stream.Listen
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Stream stream, int error, String reason, Callback callback)
|
||||
{
|
||||
responseFailure(new IOException(String.format("%s/%s", ErrorCode.toString(error, null), reason)));
|
||||
callback.succeeded();
|
||||
}
|
||||
|
||||
private void notifyContent(HttpExchange exchange, DataFrame frame, Callback callback)
|
||||
{
|
||||
contentNotifier.offer(new DataInfo(exchange, frame, callback));
|
||||
|
|
|
@ -576,6 +576,40 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
|
|||
Assert.assertArrayEquals(bytes, response.getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidResponseHPack() throws Exception
|
||||
{
|
||||
start(new ServerSessionListener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
|
||||
{
|
||||
// Produce an invalid HPACK block by adding a request pseudo-header to the response.
|
||||
HttpFields fields = new HttpFields();
|
||||
fields.put(":method", "get");
|
||||
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, fields, 0);
|
||||
int streamId = stream.getId();
|
||||
HeadersFrame responseFrame = new HeadersFrame(streamId, response, null, false);
|
||||
Callback.Completable callback = new Callback.Completable();
|
||||
stream.headers(responseFrame, callback);
|
||||
byte[] bytes = "hello".getBytes(StandardCharsets.US_ASCII);
|
||||
callback.thenRun(() -> stream.data(new DataFrame(streamId, ByteBuffer.wrap(bytes), true), Callback.NOOP));
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.timeout(5, TimeUnit.SECONDS)
|
||||
.send(result ->
|
||||
{
|
||||
if (result.isFailed())
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void testExternalServer() throws Exception
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty.http2</groupId>
|
||||
<artifactId>http2-parent</artifactId>
|
||||
|
@ -10,55 +11,76 @@
|
|||
<artifactId>http2-server</artifactId>
|
||||
<name>Jetty :: HTTP2 :: Server</name>
|
||||
|
||||
<properties>
|
||||
<properties>
|
||||
<bundle-symbolic-name>${project.groupId}.server</bundle-symbolic-name>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.github.madgnome</groupId>
|
||||
<artifactId>h2spec-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<mainClass>org.eclipse.jetty.http2.server.H2SpecServer</mainClass>
|
||||
<skip>${skipTests}</skip>
|
||||
<!-- TODO: remove the exclusion when upgrading to h2spec 2.1.1+ -->
|
||||
<excludeSpecs>
|
||||
<excludeSpec>5.1 - closed: Sends a DATA frame</excludeSpec>
|
||||
</excludeSpecs>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>h2spec</id>
|
||||
<phase>test</phase>
|
||||
<goals>
|
||||
<goal>h2spec</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.http2</groupId>
|
||||
<artifactId>http2-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.alpn</groupId>
|
||||
<artifactId>alpn-api</artifactId>
|
||||
<version>${alpn.api.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlet</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlets</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-alpn-server</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.toolchain</groupId>
|
||||
<artifactId>jetty-test-helper</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.http2</groupId>
|
||||
<artifactId>http2-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.alpn</groupId>
|
||||
<artifactId>alpn-api</artifactId>
|
||||
<version>${alpn.api.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlet</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlets</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-alpn-server</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.toolchain</groupId>
|
||||
<artifactId>jetty-test-helper</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -50,6 +50,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
|
|||
private int initialStreamRecvWindow = 512 * 1024;
|
||||
private int maxConcurrentStreams = 128;
|
||||
private int maxHeaderBlockFragment = 0;
|
||||
private int maxFrameLength = Frame.DEFAULT_MAX_LENGTH;
|
||||
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
|
||||
private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F);
|
||||
private long streamIdleTimeout;
|
||||
|
@ -146,6 +147,17 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
|
|||
this.streamIdleTimeout = streamIdleTimeout;
|
||||
}
|
||||
|
||||
@ManagedAttribute("The max frame length in bytes")
|
||||
public int getMaxFrameLength()
|
||||
{
|
||||
return maxFrameLength;
|
||||
}
|
||||
|
||||
public void setMaxFrameLength(int maxFrameLength)
|
||||
{
|
||||
this.maxFrameLength = maxFrameLength;
|
||||
}
|
||||
|
||||
@ManagedAttribute("The max number of keys in all SETTINGS frames")
|
||||
public int getMaxSettingsKeys()
|
||||
{
|
||||
|
@ -217,6 +229,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
|
|||
session.setWriteThreshold(getHttpConfiguration().getOutputBufferSize());
|
||||
|
||||
ServerParser parser = newServerParser(connector, session);
|
||||
parser.setMaxFrameLength(getMaxFrameLength());
|
||||
parser.setMaxSettingsKeys(getMaxSettingsKeys());
|
||||
|
||||
HTTP2Connection connection = new HTTP2ServerConnection(connector.getByteBufferPool(), connector.getExecutor(),
|
||||
|
|
|
@ -115,13 +115,10 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
|
|||
@Override
|
||||
public void onClose(Session session, GoAwayFrame frame, Callback callback)
|
||||
{
|
||||
ErrorCode error = ErrorCode.from(frame.getError());
|
||||
if (error == null)
|
||||
error = ErrorCode.STREAM_CLOSED_ERROR;
|
||||
String reason = frame.tryConvertPayload();
|
||||
if (reason != null && !reason.isEmpty())
|
||||
reason = " (" + reason + ")";
|
||||
getConnection().onSessionFailure(new EofException("HTTP/2 " + error + reason), callback);
|
||||
getConnection().onSessionFailure(new EofException(String.format("Close %s/%s", ErrorCode.toString(frame.getError(), null), reason)), callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -156,10 +153,13 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
|
|||
@Override
|
||||
public void onReset(Stream stream, ResetFrame frame, Callback callback)
|
||||
{
|
||||
ErrorCode error = ErrorCode.from(frame.getError());
|
||||
if (error == null)
|
||||
error = ErrorCode.CANCEL_STREAM_ERROR;
|
||||
getConnection().onStreamFailure((IStream)stream, new EofException("HTTP/2 " + error), callback);
|
||||
getConnection().onStreamFailure((IStream)stream, new EofException("Reset " + ErrorCode.toString(frame.getError(), null)), callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Stream stream, int error, String reason, Callback callback)
|
||||
{
|
||||
getConnection().onStreamFailure((IStream)stream, new EofException(String.format("Failure %s/%s", ErrorCode.toString(error, null), reason)), callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -83,16 +83,39 @@ public class HTTP2ServerSession extends HTTP2Session implements ServerParser.Lis
|
|||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Received {}", frame);
|
||||
|
||||
int streamId = frame.getStreamId();
|
||||
if (!isClientStream(streamId))
|
||||
{
|
||||
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_stream_id");
|
||||
return;
|
||||
}
|
||||
|
||||
IStream stream = getStream(streamId);
|
||||
|
||||
MetaData metaData = frame.getMetaData();
|
||||
if (metaData.isRequest())
|
||||
{
|
||||
IStream stream = createRemoteStream(frame.getStreamId());
|
||||
if (stream != null)
|
||||
if (stream == null)
|
||||
{
|
||||
onStreamOpened(stream);
|
||||
stream.process(frame, Callback.NOOP);
|
||||
Stream.Listener listener = notifyNewStream(stream, frame);
|
||||
stream.setListener(listener);
|
||||
if (isRemoteStreamClosed(streamId))
|
||||
{
|
||||
onConnectionFailure(ErrorCode.STREAM_CLOSED_ERROR.code, "unexpected_headers_frame");
|
||||
}
|
||||
else
|
||||
{
|
||||
stream = createRemoteStream(streamId);
|
||||
if (stream != null)
|
||||
{
|
||||
onStreamOpened(stream);
|
||||
stream.process(frame, Callback.NOOP);
|
||||
Stream.Listener listener = notifyNewStream(stream, frame);
|
||||
stream.setListener(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "duplicate_stream");
|
||||
}
|
||||
}
|
||||
else if (metaData.isResponse())
|
||||
|
@ -102,8 +125,6 @@ public class HTTP2ServerSession extends HTTP2Session implements ServerParser.Lis
|
|||
else
|
||||
{
|
||||
// Trailers.
|
||||
int streamId = frame.getStreamId();
|
||||
IStream stream = getStream(streamId);
|
||||
if (stream != null)
|
||||
{
|
||||
stream.process(frame, Callback.NOOP);
|
||||
|
@ -112,7 +133,8 @@ public class HTTP2ServerSession extends HTTP2Session implements ServerParser.Lis
|
|||
else
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Ignoring {}, stream #{} not found", frame, streamId);
|
||||
LOG.debug("Stream #{} not found", streamId);
|
||||
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_headers_frame");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.http2.server;
|
||||
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
|
||||
/**
|
||||
* HTTP/2 server to run the 'h2spec' tool against.
|
||||
*/
|
||||
public class H2SpecServer
|
||||
{
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
int port = Integer.parseInt(args[0]);
|
||||
|
||||
Server server = new Server();
|
||||
|
||||
HttpConfiguration http_config = new HttpConfiguration();
|
||||
http_config.setRequestHeaderSize(16 * 1024);
|
||||
|
||||
HttpConnectionFactory http = new HttpConnectionFactory(http_config);
|
||||
HTTP2CServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(http_config);
|
||||
ServerConnector connector = new ServerConnector(server, http, h2c);
|
||||
connector.setPort(port);
|
||||
server.addConnector(connector);
|
||||
|
||||
server.start();
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import java.io.IOException;
|
|||
import java.net.ConnectException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.nio.channels.CancelledKeyException;
|
||||
import java.nio.channels.ClosedSelectorException;
|
||||
import java.nio.channels.SelectableChannel;
|
||||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.Selector;
|
||||
|
@ -101,7 +102,9 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable
|
|||
_selectorManager.execute(_strategy::produce);
|
||||
|
||||
// Set started only if we really are started
|
||||
submit(s->_started.set(true));
|
||||
Start start = new Start();
|
||||
submit(start);
|
||||
start._started.await();
|
||||
}
|
||||
|
||||
public int size()
|
||||
|
@ -421,6 +424,10 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable
|
|||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Selector {} woken with none selected", selector);
|
||||
|
||||
if (Thread.interrupted() && !isRunning())
|
||||
throw new ClosedSelectorException();
|
||||
|
||||
selected = selector.selectNow();
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
|
@ -536,6 +543,18 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable
|
|||
public void update(Selector selector);
|
||||
}
|
||||
|
||||
private class Start implements SelectorUpdate
|
||||
{
|
||||
private final CountDownLatch _started = new CountDownLatch(1);
|
||||
|
||||
@Override
|
||||
public void update(Selector selector)
|
||||
{
|
||||
ManagedSelector.this._started.set(true);
|
||||
_started.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
private static class DumpKeys implements SelectorUpdate
|
||||
{
|
||||
private CountDownLatch latch = new CountDownLatch(1);
|
||||
|
|
|
@ -546,7 +546,7 @@ public class SslConnection extends AbstractConnection
|
|||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("net filled={}", net_filled);
|
||||
|
||||
if (net_filled > 0 && _handshake.get() == Handshake.INITIAL && _sslEngine.isOutboundDone())
|
||||
if (net_filled > 0 && _handshake.get() == Handshake.INITIAL && isOutboundDone())
|
||||
throw new SSLHandshakeException("Closed during handshake");
|
||||
|
||||
// Let's unwrap even if we have no net data because in that
|
||||
|
@ -783,6 +783,10 @@ public class SslConnection extends AbstractConnection
|
|||
else
|
||||
LOG.ignore(x);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
LOG.ignore(x);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1036,7 +1040,7 @@ public class SslConnection extends AbstractConnection
|
|||
if (!_closedOutbound)
|
||||
{
|
||||
_closedOutbound=true; // Only attempt this once
|
||||
_sslEngine.closeOutbound();
|
||||
closeOutbound();
|
||||
flush = true;
|
||||
}
|
||||
|
||||
|
@ -1059,6 +1063,18 @@ public class SslConnection extends AbstractConnection
|
|||
}
|
||||
}
|
||||
|
||||
private void closeOutbound()
|
||||
{
|
||||
try
|
||||
{
|
||||
_sslEngine.closeOutbound();
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
LOG.ignore(x);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureFillInterested()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
|
@ -1069,7 +1085,20 @@ public class SslConnection extends AbstractConnection
|
|||
@Override
|
||||
public boolean isOutputShutdown()
|
||||
{
|
||||
return _sslEngine.isOutboundDone() || getEndPoint().isOutputShutdown();
|
||||
return isOutboundDone() || getEndPoint().isOutputShutdown();
|
||||
}
|
||||
|
||||
private boolean isOutboundDone()
|
||||
{
|
||||
try
|
||||
{
|
||||
return _sslEngine.isOutboundDone();
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
LOG.ignore(x);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1090,7 +1119,20 @@ public class SslConnection extends AbstractConnection
|
|||
@Override
|
||||
public boolean isInputShutdown()
|
||||
{
|
||||
return getEndPoint().isInputShutdown() || _sslEngine.isInboundDone();
|
||||
return getEndPoint().isInputShutdown() || isInboundDone();
|
||||
}
|
||||
|
||||
private boolean isInboundDone()
|
||||
{
|
||||
try
|
||||
{
|
||||
return _sslEngine.isInboundDone();
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
LOG.ignore(x);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyHandshakeSucceeded(SSLEngine sslEngine)
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-project</artifactId>
|
||||
<version>9.4.12-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>jetty-jmh</artifactId>
|
||||
<name>Jetty :: Jmh</name>
|
||||
<description>Jmh classes for Jetty</description>
|
||||
<url>http://www.eclipse.org/jetty</url>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>findbugs-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.felix</groupId>
|
||||
<artifactId>maven-bundle-plugin</artifactId>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</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>
|
||||
<transformers>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.openjdk.jmh.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<filters>
|
||||
<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>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- No point deploying jmh project -->
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-util</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-http</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-http</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<classifier>tests</classifier>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.toolchain</groupId>
|
||||
<artifactId>jetty-test-helper</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-core</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-generator-annprocess</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -20,34 +20,42 @@ package org.eclipse.jetty.http.jmh;
|
|||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
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 java.util.concurrent.TimeUnit;
|
||||
|
||||
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.IO;
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Level;
|
||||
import org.openjdk.jmh.annotations.Measurement;
|
||||
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.annotations.Threads;
|
||||
import org.openjdk.jmh.annotations.Warmup;
|
||||
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)
|
||||
@Threads(4)
|
||||
@Warmup(iterations = 7, time = 500, timeUnit = TimeUnit.MILLISECONDS)
|
||||
@Measurement(iterations = 7, time = 500, timeUnit = TimeUnit.MILLISECONDS)
|
||||
public class MultiPartBenchmark
|
||||
{
|
||||
|
||||
|
@ -212,15 +220,23 @@ public class MultiPartBenchmark
|
|||
{
|
||||
for (String multiPart : data)
|
||||
{
|
||||
Path multipartRawFile = MavenTestingUtils.getTestResourcePathFile("multipart/" + multiPart + ".raw");
|
||||
Path expectationPath = MavenTestingUtils.getTestResourcePathFile("multipart/" + multiPart + ".expected.txt");
|
||||
//Path multipartRawFile = MavenTestingUtils.getTestResourcePathFile("multipart/" + multiPart + ".raw");
|
||||
String expectationPath = "multipart/" + multiPart + ".expected.txt";
|
||||
//Path expectationPath = MavenTestingUtils.getTestResourcePathFile("multipart/" + multiPart + ".expected.txt");
|
||||
|
||||
File expectationFile = File.createTempFile( expectationPath, ".tmp" );
|
||||
|
||||
try(InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(expectationPath);
|
||||
OutputStream os = Files.newOutputStream( expectationFile.toPath() )) {
|
||||
IO.copy( inputStream, os );
|
||||
}
|
||||
|
||||
Path outputDir = Files.createTempDirectory( "expected_output_jmh_jetty" );// new File("/tmp").toPath();
|
||||
|
||||
Path outputDir = new File("/tmp").toPath();
|
||||
|
||||
MultipartExpectations multipartExpectations = new MultipartExpectations(expectationPath);
|
||||
MultipartExpectations multipartExpectations = new MultipartExpectations(expectationFile.toPath());
|
||||
MultipartConfigElement config = newMultipartConfigElement(outputDir);
|
||||
|
||||
try (InputStream in = Files.newInputStream(multipartRawFile))
|
||||
try (InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream( "multipart/" + multiPart + ".raw" ))
|
||||
{
|
||||
switch (parserType)
|
||||
{
|
|
@ -0,0 +1,91 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.util.jmh;
|
||||
|
||||
import org.eclipse.jetty.util.DateCache;
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Measurement;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.annotations.Threads;
|
||||
import org.openjdk.jmh.annotations.Warmup;
|
||||
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;
|
||||
import org.openjdk.jmh.runner.options.TimeValue;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@State(Scope.Benchmark)
|
||||
@Threads(4)
|
||||
@Warmup(iterations = 7, time = 500, timeUnit = TimeUnit.MILLISECONDS)
|
||||
@Measurement(iterations = 7, time = 500, timeUnit = TimeUnit.MILLISECONDS)
|
||||
public class DateCacheBenchmark
|
||||
{
|
||||
|
||||
DateCache dateCache = new DateCache();
|
||||
long timestamp = Instant.now().toEpochMilli();
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
public void testDateCacheTimestamp()
|
||||
{
|
||||
dateCache.format(timestamp);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
public void testDateCacheNow()
|
||||
{
|
||||
dateCache.format(new Date());
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
public void testDateCacheFormatNow()
|
||||
{
|
||||
dateCache.formatNow(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws RunnerException
|
||||
{
|
||||
Options opt = new OptionsBuilder()
|
||||
.include(DateCacheBenchmark.class.getSimpleName())
|
||||
.warmupIterations(2)
|
||||
.measurementIterations(3)
|
||||
.forks(1)
|
||||
.threads(400)
|
||||
// .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,195 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.util.jmh;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Date Format Cache.
|
||||
* Computes String representations of Dates and caches
|
||||
* the results so that subsequent requests within the same second
|
||||
* will be fast.
|
||||
*
|
||||
* Only format strings that contain either "ss". Sub second formatting is
|
||||
* not handled.
|
||||
*
|
||||
* The timezone of the date may be included as an ID with the "zzz"
|
||||
* format string or as an offset with the "ZZZ" format string.
|
||||
*
|
||||
* If consecutive calls are frequently very different, then this
|
||||
* may be a little slower than a normal DateFormat.
|
||||
*/
|
||||
public class DateCacheNoTick
|
||||
{
|
||||
public static final String DEFAULT_FORMAT="EEE MMM dd HH:mm:ss zzz yyyy";
|
||||
|
||||
private final String _formatString;
|
||||
private final String _tzFormatString;
|
||||
private final DateTimeFormatter _tzFormat;
|
||||
private final Locale _locale;
|
||||
private final ZoneId _zoneId;
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Constructor.
|
||||
* Make a DateCache that will use a default format. The default format
|
||||
* generates the same results as Date.toString().
|
||||
*/
|
||||
public DateCacheNoTick()
|
||||
{
|
||||
this(DEFAULT_FORMAT);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Constructor.
|
||||
* Make a DateCache that will use the given format
|
||||
* @param format the format to use
|
||||
*/
|
||||
public DateCacheNoTick( String format)
|
||||
{
|
||||
this(format,null,TimeZone.getDefault());
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public DateCacheNoTick( String format, Locale l)
|
||||
{
|
||||
this(format,l,TimeZone.getDefault());
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public DateCacheNoTick( String format, Locale l, String tz)
|
||||
{
|
||||
this(format,l,TimeZone.getTimeZone(tz));
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public DateCacheNoTick( String format, Locale l, TimeZone tz)
|
||||
{
|
||||
_formatString=format;
|
||||
_locale = l;
|
||||
|
||||
|
||||
int zIndex = _formatString.indexOf( "ZZZ" );
|
||||
if( zIndex >= 0 )
|
||||
{
|
||||
String ss1 = _formatString.substring( 0, zIndex );
|
||||
String ss2 = _formatString.substring( zIndex+3 );
|
||||
int tzOffset = tz.getRawOffset();
|
||||
|
||||
StringBuilder sb = new StringBuilder(_formatString.length()+10);
|
||||
sb.append(ss1);
|
||||
sb.append("'");
|
||||
if( tzOffset >= 0 )
|
||||
sb.append( '+' );
|
||||
else
|
||||
{
|
||||
tzOffset = -tzOffset;
|
||||
sb.append( '-' );
|
||||
}
|
||||
|
||||
int raw = tzOffset / (1000*60); // Convert to seconds
|
||||
int hr = raw / 60;
|
||||
int min = raw % 60;
|
||||
|
||||
if( hr < 10 )
|
||||
sb.append( '0' );
|
||||
sb.append( hr );
|
||||
if( min < 10 )
|
||||
sb.append( '0' );
|
||||
sb.append( min );
|
||||
sb.append( '\'' );
|
||||
|
||||
sb.append(ss2);
|
||||
_tzFormatString=sb.toString();
|
||||
}
|
||||
else
|
||||
_tzFormatString=_formatString;
|
||||
|
||||
if( _locale != null )
|
||||
{
|
||||
_tzFormat=DateTimeFormatter.ofPattern(_tzFormatString,_locale);
|
||||
}
|
||||
else
|
||||
{
|
||||
_tzFormat=DateTimeFormatter.ofPattern(_tzFormatString);
|
||||
}
|
||||
_zoneId = tz.toZoneId();
|
||||
_tzFormat.withZone(_zoneId);
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public TimeZone getTimeZone()
|
||||
{
|
||||
return TimeZone.getTimeZone(_zoneId);
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Format a date according to our stored formatter.
|
||||
* @param inDate the Date
|
||||
* @return Formatted date
|
||||
*/
|
||||
public String format(Date inDate)
|
||||
{
|
||||
return ZonedDateTime.ofInstant(inDate.toInstant(),_zoneId).format( _tzFormat );
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Format a date according to our stored formatter.
|
||||
* If it happens to be in the same second as the last formatNow
|
||||
* call, then the format is reused.
|
||||
* @param inDate the date in milliseconds since unix epoch
|
||||
* @return Formatted date
|
||||
*/
|
||||
public String format(long inDate)
|
||||
{
|
||||
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(inDate),_zoneId).format( _tzFormat );
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Format a date according to our stored formatter.
|
||||
* The passed time is expected to be close to the current time, so it is
|
||||
* compared to the last value passed and if it is within the same second,
|
||||
* the format is reused. Otherwise a new cached format is created.
|
||||
* @param now the milliseconds since unix epoch
|
||||
* @return Formatted date
|
||||
*/
|
||||
public String formatNow(long now)
|
||||
{
|
||||
return format(now);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public String now()
|
||||
{
|
||||
return formatNow(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public String getFormatString()
|
||||
{
|
||||
return _formatString;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.util.jmh;
|
||||
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Measurement;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.annotations.Threads;
|
||||
import org.openjdk.jmh.annotations.Warmup;
|
||||
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;
|
||||
import org.openjdk.jmh.runner.options.TimeValue;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@State(Scope.Benchmark)
|
||||
@Threads(4)
|
||||
@Warmup(iterations = 7, time = 500, timeUnit = TimeUnit.MILLISECONDS)
|
||||
@Measurement(iterations = 7, time = 500, timeUnit = TimeUnit.MILLISECONDS)
|
||||
public class DateCacheNoTickBenchmark
|
||||
{
|
||||
|
||||
DateCacheNoTick dateCache = new DateCacheNoTick();
|
||||
long timestamp = Instant.now().toEpochMilli();
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
public void testDateCacheTimestamp()
|
||||
{
|
||||
dateCache.format(timestamp);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
public void testDateCacheNow()
|
||||
{
|
||||
dateCache.format(new Date());
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
public void testDateCacheFormatNow()
|
||||
{
|
||||
dateCache.formatNow(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws RunnerException
|
||||
{
|
||||
Options opt = new OptionsBuilder()
|
||||
.include(DateCacheNoTickBenchmark.class.getSimpleName())
|
||||
.warmupIterations(2)
|
||||
.measurementIterations(3)
|
||||
.forks(1)
|
||||
.threads(400)
|
||||
// .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,268 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.util.jmh;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/** Date Format Cache.
|
||||
* Computes String representations of Dates and caches
|
||||
* the results so that subsequent requests within the same second
|
||||
* will be fast.
|
||||
*
|
||||
* Only format strings that contain either "ss". Sub second formatting is
|
||||
* not handled.
|
||||
*
|
||||
* The timezone of the date may be included as an ID with the "zzz"
|
||||
* format string or as an offset with the "ZZZ" format string.
|
||||
*
|
||||
* If consecutive calls are frequently very different, then this
|
||||
* may be a little slower than a normal DateFormat.
|
||||
*/
|
||||
public class DateCacheSimpleDateFormat
|
||||
{
|
||||
|
||||
public static final String DEFAULT_FORMAT="EEE MMM dd HH:mm:ss zzz yyyy";
|
||||
|
||||
private final String _formatString;
|
||||
private final String _tzFormatString;
|
||||
private final SimpleDateFormat _tzFormat;
|
||||
private final Locale _locale ;
|
||||
|
||||
private volatile Tick _tick;
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* ------------------------------------------------------------ */
|
||||
public static class Tick
|
||||
{
|
||||
final long _seconds;
|
||||
final String _string;
|
||||
public Tick(long seconds, String string)
|
||||
{
|
||||
_seconds = seconds;
|
||||
_string = string;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Constructor.
|
||||
* Make a DateCache that will use a default format. The default format
|
||||
* generates the same results as Date.toString().
|
||||
*/
|
||||
public DateCacheSimpleDateFormat()
|
||||
{
|
||||
this(DEFAULT_FORMAT);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Constructor.
|
||||
* Make a DateCache that will use the given format
|
||||
* @param format the format to use
|
||||
*/
|
||||
public DateCacheSimpleDateFormat(String format)
|
||||
{
|
||||
this( format, null, TimeZone.getDefault());
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public DateCacheSimpleDateFormat(String format,Locale l)
|
||||
{
|
||||
this(format,l,TimeZone.getDefault());
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public DateCacheSimpleDateFormat(String format,Locale l,String tz)
|
||||
{
|
||||
this(format,l,TimeZone.getTimeZone(tz));
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public DateCacheSimpleDateFormat(String format,Locale l,TimeZone tz)
|
||||
{
|
||||
_formatString=format;
|
||||
_locale = l;
|
||||
|
||||
|
||||
int zIndex = _formatString.indexOf( "ZZZ" );
|
||||
if( zIndex >= 0 )
|
||||
{
|
||||
String ss1 = _formatString.substring( 0, zIndex );
|
||||
String ss2 = _formatString.substring( zIndex+3 );
|
||||
int tzOffset = tz.getRawOffset();
|
||||
|
||||
StringBuilder sb = new StringBuilder(_formatString.length()+10);
|
||||
sb.append(ss1);
|
||||
sb.append("'");
|
||||
if( tzOffset >= 0 )
|
||||
sb.append( '+' );
|
||||
else
|
||||
{
|
||||
tzOffset = -tzOffset;
|
||||
sb.append( '-' );
|
||||
}
|
||||
|
||||
int raw = tzOffset / (1000*60); // Convert to seconds
|
||||
int hr = raw / 60;
|
||||
int min = raw % 60;
|
||||
|
||||
if( hr < 10 )
|
||||
sb.append( '0' );
|
||||
sb.append( hr );
|
||||
if( min < 10 )
|
||||
sb.append( '0' );
|
||||
sb.append( min );
|
||||
sb.append( '\'' );
|
||||
|
||||
sb.append(ss2);
|
||||
_tzFormatString=sb.toString();
|
||||
}
|
||||
else
|
||||
_tzFormatString=_formatString;
|
||||
|
||||
if( _locale != null )
|
||||
{
|
||||
_tzFormat=new SimpleDateFormat(_tzFormatString,_locale);
|
||||
}
|
||||
else
|
||||
{
|
||||
_tzFormat=new SimpleDateFormat(_tzFormatString);
|
||||
}
|
||||
_tzFormat.setTimeZone(tz);
|
||||
|
||||
_tick=null;
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public TimeZone getTimeZone()
|
||||
{
|
||||
return _tzFormat.getTimeZone();
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Format a date according to our stored formatter.
|
||||
* @param inDate the Date
|
||||
* @return Formatted date
|
||||
*/
|
||||
public String format(Date inDate)
|
||||
{
|
||||
long seconds = inDate.getTime() / 1000;
|
||||
|
||||
Tick tick=_tick;
|
||||
|
||||
// Is this the cached time
|
||||
if (tick==null || seconds!=tick._seconds)
|
||||
{
|
||||
// It's a cache miss
|
||||
synchronized (this)
|
||||
{
|
||||
return _tzFormat.format(inDate);
|
||||
}
|
||||
}
|
||||
|
||||
return tick._string;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Format a date according to our stored formatter.
|
||||
* If it happens to be in the same second as the last formatNow
|
||||
* call, then the format is reused.
|
||||
* @param inDate the date in milliseconds since unix epoch
|
||||
* @return Formatted date
|
||||
*/
|
||||
public String format(long inDate)
|
||||
{
|
||||
long seconds = inDate / 1000;
|
||||
|
||||
Tick tick=_tick;
|
||||
|
||||
// Is this the cached time
|
||||
if (tick==null || seconds!=tick._seconds)
|
||||
{
|
||||
// It's a cache miss
|
||||
Date d = new Date(inDate);
|
||||
synchronized (this)
|
||||
{
|
||||
return _tzFormat.format(d);
|
||||
}
|
||||
}
|
||||
|
||||
return tick._string;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Format a date according to our stored formatter.
|
||||
* The passed time is expected to be close to the current time, so it is
|
||||
* compared to the last value passed and if it is within the same second,
|
||||
* the format is reused. Otherwise a new cached format is created.
|
||||
* @param now the milliseconds since unix epoch
|
||||
* @return Formatted date
|
||||
*/
|
||||
public String formatNow(long now)
|
||||
{
|
||||
long seconds = now / 1000;
|
||||
|
||||
Tick tick=_tick;
|
||||
|
||||
// Is this the cached time
|
||||
if (tick!=null && tick._seconds==seconds)
|
||||
return tick._string;
|
||||
return formatTick(now)._string;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public String now()
|
||||
{
|
||||
return formatNow(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public Tick tick()
|
||||
{
|
||||
return formatTick(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
protected Tick formatTick(long now)
|
||||
{
|
||||
long seconds = now / 1000;
|
||||
|
||||
// Synchronize to protect _tzFormat
|
||||
synchronized (this)
|
||||
{
|
||||
// recheck the tick, to save multiple formats
|
||||
if (_tick==null || _tick._seconds!=seconds)
|
||||
{
|
||||
String s= _tzFormat.format(new Date(now));
|
||||
return _tick=new Tick(seconds,s);
|
||||
}
|
||||
return _tick;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public String getFormatString()
|
||||
{
|
||||
return _formatString;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.util.jmh;
|
||||
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Measurement;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.annotations.Threads;
|
||||
import org.openjdk.jmh.annotations.Warmup;
|
||||
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;
|
||||
import org.openjdk.jmh.runner.options.TimeValue;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@State(Scope.Benchmark)
|
||||
@Threads(4)
|
||||
@Warmup(iterations = 7, time = 500, timeUnit = TimeUnit.MILLISECONDS)
|
||||
@Measurement(iterations = 7, time = 500, timeUnit = TimeUnit.MILLISECONDS)
|
||||
public class DateCacheSimpleDateFormatBenchmark
|
||||
{
|
||||
|
||||
DateCacheSimpleDateFormat dateCache = new DateCacheSimpleDateFormat();
|
||||
long timestamp = Instant.now().toEpochMilli();
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
public void testDateCacheTimestamp()
|
||||
{
|
||||
dateCache.format(timestamp);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
public void testDateCacheNow()
|
||||
{
|
||||
dateCache.format(new Date());
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
public void testDateCacheFormatNow()
|
||||
{
|
||||
dateCache.formatNow(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws RunnerException
|
||||
{
|
||||
Options opt = new OptionsBuilder()
|
||||
.include(DateCacheSimpleDateFormatBenchmark.class.getSimpleName())
|
||||
.warmupIterations(2)
|
||||
.measurementIterations(3)
|
||||
.forks(1)
|
||||
.threads(400)
|
||||
// .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,4 @@
|
|||
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
|
||||
#org.eclipse.jetty.LEVEL=DEBUG
|
||||
#org.eclipse.jetty.server.LEVEL=DEBUG
|
||||
#org.eclipse.jetty.http.LEVEL=DEBUG
|
Binary file not shown.
|
@ -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, |