merge from master

This commit is contained in:
Jesse McConnell 2012-03-07 16:44:31 -06:00
commit 381615f52d
170 changed files with 16913 additions and 217 deletions

View File

@ -1,5 +1,16 @@
jetty-8.1.3-SNAPSHOT
jetty-7.6.2.v20120302 - 02 March 2012
+ 370387 SafariWebsocketDraft0Test failure during build.
+ 371168 Update ClientCrossContextSessionTest
+ 372093 handle quotes in Require-Bundle manifest string
+ 372457 Big response + slow clients + pipelined requests cause Jetty spinning
and eventually closing connections. Added a TODO for a method renaming that
will happen in the next major release (to avoid break implementers).
+ 372487 JDBCSessionManager does not work with Oracle
+ 372806 Command line should accept relative paths for xml config files
+ JETTY-1489 WebAppProvider attempts to deploy .svn folder
jetty-8.1.2.v20120302 - 02 March 2012
+ 370387 SafariWebsocketDraft0Test failure during build.
+ 371168 Update ClientCrossContextSessionTest

View File

@ -106,7 +106,7 @@ public class GzipResponseWrapper extends HttpServletResponseWrapper
}
if ((_gzStream==null || _gzStream._out==null) &&
(_mimeTypes==null && "application/gzip".equalsIgnoreCase(ct) ||
(_mimeTypes==null && ct!=null && ct.contains("gzip") ||
_mimeTypes!=null && (ct==null||!_mimeTypes.contains(StringUtil.asciiToLowerCase(ct)))))
{
noGzip();

View File

@ -385,7 +385,7 @@ public class SelectChannelEndPoint extends ChannelEndPoint implements AsyncEndPo
try
{
updateKey();
this.wait(timeoutMs>=0?(end-now):10000);
this.wait(timeoutMs>0?(end-now):10000);
}
catch (InterruptedException e)
{
@ -433,7 +433,7 @@ public class SelectChannelEndPoint extends ChannelEndPoint implements AsyncEndPo
try
{
updateKey();
this.wait(timeoutMs>=0?(end-now):10000);
this.wait(timeoutMs>0?(end-now):10000);
}
catch (InterruptedException e)
{
@ -455,14 +455,14 @@ public class SelectChannelEndPoint extends ChannelEndPoint implements AsyncEndPo
}
return true;
}
/* ------------------------------------------------------------ */
/**
* @see org.eclipse.jetty.io.AsyncEndPoint#scheduleWrite()
*/
public void scheduleWrite()
{
if (_writable==true)
if (_writable)
LOG.debug("Required scheduleWrite {}",this);
_writable=false;
@ -687,14 +687,15 @@ public class SelectChannelEndPoint extends ChannelEndPoint implements AsyncEndPo
{
try
{
if (_key!=null)
_key.cancel();
SelectionKey key = _key;
if (key!=null)
key.cancel();
}
catch (Throwable e)
{
LOG.ignore(e);
}
try
{
super.close();

View File

@ -285,7 +285,7 @@ public class IOTest
Socket client;
Socket server;
connector = new ServerSocket(9123);
connector = new ServerSocket(0);
client = new Socket("127.0.0.1",connector.getLocalPort());
server = connector.accept();
client.setTcpNoDelay(true);

16
jetty-npn/pom.xml Normal file
View File

@ -0,0 +1,16 @@
<?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">
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
<version>7.6.2-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.eclipse.jetty.npn</groupId>
<artifactId>npn-api</artifactId>
<name>Jetty :: Next Protocol Negotiation :: API</name>
</project>

View File

@ -0,0 +1,218 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.npn;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSocket;
/**
* <p>{@link NextProtoNego} provides an API to applications that want to make use of the
* <a href="http://technotes.googlecode.com/git/nextprotoneg.html">Next Protocol Negotiation</a>.</p>
* <p>The NPN extension is only available when using the TLS protocol, therefore applications must
* ensure that the TLS protocol is used:</p>
* <pre>
* SSLContext context = SSLContext.getInstance("TLSv1");
* </pre>
* <p>Refer to the
* <a href="http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SSLContext">list
* of standard SSLContext protocol names</a> for further information on TLS protocol versions supported.</p>
* <p>Applications must register instances of either {@link SSLSocket} or {@link SSLEngine} with a
* {@link ClientProvider} or with a {@link ServerProvider}, depending whether they are on client or
* server side.</p>
* <p>The NPN implementation will invoke the provider callbacks to allow applications to interact
* with the negotiation of the next protocol.</p>
* <p>Client side typical usage:</p>
* <pre>
* SSLSocket sslSocket = ...;
* NextProtoNego.put(sslSocket, new NextProtoNego.ClientProvider()
* {
* &#64;Override
* public boolean supports()
* {
* return true;
* }
*
* &#64;Override
* public void unsupported()
* {
* }
*
* &#64;Override
* public String selectProtocol(List&lt;String&gt; protocols)
* {
* return protocols.get(0);
* }
* });
* </pre>
* <p>Server side typical usage:</p>
* <pre>
* SSLSocket sslSocket = ...;
* NextProtoNego.put(sslSocket, new NextProtoNego.ServerProvider()
* {
* &#64;Override
* public void unsupported()
* {
* }
*
* &#64;Override
* public List<String> protocols()
* {
* return Arrays.asList("http/1.1");
* }
*
* &#64;Override
* public void protocolSelected(String protocol)
* {
* System.out.println("Protocol Selected is: " + protocol);
* }
* });
* </pre>
* <p>There is no need to unregister {@link SSLSocket} or {@link SSLEngine} instances, as they
* are kept in a {@link WeakHashMap} and will be garbage collected when the application does not
* hard reference them anymore.</p>
* <p>In order to help application development, you can set the {@link NextProtoNego#debug} field
* to {@code true} to have debug code printed to {@link System#err}.</p>
*/
public class NextProtoNego
{
/**
* <p>Enables debug logging on {@link System#err}.</p>
*/
public static boolean debug = false;
private static Map<Object, Provider> objects = Collections.synchronizedMap(new WeakHashMap<Object, Provider>());
private NextProtoNego()
{
}
/**
* <p>Registers a SSLSocket with a provider.</p>
*
* @param socket the socket to register with the provider
* @param provider the provider to register with the socket
*/
public static void put(SSLSocket socket, Provider provider)
{
objects.put(socket, provider);
}
/**
* @param socket a socket registered with {@link #put(SSLSocket, Provider)}
* @return the provider registered with the given socket
*/
public static Provider get(SSLSocket socket)
{
return objects.get(socket);
}
/**
* <p>Registers a SSLEngine with a provider.</p>
*
* @param engine the engine to register with the provider
* @param provider the provider to register with the engine
*/
public static void put(SSLEngine engine, Provider provider)
{
objects.put(engine, provider);
}
/**
*
* @param engine an engine registered with {@link #put(SSLEngine, Provider)}
* @return the provider registered with the given engine
*/
public static Provider get(SSLEngine engine)
{
return objects.get(engine);
}
/**
* <p>Base, empty, interface for providers.</p>
*/
public interface Provider
{
}
/**
* <p>The client-side provider interface that applications must implement to interact
* with the negotiation of the next protocol.</p>
*/
public interface ClientProvider extends Provider
{
/**
* <p>Callback invoked to let the implementation know whether an
* empty NPN extension should be added to a ClientHello SSL message.</p>
*
* @return true to add the NPN extension, false otherwise
*/
public boolean supports();
/**
* <p>Callback invoked to let the application know that the server does
* not support NPN.</p>
*/
public void unsupported();
/**
* <p>Callback invoked to let the application select a protocol
* among the ones sent by the server.</p>
*
* @param protocols the protocols sent by the server
* @return the protocol selected by the application, or null if the
* NextProtocol SSL message should not be sent to the server
*/
public String selectProtocol(List<String> protocols);
}
/**
* <p>The server-side provider interface that applications must implement to interact
* with the negotiation of the next protocol.</p>
*/
public interface ServerProvider extends Provider
{
/**
* <p>Callback invoked to let the application know that the client does not
* support NPN.</p>
*/
public void unsupported();
/**
* <p>Callback invoked to let the implementation know the list
* of protocols that should be added to an NPN extension in a
* ServerHello SSL message.</p>
* <p>This callback is invoked only if the client sent a NPN extension.</p>
*
* @return the list of protocols, or null if no NPN extension
* should be sent to the client
*/
public List<String> protocols();
/**
* <p>Callback invoked to let the application know the protocol selected
* by the client.</p>
* <p>This callback is invoked only if the client sent a NextProtocol SSL message.</p>
*
* @param protocol the selected protocol
*/
public void protocolSelected(String protocol);
}
}

View File

@ -30,7 +30,6 @@ import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.continuation.Continuation;
import org.eclipse.jetty.continuation.ContinuationThrowable;
import org.eclipse.jetty.continuation.ContinuationListener;
import org.eclipse.jetty.http.PathMap;
import org.eclipse.jetty.io.AsyncEndPoint;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.handler.ContextHandler;
@ -940,8 +939,10 @@ public class AsyncContinuation implements AsyncContext, Continuation
synchronized (this)
{
doSuspend(context,request,response);
if ( request instanceof HttpServletRequest)
_event._pathInContext=URIUtil.addPaths(((HttpServletRequest)request).getServletPath(),((HttpServletRequest)request).getPathInfo());
if (request instanceof HttpServletRequest)
{
_event._pathInContext = URIUtil.addPaths(((HttpServletRequest)request).getServletPath(),((HttpServletRequest)request).getPathInfo());
}
}
}

View File

@ -793,7 +793,7 @@ public class Response implements HttpServletResponse
if (isCommitted() || _connection.isIncluding())
return;
_connection._generator.setContentLength(len);
if (len>=0)
if (len>0)
{
_connection.getResponseFields().putLongField(HttpHeaders.CONTENT_LENGTH, len);
if (_connection._generator.isAllContentWritten())

View File

@ -24,8 +24,8 @@ import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
/* ------------------------------------------------------------ */
/** A <code>HandlerContainer</code> that allows a hot swap
* of a wrapped handler.
/**
* A <code>HandlerContainer</code> that allows a hot swap of a wrapped handler.
*
*/
public class HotSwapHandler extends AbstractHandlerContainer
@ -48,113 +48,106 @@ public class HotSwapHandler extends AbstractHandlerContainer
{
return _handler;
}
/* ------------------------------------------------------------ */
/**
* @return Returns the handlers.
*/
public Handler[] getHandlers()
{
return new Handler[] {_handler};
return new Handler[]
{ _handler };
}
/* ------------------------------------------------------------ */
/**
* @param handler Set the {@link Handler} which should be wrapped.
* @param handler
* Set the {@link Handler} which should be wrapped.
*/
public void setHandler(Handler handler)
{
if (handler == null)
throw new IllegalArgumentException("Parameter handler is null.");
try
{
Handler old_handler = _handler;
_handler = handler;
if (handler!=null)
{
handler.setServer(getServer());
if (isStarted())
handler.start();
}
Server server = getServer();
handler.setServer(server);
addBean(handler);
if (getServer()!=null)
getServer().getContainer().update(this, old_handler, handler, "handler");
if (server != null)
server.getContainer().update(this,old_handler,handler,"handler");
// if there is an old handler and it was started, stop it
if (old_handler != null && isStarted())
if (old_handler != null)
{
old_handler.stop();
removeBean(old_handler);
}
}
catch(RuntimeException e)
{
throw e;
}
catch(Exception e)
catch (Exception e)
{
throw new RuntimeException(e);
}
}
/* ------------------------------------------------------------ */
/*
/*
* @see org.eclipse.thread.AbstractLifeCycle#doStart()
*/
@Override
protected void doStart() throws Exception
{
if (_handler!=null)
_handler.start();
super.doStart();
}
/* ------------------------------------------------------------ */
/*
/*
* @see org.eclipse.thread.AbstractLifeCycle#doStop()
*/
@Override
protected void doStop() throws Exception
{
super.doStop();
if (_handler!=null)
_handler.stop();
}
/* ------------------------------------------------------------ */
/*
/*
* @see org.eclipse.jetty.server.server.EventHandler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
if (_handler!=null && isStarted())
if (_handler != null && isStarted())
{
_handler.handle(target,baseRequest, request, response);
_handler.handle(target,baseRequest,request,response);
}
}
/* ------------------------------------------------------------ */
@Override
public void setServer(Server server)
{
Server old_server=getServer();
if (server==old_server)
Server old_server = getServer();
if (server == old_server)
return;
if (isRunning())
throw new IllegalStateException(RUNNING);
super.setServer(server);
Handler h=getHandler();
if (h!=null)
Handler h = getHandler();
if (h != null)
h.setServer(server);
if (server!=null && server!=old_server)
server.getContainer().update(this, null,_handler, "handler");
if (server != null && server != old_server)
server.getContainer().update(this,null,_handler,"handler");
}
/* ------------------------------------------------------------ */
@SuppressWarnings(
{ "rawtypes", "unchecked" })
@Override
protected Object expandChildren(Object list, Class byClass)
{
@ -167,8 +160,8 @@ public class HotSwapHandler extends AbstractHandlerContainer
{
if (!isStopped())
throw new IllegalStateException("!STOPPED");
Handler child=getHandler();
if (child!=null)
Handler child = getHandler();
if (child != null)
{
setHandler(null);
child.destroy();

View File

@ -79,7 +79,16 @@ public class SelectChannelConnector extends AbstractNIOConnector
addBean(_manager,true);
setAcceptors(Math.max(1,(Runtime.getRuntime().availableProcessors()+3)/4));
}
@Override
public void setThreadPool(ThreadPool pool)
{
super.setThreadPool(pool);
// preserve start order
removeBean(_manager);
addBean(_manager,true);
}
/* ------------------------------------------------------------ */
@Override
public void accept(int acceptorID) throws IOException

View File

@ -39,7 +39,9 @@ import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.StdErrLog;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.matchers.JUnitMatchers;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.junit.Assert.assertEquals;
@ -1048,10 +1050,13 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
else
out.println(avail);
for (int i=0;i<avail;i++)
while (avail>0)
{
buf+=(char)in.read();
avail=in.available();
avail=in.available();
}
out.println(avail);
out.println(buf);
out.close();
@ -1098,9 +1103,9 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
// skip header
while(reader.readLine().length()>0);
assertEquals(10,Integer.parseInt(reader.readLine()));
assertThat(Integer.parseInt(reader.readLine()),Matchers.greaterThan(0));
assertEquals(0,Integer.parseInt(reader.readLine()));
assertEquals(20,Integer.parseInt(reader.readLine()));
assertThat(Integer.parseInt(reader.readLine()),Matchers.greaterThan(0));
assertEquals(0,Integer.parseInt(reader.readLine()));
assertEquals("1234567890abcdefghijklmnopqrst",reader.readLine());

View File

@ -441,6 +441,20 @@ public class ResponseTest
}
}
@Test
public void testZeroContent () throws Exception
{
Response response = new Response (new TestHttpConnection(connector, new ByteArrayEndPoint(), connector.getServer()));
PrintWriter writer = response.getWriter();
response.setContentLength(0);
assertTrue(!response.isCommitted());
assertTrue(!writer.checkError());
writer.print("");
assertTrue(!writer.checkError());
assertTrue(response.isCommitted());
}
@Test
public void testHead() throws Exception
{

View File

@ -178,22 +178,26 @@ public class ConnectHandlerSSLTest extends AbstractConnectHandlerTest
private class AlwaysTrustManager implements X509TrustManager
{
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException
{
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException
{
}
@Override
public X509Certificate[] getAcceptedIssuers()
{
return null;
return new X509Certificate[]{};
}
}
private static class ServerHandler extends AbstractHandler
{
@Override
public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
{
request.setHandled(true);

View File

@ -51,16 +51,19 @@ public class SSLCloseTest extends TestCase
private static AsyncEndPoint __endp;
private static class CredulousTM implements TrustManager, X509TrustManager
{
@Override
public X509Certificate[] getAcceptedIssuers()
{
return null;
return new X509Certificate[]{};
}
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException
{
return;
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException
{
return;
@ -129,6 +132,7 @@ public class SSLCloseTest extends TestCase
private static class WriteHandler extends AbstractHandler
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
try

View File

@ -27,6 +27,7 @@ import javax.net.ssl.TrustManagerFactory;
import org.eclipse.jetty.server.HttpServerTestBase;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
/**
@ -135,4 +136,13 @@ public class SelectChannelServerSslTest extends HttpServerTestBase
client.close();
}
}
@Override
@Ignore
public void testAvailable() throws Exception
{
}
}

View File

@ -23,6 +23,7 @@ import javax.net.ssl.TrustManagerFactory;
import org.eclipse.jetty.server.HttpServerTestBase;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
/**
@ -75,4 +76,10 @@ public class SslSocketServerTest extends HttpServerTestBase
// TODO this test uses URL, so noop for now
}
@Override
@Ignore
public void testAvailable() throws Exception
{
}
}

View File

@ -1,5 +1,9 @@
package org.eclipse.jetty.servlet;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
@ -10,8 +14,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
import junit.framework.Assert;
import org.eclipse.jetty.server.AsyncContinuation;
@ -41,12 +44,13 @@ public class AsyncContextTest
@Before
public void setUp() throws Exception
{
_connector.setMaxIdleTime(3000000);
_connector.setMaxIdleTime(30000);
_server.setConnectors(new Connector[]
{ _connector });
_contextHandler.setContextPath("/");
_contextHandler.addServlet(new ServletHolder(new TestServlet()),"/servletPath");
_contextHandler.addServlet(new ServletHolder(new TestServlet()),"/path with spaces/servletPath");
_contextHandler.addServlet(new ServletHolder(new TestServlet2()),"/servletPath2");
_contextHandler.addServlet(new ServletHolder(new ForwardingServlet()),"/forward");
_contextHandler.addServlet(new ServletHolder(new AsyncDispatchingServlet()),"/dispatchingServlet");
@ -61,112 +65,102 @@ public class AsyncContextTest
@Test
public void testSimpleAsyncContext() throws Exception
{
{
String request = "GET /servletPath HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n"
+ "Connection: close\r\n" + "\r\n";
String responseString = _connector.getResponses(request);
System.err.println("REsponse string="+responseString);
BufferedReader br = new BufferedReader(new StringReader(responseString));
Assert.assertEquals("HTTP/1.1 200 OK",br.readLine());
br.readLine();// connection close
br.readLine();// server
br.readLine();// empty
Assert.assertEquals("servlet gets right path","doGet:getServletPath:/servletPath", br.readLine());
Assert.assertEquals("async context gets right path in get","doGet:async:getServletPath:/servletPath", br.readLine());
Assert.assertEquals("async context gets right path in async","async:run:attr:servletPath:/servletPath", br.readLine());
BufferedReader br = parseHeader(responseString);
Assert.assertEquals("servlet gets right path","doGet:getServletPath:/servletPath",br.readLine());
Assert.assertEquals("async context gets right path in get","doGet:async:getServletPath:/servletPath",br.readLine());
Assert.assertEquals("async context gets right path in async","async:run:attr:servletPath:/servletPath",br.readLine());
}
@Test
public void testDispatchAsyncContext() throws Exception
{
{
String request = "GET /servletPath?dispatch=true HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n"
+ "Connection: close\r\n" + "\r\n";
String responseString = _connector.getResponses(request);
BufferedReader br = new BufferedReader(new StringReader(responseString));
Assert.assertEquals("HTTP/1.1 200 OK",br.readLine());
br.readLine();// connection close
br.readLine();// server
br.readLine();// empty
Assert.assertEquals("servlet gets right path","doGet:getServletPath:/servletPath2", br.readLine());
Assert.assertEquals("async context gets right path in get","doGet:async:getServletPath:/servletPath2", br.readLine());
Assert.assertEquals("servlet path attr is original","async:run:attr:servletPath:/servletPath", br.readLine());
Assert.assertEquals("path info attr is correct","async:run:attr:pathInfo:null", br.readLine());
Assert.assertEquals("query string attr is correct","async:run:attr:queryString:dispatch=true", br.readLine());
Assert.assertEquals("context path attr is correct","async:run:attr:contextPath:", br.readLine());
Assert.assertEquals("request uri attr is correct","async:run:attr:requestURI:/servletPath", br.readLine());
}
@Test
public void testSimpleWithContextAsyncContext() throws Exception
{
_contextHandler.setContextPath("/foo");
String request = "GET /foo/servletPath HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n"
+ "Connection: close\r\n" + "\r\n";
String responseString = _connector.getResponses(request);
BufferedReader br = new BufferedReader(new StringReader(responseString));
Assert.assertEquals("HTTP/1.1 200 OK",br.readLine());
br.readLine();// connection close
br.readLine();// server
br.readLine();// empty
Assert.assertEquals("servlet gets right path","doGet:getServletPath:/servletPath", br.readLine());
Assert.assertEquals("async context gets right path in get","doGet:async:getServletPath:/servletPath", br.readLine());
Assert.assertEquals("async context gets right path in async","async:run:attr:servletPath:/servletPath", br.readLine());
BufferedReader br = parseHeader(responseString);
Assert.assertEquals("servlet gets right path","doGet:getServletPath:/servletPath2",br.readLine());
Assert.assertEquals("async context gets right path in get","doGet:async:getServletPath:/servletPath2",br.readLine());
Assert.assertEquals("servlet path attr is original","async:run:attr:servletPath:/servletPath",br.readLine());
Assert.assertEquals("path info attr is correct","async:run:attr:pathInfo:null",br.readLine());
Assert.assertEquals("query string attr is correct","async:run:attr:queryString:dispatch=true",br.readLine());
Assert.assertEquals("context path attr is correct","async:run:attr:contextPath:",br.readLine());
Assert.assertEquals("request uri attr is correct","async:run:attr:requestURI:/servletPath",br.readLine());
}
@Test
public void testDispatchWithContextAsyncContext() throws Exception
{
_contextHandler.setContextPath("/foo");
String request = "GET /foo/servletPath?dispatch=true HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n"
public void testDispatchAsyncContextEncodedPathAndQueryString() throws Exception
{
String request = "GET /path%20with%20spaces/servletPath?dispatch=true&queryStringWithEncoding=space%20space HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n"
+ "Connection: close\r\n" + "\r\n";
String responseString = _connector.getResponses(request);
System.out.println(responseString);
BufferedReader br = parseHeader(responseString);
BufferedReader br = new BufferedReader(new StringReader(responseString));
Assert.assertEquals("HTTP/1.1 200 OK",br.readLine());
br.readLine();// connection close
br.readLine();// server
br.readLine();// empty
Assert.assertEquals("servlet gets right path","doGet:getServletPath:/servletPath2", br.readLine());
Assert.assertEquals("async context gets right path in get","doGet:async:getServletPath:/servletPath2", br.readLine());
Assert.assertEquals("servlet path attr is original","async:run:attr:servletPath:/servletPath", br.readLine());
Assert.assertEquals("path info attr is correct","async:run:attr:pathInfo:null", br.readLine());
Assert.assertEquals("query string attr is correct","async:run:attr:queryString:dispatch=true", br.readLine());
Assert.assertEquals("context path attr is correct","async:run:attr:contextPath:/foo", br.readLine());
Assert.assertEquals("request uri attr is correct","async:run:attr:requestURI:/foo/servletPath", br.readLine());
}
assertThat("servlet gets right path",br.readLine(),equalTo("doGet:getServletPath:/servletPath2"));
assertThat("async context gets right path in get",br.readLine(), equalTo("doGet:async:getServletPath:/servletPath2"));
assertThat("servlet path attr is original",br.readLine(),equalTo("async:run:attr:servletPath:/path with spaces/servletPath"));
assertThat("path info attr is correct",br.readLine(),equalTo("async:run:attr:pathInfo:null"));
assertThat("query string attr is correct",br.readLine(),equalTo("async:run:attr:queryString:dispatch=true&queryStringWithEncoding=space%20space"));
assertThat("context path attr is correct",br.readLine(),equalTo("async:run:attr:contextPath:"));
assertThat("request uri attr is correct",br.readLine(),equalTo("async:run:attr:requestURI:/path%20with%20spaces/servletPath"));
}
@Test
public void testSimpleWithContextAsyncContext() throws Exception
{
_contextHandler.setContextPath("/foo");
String request = "GET /foo/servletPath HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n"
+ "Connection: close\r\n" + "\r\n";
String responseString = _connector.getResponses(request);
BufferedReader br = parseHeader(responseString);
Assert.assertEquals("servlet gets right path","doGet:getServletPath:/servletPath",br.readLine());
Assert.assertEquals("async context gets right path in get","doGet:async:getServletPath:/servletPath",br.readLine());
Assert.assertEquals("async context gets right path in async","async:run:attr:servletPath:/servletPath",br.readLine());
}
@Test
public void testDispatchWithContextAsyncContext() throws Exception
{
_contextHandler.setContextPath("/foo");
String request = "GET /foo/servletPath?dispatch=true HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n"
+ "Connection: close\r\n" + "\r\n";
String responseString = _connector.getResponses(request);
BufferedReader br = parseHeader(responseString);
Assert.assertEquals("servlet gets right path","doGet:getServletPath:/servletPath2",br.readLine());
Assert.assertEquals("async context gets right path in get","doGet:async:getServletPath:/servletPath2",br.readLine());
Assert.assertEquals("servlet path attr is original","async:run:attr:servletPath:/servletPath",br.readLine());
Assert.assertEquals("path info attr is correct","async:run:attr:pathInfo:null",br.readLine());
Assert.assertEquals("query string attr is correct","async:run:attr:queryString:dispatch=true",br.readLine());
Assert.assertEquals("context path attr is correct","async:run:attr:contextPath:/foo",br.readLine());
Assert.assertEquals("request uri attr is correct","async:run:attr:requestURI:/foo/servletPath",br.readLine());
}
@Test
public void testDispatch() throws Exception
{
String request = "GET /forward HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Connection: close\r\n"
+ "\r\n";
String responseString = _connector.getResponses(request);
BufferedReader br = new BufferedReader(new StringReader(responseString));
assertEquals("HTTP/1.1 200 OK",br.readLine());
br.readLine();// connection close
br.readLine();// server
br.readLine();// empty
BufferedReader br = parseHeader(responseString);
assertThat("!ForwardingServlet",br.readLine(),equalTo("Dispatched back to ForwardingServlet"));
}
@ -176,16 +170,24 @@ public class AsyncContextTest
{
String request = "GET /forward?dispatchRequestResponse=true HTTP/1.1\r\n" + "Host: localhost\r\n"
+ "Content-Type: application/x-www-form-urlencoded\r\n" + "Connection: close\r\n" + "\r\n";
String responseString = _connector.getResponses(request);
BufferedReader br = parseHeader(responseString);
assertThat("!AsyncDispatchingServlet",br.readLine(),equalTo("Dispatched back to AsyncDispatchingServlet"));
}
private BufferedReader parseHeader(String responseString) throws IOException
{
BufferedReader br = new BufferedReader(new StringReader(responseString));
assertEquals("HTTP/1.1 200 OK",br.readLine());
br.readLine();// connection close
br.readLine();// server
br.readLine();// empty
assertThat("!AsyncDispatchingServlet",br.readLine(),equalTo("Dispatched back to AsyncDispatchingServlet"));
return br;
}
private class ForwardingServlet extends HttpServlet
@ -206,28 +208,32 @@ public class AsyncContextTest
}
}
private class AsyncDispatchingServlet extends HttpServlet
{
private static final long serialVersionUID = 1L;
private class AsyncDispatchingServlet extends HttpServlet
{
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, final HttpServletResponse response) throws ServletException, IOException
{
Request request = (Request)req;
if (request.getDispatcherType() == DispatcherType.ASYNC)
{
response.getOutputStream().print("Dispatched back to AsyncDispatchingServlet");
}
else
{
final AsyncContext asyncContext;
if (request.getParameter("dispatchRequestResponse") != null)
asyncContext = request.startAsync(request,response);
else
asyncContext = request.startAsync();
@Override
protected void doGet(HttpServletRequest req, final HttpServletResponse response) throws ServletException, IOException
{
Request request = (Request)req;
if (request.getDispatcherType() == DispatcherType.ASYNC)
{
response.getOutputStream().print("Dispatched back to AsyncDispatchingServlet");
}
else
{
final AsyncContext asyncContext;
if (request.getParameter("dispatchRequestResponse") != null)
{
asyncContext = request.startAsync(request,response);
}
else
{
asyncContext = request.startAsync();
}
new Thread(new DispatchingRunnable(asyncContext)).start();
}
new Thread(new DispatchingRunnable(asyncContext)).start();
}
}
}

View File

@ -25,6 +25,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@ -287,7 +288,14 @@ public class MultiPartFilter implements Filter
@Override
public Map getParameterMap()
{
return Collections.unmodifiableMap(_params.toStringArrayMap());
Map<String, String> cmap = new HashMap<String,String>();
for ( Object key : _params.keySet() )
{
cmap.put((String)key,getParameter((String)key));
}
return Collections.unmodifiableMap(cmap);
}
/* ------------------------------------------------------------------------------- */

View File

@ -597,12 +597,14 @@ public class ProxyServlet implements Servlet
{
exchange.addRequestHeader("X-Forwarded-For",request.getRemoteAddr());
exchange.addRequestHeader("X-Forwarded-Proto",request.getScheme());
exchange.addRequestHeader("X-Forwarded-Host",request.getServerName());
exchange.addRequestHeader("X-Forwarded-Host",request.getHeader("Host"));
exchange.addRequestHeader("X-Forwarded-Server",request.getLocalName());
}
if (hasContent)
{
exchange.setRequestContentSource(in);
}
customizeExchange(exchange, request);

View File

@ -50,7 +50,8 @@ import javax.servlet.http.HttpServletRequest;
*/
public class UserAgentFilter implements Filter
{
private Pattern _pattern;
private static final String __defaultPattern = "(?:Mozilla[^\\(]*\\(compatible;\\s*+([^;]*);.*)|(?:.*?([^\\s]+/[^\\s]+).*)";
private Pattern _pattern = Pattern.compile(__defaultPattern);
private Map _agentCache = new ConcurrentHashMap();
private int _agentCacheSize=1024;
private String _attribute;

View File

@ -137,4 +137,26 @@ public class GzipFilterDefaultTest
}
@Test
public void testUserAgentExclusion() throws Exception
{
GzipTester tester = new GzipTester(testingdir);
FilterHolder holder = tester.setContentServlet(DefaultServlet.class);
holder.setInitParameter("excludedAgents", "foo");
tester.setUserAgent("foo");
int filesize = GzipResponseWrapper.DEFAULT_BUFFER_SIZE * 4;
tester.prepareServerFile("file.txt",filesize);
try
{
tester.start();
tester.assertIsResponseNotGzipCompressed("file.txt", filesize, HttpStatus.OK_200);
}
finally
{
tester.stop();
}
}
}

View File

@ -47,6 +47,10 @@ public class MultipartFilterTest
private ServletTester tester;
@Before
public void setUp() throws Exception
{
@ -238,6 +242,65 @@ public class MultipartFilterTest
assertTrue(response.getContent().indexOf("brown cow")>=0);
}
/*
* see the testParameterMap test
*
*/
public static class TestServletParameterMap extends DumpServlet
{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
assertEquals("How now brown cow.", req.getParameterMap().get("strupContent-Type:"));
super.doPost(req, resp);
}
}
/**
* Validate that the getParameterMap() call is correctly unencoding the parameters in the
* map that it returns.
* @throws Exception
*/
@Test
public void testParameterMap() throws Exception
{
// generated and parsed test
HttpTester request = new HttpTester();
HttpTester response = new HttpTester();
tester.addServlet(TestServletParameterMap.class,"/test2");
// test GET
request.setMethod("POST");
request.setVersion("HTTP/1.0");
request.setHeader("Host","tester");
request.setURI("/context/test2");
String boundary="XyXyXy";
request.setHeader("Content-Type","multipart/form-data; boundary="+boundary);
String content = "--" + boundary + "\r\n"+
"Content-Disposition: form-data; name=\"fileup\"; filename=\"Diplomsko Delo Lektorirano KON&#268;NA.doc\"\r\n"+
"Content-Type: application/octet-stream\r\n\r\n"+
"How now brown cow."+
"\r\n--" + boundary + "\r\n"+
"Content-Disposition: form-data; name=\"strup\""+
"Content-Type: application/octet-stream\r\n\r\n"+
"How now brown cow."+
"\r\n--" + boundary + "--\r\n\r\n";
request.setContent(content);
response.parse(tester.getResponses(request.generate()));
assertTrue(response.getMethod()==null);
assertEquals(HttpServletResponse.SC_OK,response.getStatus());
assertTrue(response.getContent().indexOf("brown cow")>=0);
}
public static class DumpServlet extends HttpServlet
{
private static final long serialVersionUID = 201012011130L;

View File

@ -4,6 +4,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
@ -12,7 +13,6 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import junit.framework.Assert;
import org.eclipse.jetty.client.ContentExchange;
import org.eclipse.jetty.client.HttpClient;
@ -30,21 +30,24 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.junit.After;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
public class ProxyServletTest
{
private Server server;
private Connector connector;
private HttpClient client;
private Server _server;
private Connector _connector;
private HttpClient _client;
public void init(HttpServlet servlet) throws Exception
{
server = new Server();
_server = new Server();
connector = new SelectChannelConnector();
server.addConnector(connector);
_connector = new SelectChannelConnector();
_server.addConnector(_connector);
HandlerCollection handlers = new HandlerCollection();
server.setHandler(handlers);
_server.setHandler(handlers);
ServletContextHandler proxyCtx = new ServletContextHandler(handlers, "/proxy", ServletContextHandler.NO_SESSIONS);
ServletHolder proxyServletHolder = new ServletHolder(new ProxyServlet()
@ -66,25 +69,49 @@ public class ProxyServletTest
handlers.addHandler(proxyCtx);
handlers.addHandler(appCtx);
server.start();
_server.start();
client = new HttpClient();
client.start();
_client = new HttpClient();
_client.start();
}
@After
public void destroy() throws Exception
{
if (client != null)
client.stop();
if (_client != null)
_client.stop();
if (server != null)
if (_server != null)
{
server.stop();
server.join();
_server.stop();
_server.join();
}
}
@Test
public void testXForwardedHostHeader() throws Exception
{
init(new HttpServlet()
{
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
PrintWriter writer = resp.getWriter();
writer.write(req.getHeader("X-Forwarded-Host"));
writer.flush();
}
});
String url = "http://localhost:" + _connector.getLocalPort() + "/proxy/test";
ContentExchange exchange = new ContentExchange();
exchange.setURL(url);
_client.send(exchange);
exchange.waitForDone();
assertThat("Response expected to contain content of X-Forwarded-Host Header from the request",exchange.getResponseContent(),equalTo("localhost:"
+ _connector.getLocalPort()));
}
@Test
public void testBigDownloadWithSlowReader() throws Exception
@ -101,6 +128,8 @@ public class ProxyServletTest
init(new HttpServlet()
{
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
@ -114,7 +143,7 @@ public class ProxyServletTest
}
});
String url = "http://localhost:" + connector.getLocalPort() + "/proxy" + "/test";
String url = "http://localhost:" + _connector.getLocalPort() + "/proxy/test";
ContentExchange exchange = new ContentExchange(true)
{
@Override
@ -134,7 +163,7 @@ public class ProxyServletTest
};
exchange.setURL(url);
long start = System.nanoTime();
client.send(exchange);
_client.send(exchange);
Assert.assertEquals(HttpExchange.STATUS_COMPLETED, exchange.waitForDone());
long elapsed = System.nanoTime() - start;
Assert.assertEquals(HttpStatus.OK_200, exchange.getResponseStatus());

View File

@ -39,6 +39,7 @@ public class GzipTester
{
private Class<? extends GzipFilter> gzipFilterClass = GzipFilter.class;
private String encoding = "ISO8859_1";
private String userAgent = null;
private ServletTester servletTester;
private TestingDir testdir;
@ -64,6 +65,8 @@ public class GzipTester
request.setVersion("HTTP/1.0");
request.setHeader("Host","tester");
request.setHeader("Accept-Encoding","gzip");
if (this.userAgent != null)
request.setHeader("User-Agent", this.userAgent);
request.setURI("/context/" + requestedFilename);
// Issue the request
@ -129,6 +132,8 @@ public class GzipTester
request.setVersion("HTTP/1.0");
request.setHeader("Host","tester");
request.setHeader("Accept-Encoding","gzip");
if (this.userAgent != null)
request.setHeader("User-Agent", this.userAgent);
request.setURI("/context/" + requestedFilename);
// Issue the request
@ -214,6 +219,8 @@ public class GzipTester
request.setVersion("HTTP/1.0");
request.setHeader("Host","tester");
request.setHeader("Accept-Encoding","gzip");
if (this.userAgent != null)
request.setHeader("User-Agent", this.userAgent);
if (filename == null)
request.setURI("/context/");
else
@ -387,6 +394,11 @@ public class GzipTester
{
this.encoding = encoding;
}
public void setUserAgent(String ua)
{
this.userAgent = ua;
}
public void start() throws Exception
{

60
jetty-spdy/pom.xml Normal file
View File

@ -0,0 +1,60 @@
<?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">
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
<version>7.6.2-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.eclipse.jetty.spdy</groupId>
<artifactId>spdy-parent</artifactId>
<packaging>pom</packaging>
<name>Jetty :: SPDY :: Parent</name>
<modules>
<module>spdy-core</module>
<module>spdy-jetty</module>
<module>spdy-jetty-http</module>
<module>spdy-jetty-http-webapp</module>
</modules>
<build>
<plugins>
<plugin>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>require-jdk7</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireJavaVersion>
<version>[1.7,)</version>
</requireJavaVersion>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-pmd-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,32 @@
<?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">
<parent>
<groupId>org.eclipse.jetty.spdy</groupId>
<artifactId>spdy-parent</artifactId>
<version>7.6.2-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spdy-core</artifactId>
<name>Jetty :: SPDY :: Core</name>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j-version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy;
import java.nio.ByteBuffer;
/**
* <p>A {@link ByteBuffer} pool.</p>
* <p>Acquired buffers may be {@link #release(ByteBuffer) released} but they do not need to;
* if they are released, they may be recycled and reused, otherwise they will be garbage
* collected as usual.</p>
*/
public interface ByteBufferPool
{
/**
* <p>Requests a {@link ByteBuffer} of the given size.</p>
* <p>The returned buffer may have a bigger capacity than the size being
* requested but it will have the limit set to the given size.</p>
*
* @param size the size of the buffer
* @param direct whether the buffer must be direct or not
* @return the requested buffer
* @see #release(ByteBuffer)
*/
public ByteBuffer acquire(int size, boolean direct);
/**
* <p>Returns a {@link ByteBuffer}, usually obtained with {@link #acquire(int, boolean)}
* (but not necessarily), making it available for recycling and reuse.</p>
*
* @param buffer the buffer to return
* @see #acquire(int, boolean)
*/
public void release(ByteBuffer buffer);
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy;
import org.eclipse.jetty.spdy.api.SPDY;
public class CompressionDictionary
{
private static final byte[] DICTIONARY_V2 = ("" +
"optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-" +
"languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi" +
"f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser" +
"-agent10010120020120220320420520630030130230330430530630740040140240340440" +
"5406407408409410411412413414415416417500501502503504505accept-rangesageeta" +
"glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic" +
"ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran" +
"sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati" +
"oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo" +
"ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe" +
"pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic" +
"ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1" +
".1statusversionurl" +
// Must be NUL terminated
"\u0000").getBytes();
private static final byte[] DICTIONARY_V3 = ("" +
"\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004head\u0000\u0000\u0000\u0004post" +
"\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006delete\u0000\u0000\u0000\u0005trace" +
"\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000\u000Eaccept-charset" +
"\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Faccept-language" +
"\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000\u0000\u0000\u0005allow" +
"\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-control" +
"\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000\u0000\u0010content-encoding" +
"\u0000\u0000\u0000\u0010content-language\u0000\u0000\u0000\u000Econtent-length" +
"\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000\u000Bcontent-md5" +
"\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type\u0000\u0000\u0000\u0004date" +
"\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expect\u0000\u0000\u0000\u0007expires" +
"\u0000\u0000\u0000\u0004from\u0000\u0000\u0000\u0004host\u0000\u0000\u0000\bif-match" +
"\u0000\u0000\u0000\u0011if-modified-since\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range" +
"\u0000\u0000\u0000\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified" +
"\u0000\u0000\u0000\blocation\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma" +
"\u0000\u0000\u0000\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization" +
"\u0000\u0000\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after" +
"\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trailer" +
"\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000\u0000\u0000\nuser-agent" +
"\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via\u0000\u0000\u0000\u0007warning" +
"\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000\u0000\u0006method\u0000\u0000\u0000\u0003get" +
"\u0000\u0000\u0000\u0006status\u0000\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version" +
"\u0000\u0000\u0000\bHTTP/1.1\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public" +
"\u0000\u0000\u0000\nset-cookie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin" +
"100101201202205206300302303304305306307402405406407408409410411412413414415416417502504505" +
"203 Non-Authoritative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized" +
"403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Unavailable" +
"Jan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Thu, Fri, Sat, Sun, GMT" +
"chunked,text/html,image/png,image/jpg,image/gif,application/xml,application/xhtml+xml,text/plain," +
"text/javascript,publicprivatemax-age=gzip,deflate,sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.")
.getBytes();
public static byte[] get(short version)
{
switch (version)
{
case SPDY.V2:
return DICTIONARY_V2;
case SPDY.V3:
return DICTIONARY_V3;
default:
throw new IllegalStateException();
}
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy;
import java.util.zip.ZipException;
public interface CompressionFactory
{
public Compressor newCompressor();
public Decompressor newDecompressor();
public interface Compressor
{
public void setInput(byte[] input);
public void setDictionary(byte[] dictionary);
public int compress(byte[] output);
}
public interface Decompressor
{
public void setDictionary(byte[] dictionary);
public void setInput(byte[] input);
public int decompress(byte[] output) throws ZipException;
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.api.Handler;
public interface Controller<T>
{
public int write(ByteBuffer buffer, Handler<T> handler, T context);
public void close(boolean onlyOutput);
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.spdy.api.DataInfo;
import org.eclipse.jetty.spdy.api.Handler;
import org.eclipse.jetty.spdy.api.Session;
import org.eclipse.jetty.spdy.frames.ControlFrame;
public interface ISession extends Session
{
/**
* <p>Initiates the flush of data to the other peer.</p>
* <p>Note that the flush may do nothing if, for example, there is nothing to flush, or
* if the data to be flushed belong to streams that have their flow-control stalled.</p>
*/
public void flush();
public <C> void control(IStream stream, ControlFrame frame, long timeout, TimeUnit unit, Handler<C> handler, C context);
public <C> void data(IStream stream, DataInfo dataInfo, long timeout, TimeUnit unit, Handler<C> handler, C context);
public int getWindowSize();
}

View File

@ -0,0 +1,92 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.api.SessionFrameListener;
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.DataFrame;
/**
* <p>The internal interface that represents a stream.</p>
* <p>{@link IStream} contains additional methods used by a SPDY
* implementation (and not by an application).</p>
*/
public interface IStream extends Stream
{
/**
* <p>Senders of data frames need to know the current window size
* to determine whether they can send more data.</p>
*
* @return the current window size for this stream.
* @see #updateWindowSize(int)
*/
public int getWindowSize();
/**
* <p>Updates the window size for this stream by the given amount,
* that can be positive or negative.</p>
* <p>Senders and recipients of data frames update the window size,
* respectively, with negative values and positive values.</p>
*
* @param delta the signed amount the window size needs to be updated
* @see #getWindowSize()
*/
public void updateWindowSize(int delta);
/**
* @param listener the stream frame listener associated to this stream
* as returned by {@link SessionFrameListener#onSyn(Stream, SynInfo)}
*/
public void setStreamFrameListener(StreamFrameListener listener);
/**
* <p>A stream can be open, {@link #isHalfClosed() half closed} or
* {@link #isClosed() closed} and this method updates the close state
* of this stream.</p>
* <p>If the stream is open, calling this method with a value of true
* puts the stream into half closed state.</p>
* <p>If the stream is half closed, calling this method with a value
* of true puts the stream into closed state.</p>
*
* @param close whether the close state should be updated
*/
public void updateCloseState(boolean close);
/**
* <p>Processes the given control frame,
* for example by updating the stream's state or by calling listeners.</p>
*
* @param frame the control frame to process
* @see #process(DataFrame, ByteBuffer)
*/
public void process(ControlFrame frame);
/**
* <p>Processes the give data frame along with the given byte buffer,
* for example by updating the stream's state or by calling listeners.</p>
*
* @param frame the data frame to process
* @param data the byte buffer to process
* @see #process(ControlFrame)
*/
public void process(DataFrame frame, ByteBuffer data);
}

View File

@ -0,0 +1,22 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy;
public interface IdleListener
{
public void onIdle(boolean idle);
}

View File

@ -0,0 +1,94 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.spdy.api.Handler;
/**
* <p>A {@link Promise} is a {@link Future} that allows a result or a failure to be set,
* so that the {@link Future} will be {@link #isDone() done}.</p>
*
* @param <T> the type of the result object
*/
public class Promise<T> implements Handler<T>, Future<T>
{
private final CountDownLatch latch = new CountDownLatch(1);
private boolean cancelled;
private Throwable failure;
private T promise;
@Override
public void completed(T result)
{
this.promise = result;
latch.countDown();
}
public void failed(Throwable x)
{
this.failure = x;
latch.countDown();
}
@Override
public boolean cancel(boolean mayInterruptIfRunning)
{
cancelled = true;
latch.countDown();
return true;
}
@Override
public boolean isCancelled()
{
return cancelled;
}
@Override
public boolean isDone()
{
return cancelled || latch.getCount() == 0;
}
@Override
public T get() throws InterruptedException, ExecutionException
{
latch.await();
return result();
}
@Override
public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
{
latch.await(timeout, unit);
return result();
}
private T result() throws ExecutionException
{
Throwable failure = this.failure;
if (failure != null)
throw new ExecutionException(failure);
return promise;
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy;
import org.eclipse.jetty.spdy.api.SessionStatus;
public class SessionException extends RuntimeException
{
private final SessionStatus sessionStatus;
public SessionException(SessionStatus sessionStatus)
{
this.sessionStatus = sessionStatus;
}
public SessionException(SessionStatus sessionStatus, String message)
{
super(message);
this.sessionStatus = sessionStatus;
}
public SessionException(SessionStatus sessionStatus, Throwable cause)
{
super(cause);
this.sessionStatus = sessionStatus;
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy;
import java.nio.ByteBuffer;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
public class StandardByteBufferPool implements ByteBufferPool
{
private final ConcurrentMap<Integer, Queue<ByteBuffer>> directBuffers = new ConcurrentHashMap<>();
private final ConcurrentMap<Integer, Queue<ByteBuffer>> heapBuffers = new ConcurrentHashMap<>();
private final int factor;
public StandardByteBufferPool()
{
this(1024);
}
public StandardByteBufferPool(int factor)
{
this.factor = factor;
}
public ByteBuffer acquire(int size, boolean direct)
{
int bucket = bucketFor(size);
ConcurrentMap<Integer, Queue<ByteBuffer>> buffers = buffersFor(direct);
ByteBuffer result = null;
Queue<ByteBuffer> byteBuffers = buffers.get(bucket);
if (byteBuffers != null)
result = byteBuffers.poll();
if (result == null)
{
int capacity = bucket * factor;
result = direct ? ByteBuffer.allocateDirect(capacity) : ByteBuffer.allocate(capacity);
}
result.clear();
result.limit(size);
return result;
}
public void release(ByteBuffer buffer)
{
int bucket = bucketFor(buffer.capacity());
ConcurrentMap<Integer, Queue<ByteBuffer>> buffers = buffersFor(buffer.isDirect());
// Avoid to create a new queue every time, just to be discarded immediately
Queue<ByteBuffer> byteBuffers = buffers.get(bucket);
if (byteBuffers == null)
{
byteBuffers = new ConcurrentLinkedQueue<>();
Queue<ByteBuffer> existing = buffers.putIfAbsent(bucket, byteBuffers);
if (existing != null)
byteBuffers = existing;
}
buffer.clear();
byteBuffers.offer(buffer);
}
public void clear()
{
directBuffers.clear();
heapBuffers.clear();
}
private int bucketFor(int size)
{
int bucket = size / factor;
if (size % factor > 0)
++bucket;
return bucket;
}
private ConcurrentMap<Integer, Queue<ByteBuffer>> buffersFor(boolean direct)
{
return direct ? directBuffers : heapBuffers;
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import java.util.zip.ZipException;
public class StandardCompressionFactory implements CompressionFactory
{
@Override
public Compressor newCompressor()
{
return new StandardCompressor();
}
@Override
public Decompressor newDecompressor()
{
return new StandardDecompressor();
}
public static class StandardCompressor implements Compressor
{
private final Deflater deflater = new Deflater();
@Override
public void setInput(byte[] input)
{
deflater.setInput(input);
}
@Override
public void setDictionary(byte[] dictionary)
{
deflater.setDictionary(dictionary);
}
@Override
public int compress(byte[] output)
{
return deflater.deflate(output, 0, output.length, Deflater.SYNC_FLUSH);
}
}
public static class StandardDecompressor implements CompressionFactory.Decompressor
{
private final Inflater inflater = new Inflater();
@Override
public void setDictionary(byte[] dictionary)
{
inflater.setDictionary(dictionary);
}
@Override
public void setInput(byte[] input)
{
inflater.setInput(input);
}
@Override
public int decompress(byte[] output) throws ZipException
{
try
{
return inflater.inflate(output);
}
catch (DataFormatException x)
{
throw (ZipException)new ZipException().initCause(x);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,337 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.spdy.api.ByteBufferDataInfo;
import org.eclipse.jetty.spdy.api.DataInfo;
import org.eclipse.jetty.spdy.api.Handler;
import org.eclipse.jetty.spdy.api.HeadersInfo;
import org.eclipse.jetty.spdy.api.ReplyInfo;
import org.eclipse.jetty.spdy.api.RstInfo;
import org.eclipse.jetty.spdy.api.Session;
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.StreamStatus;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.DataFrame;
import org.eclipse.jetty.spdy.frames.HeadersFrame;
import org.eclipse.jetty.spdy.frames.SynReplyFrame;
import org.eclipse.jetty.spdy.frames.SynStreamFrame;
import org.eclipse.jetty.spdy.frames.WindowUpdateFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class StandardStream implements IStream
{
private static final Logger logger = LoggerFactory.getLogger(Stream.class);
private final Map<String, Object> attributes = new ConcurrentHashMap<>();
private final SynStreamFrame frame;
private final ISession session;
private final AtomicInteger windowSize;
private volatile StreamFrameListener listener;
private volatile boolean opened;
private volatile boolean halfClosed;
private volatile boolean closed;
public StandardStream(SynStreamFrame frame, ISession session, int windowSize)
{
this.frame = frame;
this.session = session;
this.windowSize = new AtomicInteger(windowSize);
this.halfClosed = frame.isClose();
}
@Override
public int getId()
{
return frame.getStreamId();
}
@Override
public byte getPriority()
{
return frame.getPriority();
}
@Override
public int getWindowSize()
{
return windowSize.get();
}
@Override
public void updateWindowSize(int delta)
{
int size = windowSize.addAndGet(delta);
logger.debug("Updated window size by {}, new window size {}", delta, size);
}
@Override
public Session getSession()
{
return session;
}
public boolean isHalfClosed()
{
return halfClosed;
}
@Override
public Object getAttribute(String key)
{
return attributes.get(key);
}
@Override
public void setAttribute(String key, Object value)
{
attributes.put(key, value);
}
@Override
public Object removeAttribute(String key)
{
return attributes.remove(key);
}
@Override
public void setStreamFrameListener(StreamFrameListener listener)
{
this.listener = listener;
}
@Override
public void updateCloseState(boolean close)
{
if (close)
{
if (isHalfClosed())
closed = true;
else
halfClosed = true;
}
}
@Override
public void process(ControlFrame frame)
{
switch (frame.getType())
{
case SYN_STREAM:
{
opened = true;
break;
}
case SYN_REPLY:
{
opened = true;
SynReplyFrame synReply = (SynReplyFrame)frame;
updateCloseState(synReply.isClose());
ReplyInfo replyInfo = new ReplyInfo(synReply.getHeaders(), synReply.isClose());
notifyOnReply(replyInfo);
break;
}
case HEADERS:
{
HeadersFrame headers = (HeadersFrame)frame;
updateCloseState(headers.isClose());
HeadersInfo headersInfo = new HeadersInfo(headers.getHeaders(), headers.isClose(), headers.isResetCompression());
notifyOnHeaders(headersInfo);
break;
}
case WINDOW_UPDATE:
{
WindowUpdateFrame windowUpdate = (WindowUpdateFrame)frame;
updateWindowSize(windowUpdate.getWindowDelta());
break;
}
case RST_STREAM:
{
// TODO:
break;
}
default:
{
throw new IllegalStateException();
}
}
session.flush();
}
@Override
public void process(DataFrame frame, ByteBuffer data)
{
if (!opened)
{
session.rst(new RstInfo(getId(), StreamStatus.PROTOCOL_ERROR));
return;
}
updateCloseState(frame.isClose());
ByteBufferDataInfo dataInfo = new ByteBufferDataInfo(data, frame.isClose(), frame.isCompress())
{
@Override
public void consume(int delta)
{
super.consume(delta);
// This is the algorithm for flow control.
// This method may be called multiple times with delta=1, but we only send a window
// update when the whole dataInfo has been consumed.
// Other policies may be to send window updates when consumed() is greater than
// a certain threshold, etc. but for now the policy is not pluggable for simplicity.
// Note that the frequency of window updates depends on the read buffer, that
// should not be too smaller than the window size to avoid frequent window updates.
// Therefore, a pluggable policy should be able to modify the read buffer capacity.
if (consumed() == length() && !isClosed())
windowUpdate(length());
}
};
notifyOnData(dataInfo);
session.flush();
}
private void windowUpdate(int delta)
{
if (delta > 0)
{
WindowUpdateFrame windowUpdateFrame = new WindowUpdateFrame(session.getVersion(), getId(), delta);
session.control(this, windowUpdateFrame, 0, TimeUnit.MILLISECONDS, null, null);
}
}
private void notifyOnReply(ReplyInfo replyInfo)
{
final StreamFrameListener listener = this.listener;
try
{
if (listener != null)
{
logger.debug("Invoking reply callback with {} on listener {}", replyInfo, listener);
listener.onReply(this, replyInfo);
}
}
catch (Exception x)
{
logger.info("Exception while notifying listener " + listener, x);
}
}
private void notifyOnHeaders(HeadersInfo headersInfo)
{
final StreamFrameListener listener = this.listener;
try
{
if (listener != null)
{
logger.debug("Invoking headers callback with {} on listener {}", frame, listener);
listener.onHeaders(this, headersInfo);
}
}
catch (Exception x)
{
logger.info("Exception while notifying listener " + listener, x);
}
}
private void notifyOnData(DataInfo dataInfo)
{
final StreamFrameListener listener = this.listener;
try
{
if (listener != null)
{
logger.debug("Invoking data callback with {} on listener {}", dataInfo, listener);
listener.onData(this, dataInfo);
logger.debug("Invoked data callback with {} on listener {}", dataInfo, listener);
}
}
catch (Exception x)
{
logger.info("Exception while notifying listener " + listener, x);
}
}
@Override
public Future<Void> reply(ReplyInfo replyInfo)
{
Promise<Void> result = new Promise<>();
reply(replyInfo, 0, TimeUnit.MILLISECONDS, result);
return result;
}
@Override
public void reply(ReplyInfo replyInfo, long timeout, TimeUnit unit, Handler<Void> handler)
{
updateCloseState(replyInfo.isClose());
SynReplyFrame frame = new SynReplyFrame(session.getVersion(), replyInfo.getFlags(), getId(), replyInfo.getHeaders());
session.control(this, frame, timeout, unit, handler, null);
}
@Override
public Future<Void> data(DataInfo dataInfo)
{
Promise<Void> result = new Promise<>();
data(dataInfo, 0, TimeUnit.MILLISECONDS, result);
return result;
}
@Override
public void data(DataInfo dataInfo, long timeout, TimeUnit unit, Handler<Void> handler)
{
// Cannot update the close state here, because the data that we send may
// be flow controlled, so we need the stream to update the window size.
session.data(this, dataInfo, timeout, unit, handler, null);
}
@Override
public Future<Void> headers(HeadersInfo headersInfo)
{
Promise<Void> result = new Promise<>();
headers(headersInfo, 0, TimeUnit.MILLISECONDS, result);
return result;
}
@Override
public void headers(HeadersInfo headersInfo, long timeout, TimeUnit unit, Handler<Void> handler)
{
updateCloseState(headersInfo.isClose());
HeadersFrame frame = new HeadersFrame(session.getVersion(), headersInfo.getFlags(), getId(), headersInfo.getHeaders());
session.control(this, frame, timeout, unit, handler, null);
}
@Override
public boolean isClosed()
{
return closed;
}
@Override
public String toString()
{
return String.format("stream=%d v%d closed=%s", getId(), session.getVersion(), isClosed() ? "true" : isHalfClosed() ? "half" : "false");
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy;
import org.eclipse.jetty.spdy.api.StreamStatus;
public class StreamException extends RuntimeException
{
private final int streamId;
private final StreamStatus streamStatus;
public StreamException(int streamId, StreamStatus streamStatus)
{
this.streamId = streamId;
this.streamStatus = streamStatus;
}
public StreamException(int streamId, StreamStatus streamStatus, String message)
{
super(message);
this.streamId = streamId;
this.streamStatus = streamStatus;
}
public StreamException(int streamId, StreamStatus streamStatus, Throwable x)
{
super(x);
this.streamId = streamId;
this.streamStatus = streamStatus;
}
public int getStreamId()
{
return streamId;
}
public StreamStatus getStreamStatus()
{
return streamStatus;
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
import java.nio.ByteBuffer;
/**
* <p>Specialized {@link DataInfo} for {@link ByteBuffer} content.</p>
*/
public class ByteBufferDataInfo extends DataInfo
{
private final ByteBuffer buffer;
private final int length;
public ByteBufferDataInfo(ByteBuffer buffer, boolean close)
{
this(buffer, close, false);
}
public ByteBufferDataInfo(ByteBuffer buffer, boolean close, boolean compress)
{
super(close, compress);
this.buffer = buffer;
this.length = buffer.remaining();
}
@Override
public int length()
{
return length;
}
@Override
public int available()
{
return buffer.remaining();
}
@Override
public int readInto(ByteBuffer output)
{
int space = output.remaining();
if (available() > space)
{
int limit = buffer.limit();
buffer.limit(buffer.position() + space);
output.put(buffer);
buffer.limit(limit);
}
else
{
space = buffer.remaining();
output.put(buffer);
}
return space;
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
import java.nio.ByteBuffer;
/**
* <p>Specialized {@link DataInfo} for byte array content.</p>
*/
public class BytesDataInfo extends DataInfo
{
private byte[] bytes;
private int offset;
public BytesDataInfo(byte[] bytes, boolean close)
{
this(bytes, close, false);
}
public BytesDataInfo(byte[] bytes, boolean close, boolean compress)
{
super(close, compress);
this.bytes = bytes;
}
@Override
public int length()
{
return bytes.length;
}
@Override
public int available()
{
return length() - offset;
}
@Override
public int readInto(ByteBuffer output)
{
int space = output.remaining();
int length = Math.min(available(), space);
output.put(bytes, offset, length);
offset += length;
return length;
}
}

View File

@ -0,0 +1,249 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.concurrent.atomic.AtomicInteger;
/**
* <p>A container for DATA frames metadata and content bytes.</p>
* <p>Specialized subclasses (like {@link StringDataInfo}) may be used by applications
* to send specific types of content.</p>
* <p>Applications may send multiple instances of {@link DataInfo}, usually of the same
* type, via {@link Stream#data(DataInfo)}. The last instance must have the
* {@link #isClose() close flag} set, so that the client knows that no more content is
* expected.</p>
* <p>Receivers of {@link DataInfo} via {@link StreamFrameListener#onData(Stream, DataInfo)}
* have two different APIs to read the data content bytes: a {@link #readInto(ByteBuffer) read}
* API that does not interact with flow control, and a {@link #consumeInto(ByteBuffer) drain}
* API that interacts with flow control.</p>
* <p>Flow control is defined so that when the sender wants to sends a number of bytes larger
* than the {@link Settings.ID#INITIAL_WINDOW_SIZE} value, it will stop sending as soon as it
* has sent a number of bytes equal to the window size. The receiver has to <em>consume</em>
* the data bytes that it received in order to tell the sender to send more bytes.</p>
* <p>Consuming the data bytes can be done only via {@link #consumeInto(ByteBuffer)} or by a combination
* of {@link #readInto(ByteBuffer)} and {@link #consume(int)} (possibly at different times).</p>
*/
public abstract class DataInfo
{
/**
* <p>Flag that indicates that this {@link DataInfo} is the last frame in the stream.</p>
*
* @see #isClose()
* @see #getFlags()
*/
public final static byte FLAG_CLOSE = 1;
/**
* <p>Flag that indicates that this {@link DataInfo}'s data is compressed.</p>
*
* @see #isCompress()
* @see #getFlags()
*/
public final static byte FLAG_COMPRESS = 2;
private final AtomicInteger consumed = new AtomicInteger();
private boolean close;
private boolean compress;
/**
* <p>Creates a new {@link DataInfo} with the given close flag and no compression flag.</p>
*
* @param close the value of the close flag
*/
public DataInfo(boolean close)
{
setClose(close);
}
/**
* <p>Creates a new {@link DataInfo} with the given close flag and given compression flag.</p>
*
* @param close the close flag
* @param compress the compress flag
*/
public DataInfo(boolean close, boolean compress)
{
setClose(close);
setCompress(compress);
}
/**
* @return the value of the compress flag
* @see #setCompress(boolean)
*/
public boolean isCompress()
{
return compress;
}
/**
* @param compress the value of the compress flag
* @see #isCompress()
*/
public void setCompress(boolean compress)
{
this.compress = compress;
}
/**
* @return the value of the close flag
* @see #setClose(boolean)
*/
public boolean isClose()
{
return close;
}
/**
* @param close the value of the close flag
* @see #isClose()
*/
public void setClose(boolean close)
{
this.close = close;
}
/**
* @return the close and compress flags as integer
* @see #FLAG_CLOSE
* @see #FLAG_COMPRESS
*/
public byte getFlags()
{
byte flags = isClose() ? FLAG_CLOSE : 0;
flags |= isCompress() ? FLAG_COMPRESS : 0;
return flags;
}
/**
* @return the total number of content bytes
* @see #available()
*/
public abstract int length();
/**
* <p>Returns the available content bytes that can be read via {@link #readInto(ByteBuffer)}.</p>
* <p>Each invocation to {@link #readInto(ByteBuffer)} modifies the value returned by this method,
* until no more content bytes are available.</p>
*
* @return the available content bytes
* @see #readInto(ByteBuffer)
*/
public abstract int available();
/**
* <p>Copies the content bytes of this {@link DataInfo} into the given {@link ByteBuffer}.</p>
* <p>If the given {@link ByteBuffer} cannot contain the whole content of this {@link DataInfo}
* then after the read {@link #available()} will return a positive value, and further content
* may be retrieved by invoking again this method with a new output buffer.</p>
*
* @param output the {@link ByteBuffer} to copy to bytes into
* @return the number of bytes copied
* @see #available()
* @see #consumeInto(ByteBuffer)
*/
public abstract int readInto(ByteBuffer output);
/**
* <p>Reads and consumes the content bytes of this {@link DataInfo} into the given {@link ByteBuffer}.</p>
*
* @param output the {@link ByteBuffer} to copy to bytes into
* @return the number of bytes copied
* @see #consume(int)
*/
public int consumeInto(ByteBuffer output)
{
int read = readInto(output);
consume(read);
return read;
}
/**
* <p>Consumes the given number of bytes from this {@link DataInfo}.</p>
*
* @param delta the number of bytes consumed
*/
public void consume(int delta)
{
if (delta < 0)
throw new IllegalArgumentException();
int read = length() - available();
int newConsumed = consumed() + delta;
// if (newConsumed > read)
// throw new IllegalStateException("Consuming without reading: consumed " + newConsumed + " but only read " + read);
consumed.addAndGet(delta);
}
/**
* @return the number of bytes consumed
*/
public int consumed()
{
return consumed.get();
}
/**
*
* @param charset the charset used to convert the bytes
* @param consume whether to consume the content
* @return a String with the content of this {@link DataInfo}
*/
public String asString(String charset, boolean consume)
{
ByteBuffer buffer = asByteBuffer(consume);
return Charset.forName(charset).decode(buffer).toString();
}
/**
* @return a byte array with the content of this {@link DataInfo}
* @param consume whether to consume the content
*/
public byte[] asBytes(boolean consume)
{
ByteBuffer buffer = asByteBuffer(consume);
byte[] result = new byte[buffer.remaining()];
buffer.get(result);
return result;
}
/**
* @return a {@link ByteBuffer} with the content of this {@link DataInfo}
* @param consume whether to consume the content
*/
public ByteBuffer asByteBuffer(boolean consume)
{
ByteBuffer buffer = allocate(available());
if (consume)
consumeInto(buffer);
else
readInto(buffer);
buffer.flip();
return buffer;
}
protected ByteBuffer allocate(int size)
{
return ByteBuffer.allocate(size);
}
@Override
public String toString()
{
return String.format("DATA @%x available=%d consumed=%d close=%b compress=%b", hashCode(), available(), consumed(), isClose(), isCompress());
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
/**
* <p>A container for GOAWAY frames metadata: the last good stream id and
* the session status.</p>
*/
public class GoAwayInfo
{
private final int lastStreamId;
private final SessionStatus sessionStatus;
/**
* <p>Creates a new {@link GoAwayInfo} with the given last good stream id and session status</p>
*
* @param lastStreamId the last good stream id
* @param sessionStatus the session status
*/
public GoAwayInfo(int lastStreamId, SessionStatus sessionStatus)
{
this.lastStreamId = lastStreamId;
this.sessionStatus = sessionStatus;
}
/**
* @return the last good stream id
*/
public int getLastStreamId()
{
return lastStreamId;
}
/**
* @return the session status
*/
public SessionStatus getSessionStatus()
{
return sessionStatus;
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
/**
* <p>A callback abstraction that handles completed/failed events of asynchronous operations.</p>
* <p>Instances of this class capture a context that is made available on the completion callback.</p>
*
* @param <C> the type of the context object
*/
public interface Handler<C>
{
/**
* <p>Callback invoked when the operation completes.</p>
*
* @param context the context
* @see #failed(Throwable)
*/
public abstract void completed(C context);
/**
* <p>Callback invoked when the operation fails.</p>
*
* @param x the reason for the operation failure
*/
public void failed(Throwable x);
/**
* <p>Empty implementation of {@link Handler}</p>
*
* @param <C> the type of the context object
*/
public static class Adapter<C> implements Handler<C>
{
@Override
public void completed(C context)
{
}
@Override
public void failed(Throwable x)
{
throw new SPDYException(x);
}
}
}

View File

@ -0,0 +1,284 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* <p>A container for name/value pairs, known as headers.</p>
* <p>A {@link Header} is composed of a case-insensitive name string and
* of a case-sensitive set of value strings.</p>
* <p>The implementation of this class is not thread safe.</p>
*/
public class Headers implements Iterable<Headers.Header>
{
private final Map<String, Header> headers;
/**
* <p>Creates an empty modifiable {@link Headers} instance.</p>
* @see #Headers(Headers, boolean)
*/
public Headers()
{
headers = new LinkedHashMap<>();
}
/**
* <p>Creates a {@link Headers} instance by copying the headers from the given
* {@link Headers} and making it (im)mutable depending on the given {@code immutable} parameter</p>
*
* @param original the {@link Headers} to copy headers from
* @param immutable whether this instance is immutable
*/
public Headers(Headers original, boolean immutable)
{
Map<String, Header> copy = new LinkedHashMap<>();
copy.putAll(original.headers);
headers = immutable ? Collections.unmodifiableMap(copy) : copy;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Headers that = (Headers)obj;
return headers.equals(that.headers);
}
@Override
public int hashCode()
{
return headers.hashCode();
}
/**
* @return a set of header names
*/
public Set<String> names()
{
Set<String> result = new LinkedHashSet<>();
for (Header header : headers.values())
result.add(header.name);
return result;
}
/**
* @param name the header name
* @return the {@link Header} with the given name, or null if no such header exists
*/
public Header get(String name)
{
return headers.get(name.trim().toLowerCase());
}
/**
* <p>Inserts or replaces the given name/value pair as a single-valued {@link Header}.</p>
*
* @param name the header name
* @param value the header value
*/
public void put(String name, String value)
{
name = name.trim();
Header header = new Header(name, value.trim());
headers.put(name.toLowerCase(), header);
}
/**
* <p>Inserts or replaces the given {@link Header}, mapped to the {@link Header#name() header's name}</p>
*
* @param header the header to add
*/
public void put(Header header)
{
headers.put(header.name().toLowerCase(), header);
}
/**
* <p>Adds the given value to a header with the given name, creating a {@link Header} is none exists
* for the given name.</p>
*
* @param name the header name
* @param value the header value to add
*/
public void add(String name, String value)
{
name = name.trim();
Header header = headers.get(name.toLowerCase());
if (header == null)
{
header = new Header(name, value.trim());
headers.put(name.toLowerCase(), header);
}
else
{
header = new Header(header.name(), header.value() + "," + value.trim());
headers.put(name.toLowerCase(), header);
}
}
/**
* <p>Removes the {@link Header} with the given name</p>
*
* @param name the name of the header to remove
* @return the removed header, or null if no such header existed
*/
public Header remove(String name)
{
name = name.trim();
return headers.remove(name.toLowerCase());
}
/**
* <p>Empties this {@link Headers} instance from all headers</p>
* @see #isEmpty()
*/
public void clear()
{
headers.clear();
}
/**
* @return whether this {@link Headers} instance is empty
*/
public boolean isEmpty()
{
return headers.isEmpty();
}
/**
* @return the number of headers
*/
public int size()
{
return headers.size();
}
/**
* @return an iterator over the {@link Header} present in this instance
*/
@Override
public Iterator<Header> iterator()
{
return headers.values().iterator();
}
@Override
public String toString()
{
return headers.toString();
}
/**
* <p>A named list of string values.</p>
* <p>The name is case-sensitive and there must be at least one value.</p>
*/
public static class Header
{
private final String name;
private final String[] values;
private Header(String name, String value, String... values)
{
this.name = name;
this.values = new String[values.length + 1];
this.values[0] = value;
if (values.length > 0)
System.arraycopy(values, 0, this.values, 1, values.length);
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Header that = (Header)obj;
return name.equals(that.name) && Arrays.equals(values, that.values);
}
@Override
public int hashCode()
{
int result = name.hashCode();
result = 31 * result + Arrays.hashCode(values);
return result;
}
/**
* @return the header's name
*/
public String name()
{
return name;
}
/**
* @return the first header's value
*/
public String value()
{
return values[0];
}
/**
* <p>Attempts to convert the result of {@link #value()} to an integer,
* returning it if the conversion is successful; returns null if the
* result of {@link #value()} is null.</p>
*
* @return the result of {@link #value()} converted to an integer, or null
* @throws NumberFormatException if the conversion fails
*/
public Integer valueAsInt()
{
final String value = value();
return value == null ? null : Integer.valueOf(value);
}
/**
* @return the header's values
*/
public String[] values()
{
return values;
}
/**
* @return whether the header has multiple values
*/
public boolean hasMultipleValues()
{
return values.length > 1;
}
@Override
public String toString()
{
return Arrays.toString(values);
}
}
}

View File

@ -0,0 +1,111 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
/**
* <p>A container for HEADERS frame metadata and headers.</p>
*/
public class HeadersInfo
{
/**
* <p>Flag that indicates that this {@link HeadersInfo} is the last frame in the stream.</p>
*
* @see #isClose()
* @see #getFlags()
*/
public static final byte FLAG_CLOSE = 1;
/**
* <p>Flag that indicates that the compression of the stream must be reset.</p>
*
* @see #isResetCompression()
* @see #getFlags()
*/
public static final byte FLAG_RESET_COMPRESSION = 2;
private final boolean close;
private final boolean resetCompression;
private final Headers headers;
/**
* <p>Creates a new {@link HeadersInfo} instance with the given headers,
* the given close flag and no reset compression flag</p>
*
* @param headers the {@link Headers}
* @param close the value of the close flag
*/
public HeadersInfo(Headers headers, boolean close)
{
this(headers, close, false);
}
/**
* <p>Creates a new {@link HeadersInfo} instance with the given headers,
* the given close flag and the given reset compression flag</p>
*
* @param headers the {@link Headers}
* @param close the value of the close flag
* @param resetCompression the value of the reset compression flag
*/
public HeadersInfo(Headers headers, boolean close, boolean resetCompression)
{
this.headers = headers;
this.close = close;
this.resetCompression = resetCompression;
}
/**
* @return the value of the close flag
*/
public boolean isClose()
{
return close;
}
/**
* @return the value of the reset compression flag
*/
public boolean isResetCompression()
{
return resetCompression;
}
/**
* @return the {@link Headers}
*/
public Headers getHeaders()
{
return headers;
}
/**
* @return the close and reset compression flags as integer
* @see #FLAG_CLOSE
* @see #FLAG_RESET_COMPRESSION
*/
public byte getFlags()
{
byte flags = isClose() ? FLAG_CLOSE : 0;
flags += isResetCompression() ? FLAG_RESET_COMPRESSION : 0;
return flags;
}
@Override
public String toString()
{
return String.format("HEADER close=%b %s", close, headers);
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
/**
* <p>A container for PING frames data.</p>
*/
public class PingInfo
{
private final int pingId;
/**
* <p>Creates a {@link PingInfo} with the given ping id</p>
* @param pingId the ping id
*/
public PingInfo(int pingId)
{
this.pingId = pingId;
}
/**
* @return the ping id
*/
public int getPingId()
{
return pingId;
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
/**
* <p>A container for SYN_REPLY frames metadata and headers.</p>
*/
public class ReplyInfo
{
/**
* <p>Flag that indicates that this {@link ReplyInfo} is the last frame in the stream.</p>
*
* @see #isClose()
* @see #getFlags()
*/
public static final byte FLAG_CLOSE = 1;
private final Headers headers;
private final boolean close;
/**
* <p>Creates a new {@link ReplyInfo} instance with empty headers and the given close flag.</p>
*
* @param close the value of the close flag
*/
public ReplyInfo(boolean close)
{
this(new Headers(), close);
}
/**
* <p>Creates a {@link ReplyInfo} instance with the given headers and the given close flag.</p>
*
* @param headers the {@link Headers}
* @param close the value of the close flag
*/
public ReplyInfo(Headers headers, boolean close)
{
this.headers = headers;
this.close = close;
}
/**
* @return the {@link Headers}
*/
public Headers getHeaders()
{
return headers;
}
/**
* @return the value of the close flag
*/
public boolean isClose()
{
return close;
}
/**
* @return the close and reset compression flags as integer
* @see #FLAG_CLOSE
*/
public byte getFlags()
{
return isClose() ? FLAG_CLOSE : 0;
}
@Override
public String toString()
{
return String.format("REPLY close=%b %s", close, headers);
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
/**
* <p>A container for RST_STREAM frames data: the stream id and the stream status.</p>
*/
public class RstInfo
{
private final int streamId;
private final StreamStatus streamStatus;
/**
* <p>Creates a new {@link RstInfo} with the given stream id and stream status</p>
*
* @param streamId the stream id
* @param streamStatus the stream status
*/
public RstInfo(int streamId, StreamStatus streamStatus)
{
this.streamId = streamId;
this.streamStatus = streamStatus;
}
/**
* @return the stream id
*/
public int getStreamId()
{
return streamId;
}
/**
* @return the stream status
*/
public StreamStatus getStreamStatus()
{
return streamStatus;
}
@Override
public String toString()
{
return String.format("RST stream=%d %s", streamId, streamStatus);
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
/**
* <p>Helper class that holds useful SPDY constants.</p>
*/
public class SPDY
{
/**
* <p>Constant that indicates the version 2 of the SPDY protocol</p>
*/
public static final short V2 = 2;
/**
* <p>Constant that indicates the version 3 of the SPDY protocol</p>
*/
public static final short V3 = 3;
private SPDY()
{
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
/**
* <p>An unrecoverable exception that signals to the application that
* something wrong happened.</p>
*/
public class SPDYException extends RuntimeException
{
public SPDYException()
{
}
public SPDYException(String message)
{
super(message);
}
public SPDYException(String message, Throwable cause)
{
super(message, cause);
}
public SPDYException(Throwable cause)
{
super(cause);
}
public SPDYException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)
{
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@ -0,0 +1,228 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
import java.util.EventListener;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* <p>A {@link Session} represents the client-side endpoint of a SPDY connection to a single origin server.</p>
* <p>Once a {@link Session} has been obtained, it can be used to open SPDY streams:</p>
* <pre>
* Session session = ...;
* SynInfo synInfo = new SynInfo(true);
* session.syn(synInfo, new Stream.FrameListener.Adapter()
* {
* public void onReply(Stream stream, ReplyInfo replyInfo)
* {
* // Stream reply received
* }
* });
* </pre>
* <p>A {@link Session} is the active part of the endpoint, and by calling its API applications can generate
* events on the connection; conversely {@link SessionFrameListener} is the passive part of the endpoint, and
* has callbacks that are invoked when events happen on the connection.</p>
*
* @see SessionFrameListener
*/
public interface Session
{
/**
* @return the SPDY protocol version used by this session
*/
public short getVersion();
/**
* <p>Registers the given {@code listener} to be notified of session events.</p>
*
* @param listener the listener to register
* @see #removeListener(Listener)
*/
public void addListener(Listener listener);
/**
* <p>Deregisters the give {@code listener} from being notified of session events.</p>
*
* @param listener the listener to deregister
* @see #addListener(Listener)
*/
public void removeListener(Listener listener);
/**
* <p>Sends asynchronously a SYN_FRAME to create a new {@link Stream SPDY stream}.</p>
* <p>Callers may use the returned future to wait for the stream to be created, and
* use the stream, for example, to send data frames.</p>
*
* @param synInfo the metadata to send on stream creation
* @param listener the listener to invoke when events happen on the stream just created
* @return a future for the stream that will be created
* @see #syn(SynInfo, StreamFrameListener, long, TimeUnit, Handler)
*/
public Future<Stream> syn(SynInfo synInfo, StreamFrameListener listener);
/**
* <p>Sends asynchronously a SYN_FRAME to create a new {@link Stream SPDY stream}.</p>
* <p>Callers may pass a non-null completion handler to be notified of when the
* stream has been created and use the stream, for example, to send data frames.</p>
*
* @param synInfo the metadata to send on stream creation
* @param listener the listener to invoke when events happen on the stream just created
* @param timeout the operation's timeout
* @param unit the timeout's unit
* @param handler the completion handler that gets notified of stream creation
* @see #syn(SynInfo, StreamFrameListener)
*/
public void syn(SynInfo synInfo, StreamFrameListener listener, long timeout, TimeUnit unit, Handler<Stream> handler);
/**
* <p>Sends asynchronously a RST_STREAM to abort a stream.</p>
* <p>Callers may use the returned future to wait for the reset to be sent.</p>
*
* @param rstInfo the metadata to reset the stream
* @return a future to wait for the reset to be sent
* @see #rst(RstInfo, long, TimeUnit, Handler)
*/
public Future<Void> rst(RstInfo rstInfo);
/**
* <p>Sends asynchronously a RST_STREAM to abort a stream.</p>
* <p>Callers may pass a non-null completion handler to be notified of when the
* reset has been actually sent.</p>
*
* @param rstInfo the metadata to reset the stream
* @param timeout the operation's timeout
* @param unit the timeout's unit
* @param handler the completion handler that gets notified of reset's send
* @see #rst(RstInfo)
*/
public void rst(RstInfo rstInfo, long timeout, TimeUnit unit, Handler<Void> handler);
/**
* <p>Sends asynchronously a SETTINGS to configure the SPDY connection.</p>
* <p>Callers may use the returned future to wait for the settings to be sent.</p>
*
* @param settingsInfo the metadata to send
* @return a future to wait for the settings to be sent
* @see #settings(SettingsInfo, long, TimeUnit, Handler)
*/
public Future<Void> settings(SettingsInfo settingsInfo);
/**
* <p>Sends asynchronously a SETTINGS to configure the SPDY connection.</p>
* <p>Callers may pass a non-null completion handler to be notified of when the
* settings has been actually sent.</p>
*
* @param settingsInfo the metadata to send
* @param timeout the operation's timeout
* @param unit the timeout's unit
* @param handler the completion handler that gets notified of settings' send
* @see #settings(SettingsInfo)
*/
public void settings(SettingsInfo settingsInfo, long timeout, TimeUnit unit, Handler<Void> handler);
/**
* <p>Sends asynchronously a PING, normally to measure round-trip time.</p>
* <p>Callers may use the returned future to wait for the ping to be sent.</p>
*
* @return a future for the metadata sent
* @see #ping(long, TimeUnit, Handler)
*/
public Future<PingInfo> ping();
/**
* <p>Sends asynchronously a PING, normally to measure round-trip time.</p>
* <p>Callers may pass a non-null completion handler to be notified of when the
* ping has been actually sent.</p>
*
* @param timeout the operation's timeout
* @param unit the timeout's unit
* @param handler the completion handler that gets notified of ping's send
* @see #ping()
*/
public void ping(long timeout, TimeUnit unit, Handler<PingInfo> handler);
/**
* <p>Closes gracefully this session, sending a GO_AWAY frame and then closing the TCP connection.</p>
* <p>Callers may use the returned future to wait for the go away to be sent.</p>
*
* @return a future to wait for the go away to be sent
* @see #goAway(long, TimeUnit, Handler)
*/
public Future<Void> goAway();
/**
* <p>Closes gracefully this session, sending a GO_AWAY frame and then closing the TCP connection.</p>
* <p>Callers may pass a non-null completion handler to be notified of when the
* go away has been actually sent.</p>
*
* @param timeout the operation's timeout
* @param unit the timeout's unit
* @param handler the completion handler that gets notified of go away's send
* @see #goAway()
*/
public void goAway(long timeout, TimeUnit unit, Handler<Void> handler);
/**
* @return the streams currently active in this session
*/
public List<Stream> getStreams();
/**
* <p>Super interface for listeners with callbacks that are invoked on specific session events.</p>
*/
public interface Listener extends EventListener
{
}
/**
* <p>Specialized listener that is invoked upon creation and removal of streams.</p>
*/
public interface StreamListener extends Listener
{
/**
* <p>Callback invoked when a new SPDY stream is created.</p>
*
* @param stream the stream just created
*/
public void onStreamCreated(Stream stream);
/**
* <p>Callback invoked when a SPDY stream is closed.</p>
*
* @param stream the stream just closed.
*/
public void onStreamClosed(Stream stream);
/**
* <p>Empty implementation of {@link StreamListener}.</p>
*/
public static class Adapter implements StreamListener
{
@Override
public void onStreamCreated(Stream stream)
{
}
@Override
public void onStreamClosed(Stream stream)
{
}
}
}
}

View File

@ -0,0 +1,139 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
import java.util.EventListener;
/**
* <p>A {@link SessionFrameListener} is the passive counterpart of a {@link Session} and receives events happening
* on a SPDY session.</p>
*
* @see Session
*/
public interface SessionFrameListener extends EventListener
{
/**
* <p>Callback invoked when a request to create a stream has been received.</p>
* <p>Application code should implement this method and reply to the stream creation, eventually
* sending data:</p>
* <pre>
* public Stream.FrameListener onSyn(Stream stream, SynInfo synInfo)
* {
* // Do something with the metadata contained in synInfo
*
* if (stream.isHalfClosed()) // The other peer will not send data
* {
* stream.reply(new ReplyInfo(false));
* stream.data(new StringDataInfo("foo", true));
* return null; // Not interested in further stream events
* }
*
* ...
* }
* </pre>
* <p>Alternatively, if the stream creation requires reading data sent from the other peer:</p>
* <pre>
* public Stream.FrameListener onSyn(Stream stream, SynInfo synInfo)
* {
* // Do something with the metadata contained in synInfo
*
* if (!stream.isHalfClosed()) // The other peer will send data
* {
* stream.reply(new ReplyInfo(true));
* return new Stream.FrameListener.Adapter() // Interested in stream events
* {
* public void onData(Stream stream, DataInfo dataInfo)
* {
* // Do something with the incoming data in dataInfo
* }
* };
* }
*
* ...
* }
* </pre>
*
* @param stream the stream just created
* @param synInfo the metadata sent on stream creation
* @return a listener for stream events, or null if there is no interest in being notified of stream events
*/
public StreamFrameListener onSyn(Stream stream, SynInfo synInfo);
/**
* <p>Callback invoked when a stream error happens.</p>
*
* @param session the session
* @param rstInfo the metadata of the stream error
*/
public void onRst(Session session, RstInfo rstInfo);
/**
* <p>Callback invoked when a request to configure the SPDY connection has been received.</p>
*
* @param session the session
* @param settingsInfo the metadata sent to configure
*/
public void onSettings(Session session, SettingsInfo settingsInfo);
/**
* <p>Callback invoked when a ping request has completed its round-trip.</p>
*
* @param session the session
* @param pingInfo the metadata received
*/
public void onPing(Session session, PingInfo pingInfo);
/**
* <p>Callback invoked when the other peer signals that it is closing the connection.</p>
*
* @param session the session
* @param goAwayInfo the metadata sent
*/
public void onGoAway(Session session, GoAwayInfo goAwayInfo);
/**
* <p>Empty implementation of {@link SessionFrameListener}</p>
*/
public static class Adapter implements SessionFrameListener
{
@Override
public StreamFrameListener onSyn(Stream stream, SynInfo synInfo)
{
return null;
}
@Override
public void onRst(Session session, RstInfo rstInfo)
{
}
@Override
public void onSettings(Session session, SettingsInfo settingsInfo)
{
}
@Override
public void onPing(Session session, PingInfo pingInfo)
{
}
@Override
public void onGoAway(Session session, GoAwayInfo goAwayInfo)
{
}
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
import java.util.HashMap;
import java.util.Map;
/**
* <p>An enumeration of session statuses.</p>
*/
public enum SessionStatus
{
/**
* <p>The session status indicating no errors</p>
*/
OK(0),
/**
* <p>The session status indicating a protocol error</p>
*/
PROTOCOL_ERROR(1);
/**
* @param code the session status code
* @return a {@link SessionStatus} from the given code,
* or null if no status exists
*/
public static SessionStatus from(int code)
{
return Codes.codes.get(code);
}
private final int code;
private SessionStatus(int code)
{
this.code = code;
Codes.codes.put(code, this);
}
/**
* @return the code of this {@link SessionStatus}
*/
public int getCode()
{
return code;
}
private static class Codes
{
private static final Map<Integer, SessionStatus> codes = new HashMap<>();
}
}

View File

@ -0,0 +1,225 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class Settings implements Iterable<Settings.Setting>
{
private Map<ID, Settings.Setting> settings = new HashMap<>();
public Settings()
{
}
public Settings(Settings original, boolean immutable)
{
Map<ID, Settings.Setting> copy = new HashMap<>(original.size());
copy.putAll(original.settings);
settings = immutable ? Collections.unmodifiableMap(copy) : copy;
}
public Setting get(ID id)
{
return settings.get(id);
}
public void put(Setting setting)
{
settings.put(setting.id(), setting);
}
public Setting remove(ID id)
{
return settings.remove(id);
}
public int size()
{
return settings.size();
}
public void clear()
{
settings.clear();
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Settings that = (Settings)obj;
return settings.equals(that.settings);
}
@Override
public int hashCode()
{
return settings.hashCode();
}
@Override
public Iterator<Setting> iterator()
{
return settings.values().iterator();
}
@Override
public String toString()
{
return settings.toString();
}
public static final class ID
{
public static ID UPLOAD_BANDWIDTH = new ID(1);
public static ID DOWNLOAD_BANDWIDTH = new ID(2);
public static ID ROUND_TRIP_TIME = new ID(3);
public static ID MAX_CONCURRENT_STREAMS = new ID(4);
public static ID CURRENT_CONGESTION_WINDOW = new ID(5);
public static ID DOWNLOAD_RETRANSMISSION_RATE = new ID(6);
public static ID INITIAL_WINDOW_SIZE = new ID(7);
public synchronized static ID from(int code)
{
ID id = Codes.codes.get(code);
if (id == null)
id = new ID(code);
return id;
}
private final int code;
private ID(int code)
{
this.code = code;
Codes.codes.put(code, this);
}
public int code()
{
return code;
}
@Override
public String toString()
{
return String.valueOf(code);
}
private static class Codes
{
private static final Map<Integer, ID> codes = new HashMap<>();
}
}
public static enum Flag
{
NONE((byte)0),
PERSIST((byte)1),
PERSISTED((byte)2);
public static Flag from(byte code)
{
return Codes.codes.get(code);
}
private final byte code;
private Flag(byte code)
{
this.code = code;
Codes.codes.put(code, this);
}
public byte code()
{
return code;
}
private static class Codes
{
private static final Map<Byte, Flag> codes = new HashMap<>();
}
}
public static class Setting
{
private final ID id;
private final Flag flag;
private final int value;
public Setting(ID id, int value)
{
this(id, Flag.NONE, value);
}
public Setting(ID id, Flag flag, int value)
{
this.id = id;
this.flag = flag;
this.value = value;
}
public ID id()
{
return id;
}
public Flag flag()
{
return flag;
}
public int value()
{
return value;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Setting that = (Setting)obj;
return value == that.value && flag == that.flag && id == that.id;
}
@Override
public int hashCode()
{
int result = id.hashCode();
result = 31 * result + flag.hashCode();
result = 31 * result + value;
return result;
}
@Override
public String toString()
{
return String.format("[id=%s,flags=%s:value=%d]", id(), flag(), value());
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
public class SettingsInfo
{
public static final byte CLEAR_PERSISTED = 1;
private final Settings settings;
private final boolean clearPersisted;
public SettingsInfo(Settings settings)
{
this(settings, false);
}
public SettingsInfo(Settings settings, boolean clearPersisted)
{
this.settings = settings;
this.clearPersisted = clearPersisted;
}
public boolean isClearPersisted()
{
return clearPersisted;
}
public byte getFlags()
{
return isClearPersisted() ? CLEAR_PERSISTED : 0;
}
public Settings getSettings()
{
return settings;
}
}

View File

@ -0,0 +1,199 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
import java.nio.channels.WritePendingException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* <p>A {@link Stream} represents a bidirectional exchange of data on top of a {@link Session}.</p>
* <p>Differently from socket streams, where the input and output streams are permanently associated
* with the socket (and hence with the connection that the socket represents), there can be multiple
* SPDY streams for a SPDY session.</p>
* <p>SPDY streams may terminate without this implying that the SPDY session is terminated.</p>
* <p>If SPDY is used to transport the HTTP protocol, then a SPDY stream maps to a HTTP request/response
* cycle, and after the request/response cycle is completed, the stream is closed, and other streams
* may be opened. Differently from HTTP, though, multiple SPDY streams may be opened concurrently
* on the same SPDY session.</p>
* <p>Like {@link Session}, {@link Stream} is the active part and by calling its API applications
* can generate events on the stream; conversely, {@link StreamFrameListener} is the passive part, and its
* callbacks are invoked when events happen on the stream.</p>
* <p>A {@link Stream} can send multiple data frames one after the other but implementations use a
* flow control mechanism that only sends the data frames if the other end has signalled that it can
* accept the frame.</p>
* <p>Data frames should be sent sequentially only when the previous frame has been completely sent.
* The reason for this requirement is to avoid potentially confusing code such as:</p>
* <pre>
* // WRONG CODE, DO NOT USE IT
* final Stream stream = ...;
* stream.data(StringDataInfo("chunk1", false), 5, TimeUnit.SECONDS, new Handler&lt;Void&gt;() { ... });
* stream.data(StringDataInfo("chunk2", true), 1, TimeUnit.SECONDS, new Handler&lt;Void&gt;() { ... });
* </pre>
* <p>where the second call to {@link #data(DataInfo, long, TimeUnit, Handler)} has a timeout smaller
* than the previous call.</p>
* <p>The behavior of such style of invocations is unspecified (it may even throw an exception - similar
* to {@link WritePendingException}).</p>
* <p>The correct sending of data frames is the following:</p>
* <pre>
* final Stream stream = ...;
* ...
* // Blocking version
* stream.data(new StringDataInfo("chunk1", false)).get(1, TimeUnit.SECONDS);
* stream.data(new StringDataInfo("chunk2", true)).get(1, TimeUnit.SECONDS);
*
* // Asynchronous version
* stream.data(new StringDataInfo("chunk1", false), 1, TimeUnit.SECONDS, new Handler.Adapter&lt;Void&gt;()
* {
* public void completed(Void context)
* {
* stream.data(new StringDataInfo("chunk2", true));
* }
* });
* </pre>
*
* @see StreamFrameListener
*/
public interface Stream
{
/**
* @return the id of this stream
*/
public int getId();
/**
* @return the priority of this stream
*/
public byte getPriority();
/**
* @return the session this stream is associated to
*/
public Session getSession();
/**
* <p>Sends asynchronously a SYN_REPLY frame in response to a SYN_STREAM frame.</p>
* <p>Callers may use the returned future to wait for the reply to be actually sent.</p>
*
* @param replyInfo the metadata to send
* @return a future to wait for the reply to be sent
* @see #reply(ReplyInfo, long, TimeUnit, Handler)
* @see SessionFrameListener#onSyn(Stream, SynInfo)
*/
public Future<Void> reply(ReplyInfo replyInfo);
/**
* <p>Sends asynchronously a SYN_REPLY frame in response to a SYN_STREAM frame.</p>
* <p>Callers may pass a non-null completion handler to be notified of when the
* reply has been actually sent.</p>
*
* @param replyInfo the metadata to send
* @param timeout the operation's timeout
* @param unit the timeout's unit
* @param handler the completion handler that gets notified of reply sent
* @see #reply(ReplyInfo)
*/
public void reply(ReplyInfo replyInfo, long timeout, TimeUnit unit, Handler<Void> handler);
/**
* <p>Sends asynchronously a DATA frame on this stream.</p>
* <p>DATA frames should always be sent after a SYN_REPLY frame.</p>
* <p>Callers may use the returned future to wait for the data to be actually sent.</p>
*
* @param dataInfo the metadata to send
* @return a future to wait for the data to be sent
* @see #data(DataInfo, long, TimeUnit, Handler)
* @see #reply(ReplyInfo)
*/
public Future<Void> data(DataInfo dataInfo);
/**
* <p>Sends asynchronously a DATA frame on this stream.</p>
* <p>DATA frames should always be sent after a SYN_REPLY frame.</p>
* <p>Callers may pass a non-null completion handler to be notified of when the
* data has been actually sent.</p>
*
* @param dataInfo the metadata to send
* @param timeout the operation's timeout
* @param unit the timeout's unit
* @param handler the completion handler that gets notified of data sent
* @see #data(DataInfo)
*/
public void data(DataInfo dataInfo, long timeout, TimeUnit unit, Handler<Void> handler);
/**
* <p>Sends asynchronously a HEADER frame on this stream.</p>
* <p>HEADERS frames should always be sent after a SYN_REPLY frame.</p>
* <p>Callers may use the returned future to wait for the headers to be actually sent.</p>
*
* @param headersInfo the metadata to send
* @return a future to wait for the headers to be sent
* @see #headers(HeadersInfo, long, TimeUnit, Handler)
* @see #reply(ReplyInfo)
*/
public Future<Void> headers(HeadersInfo headersInfo);
/**
* <p>Sends asynchronously a HEADER frame on this stream.</p>
* <p>HEADERS frames should always be sent after a SYN_REPLY frame.</p>
* <p>Callers may pass a non-null completion handler to be notified of when the
* headers have been actually sent.</p>
*
* @param headersInfo the metadata to send
* @param timeout the operation's timeout
* @param unit the timeout's unit
* @param handler the completion handler that gets notified of headers sent
* @see #headers(HeadersInfo)
*/
public void headers(HeadersInfo headersInfo, long timeout, TimeUnit unit, Handler<Void> handler);
/**
* @return whether this stream has been closed by both parties
* @see #isHalfClosed()
*/
public boolean isClosed();
/**
* @return whether this stream has been closed by one party only
* @see #isClosed() * @param timeout the timeout for the stream creation
* @param unit the timeout's unit
*/
public boolean isHalfClosed();
/**
* @param key the attribute key
* @return an arbitrary object associated with the given key to this stream
* @see #setAttribute(String, Object)
*/
public Object getAttribute(String key);
/**
* @param key the attribute key
* @param value an arbitrary object to associate with the given key to this stream
* @see #getAttribute(String)
* @see #removeAttribute(String)
*/
public void setAttribute(String key, Object value);
/**
* @param key the attribute key
* @return the arbitrary object associated with the given key to this stream
* @see #setAttribute(String, Object)
*/
public Object removeAttribute(String key);
}

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
import java.util.EventListener;
/**
* <p>A {@link StreamFrameListener} is the passive counterpart of a {@link Stream} and receives
* events happening on a SPDY stream.</p>
*
* @see Stream
*/
public interface StreamFrameListener extends EventListener
{
/**
* <p>Callback invoked when a reply to a stream creation has been received.</p>
* <p>Application code may implement this method to send more data to the other end:</p>
* <pre>
* public void onReply(Stream stream, ReplyInfo replyInfo)
* {
* stream.data(new StringDataInfo("content"), true);
* }
* </pre>
* @param stream the stream
* @param replyInfo the reply metadata
*/
public void onReply(Stream stream, ReplyInfo replyInfo);
/**
* <p>Callback invoked when headers are received on a stream.</p>
*
* @param stream the stream
* @param headersInfo the headers metadata
*/
public void onHeaders(Stream stream, HeadersInfo headersInfo);
/**
* <p>Callback invoked when data bytes are received on a stream.</p>
* <p>Implementers should be read or consume the content of the
* {@link DataInfo} before this method returns.</p>
*
* @param stream the stream
* @param dataInfo the data metadata
*/
public void onData(Stream stream, DataInfo dataInfo);
/**
* <p>Empty implementation of {@link StreamFrameListener}</p>
*/
public static class Adapter implements StreamFrameListener
{
@Override
public void onReply(Stream stream, ReplyInfo replyInfo)
{
}
@Override
public void onHeaders(Stream stream, HeadersInfo headersInfo)
{
}
@Override
public void onData(Stream stream, DataInfo dataInfo)
{
}
}
}

View File

@ -0,0 +1,126 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
import java.util.HashMap;
import java.util.Map;
/**
* <p>An enumeration of stream statuses.</p>
*/
public enum StreamStatus
{
/**
* <p>The stream status indicating a protocol error</p>
*/
PROTOCOL_ERROR(1, 1),
/**
* <p>The stream status indicating that the stream is not valid</p>
*/
INVALID_STREAM(2, 2),
/**
* <p>The stream status indicating that the stream has been refused</p>
*/
REFUSED_STREAM(3, 3),
/**
* <p>The stream status indicating that the implementation does not support the SPDY version of the stream</p>
*/
UNSUPPORTED_VERSION(4, 4),
/**
* <p>The stream status indicating that the stream is no longer needed</p>
*/
CANCEL_STREAM(5, 5),
/**
* <p>The stream status indicating an implementation error</p>
*/
INTERNAL_ERROR(6, 11),
/**
* <p>The stream status indicating a flow control error</p>
*/
FLOW_CONTROL_ERROR(7, 6),
/**
* <p>The stream status indicating a stream opened more than once</p>
*/
STREAM_IN_USE(-1, 7),
/**
* <p>The stream status indicating data on a stream already closed</p>
*/
STREAM_ALREADY_CLOSED(-1, 8),
/**
* <p>The stream status indicating credentials not valid</p>
*/
INVALID_CREDENTIALS(-1, 9),
/**
* <p>The stream status indicating that the implementation could not support a frame too large</p>
*/
FRAME_TOO_LARGE(-1, 10);
/**
* @param version the SPDY protocol version
* @param code the stream status code
* @return a {@link StreamStatus} from the given version and code,
* or null if no such status exists
*/
public static StreamStatus from(short version, int code)
{
switch (version)
{
case SPDY.V2:
return Codes.v2Codes.get(code);
case SPDY.V3:
return Codes.v3Codes.get(code);
default:
throw new IllegalStateException();
}
}
private final int v2Code;
private final int v3Code;
private StreamStatus(int v2Code, int v3Code)
{
this.v2Code = v2Code;
if (v2Code >= 0)
Codes.v2Codes.put(v2Code, this);
this.v3Code = v3Code;
if (v3Code >= 0)
Codes.v3Codes.put(v3Code, this);
}
/**
* @param version the SPDY protocol version
* @return the stream status code
*/
public int getCode(short version)
{
switch (version)
{
case SPDY.V2:
return v2Code;
case SPDY.V3:
return v3Code;
default:
throw new IllegalStateException();
}
}
private static class Codes
{
private static final Map<Integer, StreamStatus> v2Codes = new HashMap<>();
private static final Map<Integer, StreamStatus> v3Codes = new HashMap<>();
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
import java.nio.charset.Charset;
/**
* <p>Specialized {@link DataInfo} for {@link String} content.</p>
*/
public class StringDataInfo extends BytesDataInfo
{
public StringDataInfo(String string, boolean close)
{
this(string, close, false);
}
public StringDataInfo(String string, boolean close, boolean compress)
{
super(string.getBytes(Charset.forName("UTF-8")), close, compress);
}
}

View File

@ -0,0 +1,138 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api;
/**
* <p>A container for SYN_STREAM frames metadata and data.</p>
*/
public class SynInfo
{
/**
* <p>Flag that indicates that this {@link DataInfo} is the last frame in the stream.</p>
*
* @see #isClose()
* @see #getFlags()
*/
public static final byte FLAG_CLOSE = 1;
public static final byte FLAG_UNIDIRECTIONAL = 2;
private final boolean close;
private final boolean unidirectional;
private final int associatedStreamId;
private final byte priority;
private final Headers headers;
/**
* <p>Creates a new {@link SynInfo} instance with empty headers and the given close flag,
* not unidirectional, without associated stream, and with default priority.</p>
*
* @param close the value of the close flag
*/
public SynInfo(boolean close)
{
this(new Headers(), close);
}
/**
* <p>Creates a {@link ReplyInfo} instance with the given headers and the given close flag,
* not unidirectional, without associated stream, and with default priority.</p>
*
* @param headers the {@link Headers}
* @param close the value of the close flag
*/
public SynInfo(Headers headers, boolean close)
{
this(headers, close, false, 0, (byte)0);
}
/**
* <p>Creates a {@link ReplyInfo} instance with the given headers and the given close flag,
* the given unidirectional flag, the given associated stream, and with the given priority.</p>
*
* @param headers the {@link Headers}
* @param close the value of the close flag
* @param unidirectional the value of the unidirectional flag
* @param associatedStreamId the associated stream id
* @param priority the priority
*/
public SynInfo(Headers headers, boolean close, boolean unidirectional, int associatedStreamId, byte priority)
{
this.close = close;
this.unidirectional = unidirectional;
this.associatedStreamId = associatedStreamId;
this.priority = priority;
this.headers = headers;
}
/**
* @return the value of the close flag
*/
public boolean isClose()
{
return close;
}
/**
* @return the value of the unidirectional flag
*/
public boolean isUnidirectional()
{
return unidirectional;
}
/**
* @return the associated stream id
*/
public int getAssociatedStreamId()
{
return associatedStreamId;
}
/**
* @return the priority
*/
public byte getPriority()
{
return priority;
}
/**
* @return the {@link Headers}
*/
public Headers getHeaders()
{
return headers;
}
/**
* @return the close and unidirectional flags as integer
* @see #FLAG_CLOSE
* @see #FLAG_UNIDIRECTIONAL
*/
public byte getFlags()
{
byte flags = isClose() ? FLAG_CLOSE : 0;
flags += isUnidirectional() ? FLAG_UNIDIRECTIONAL : 0;
return flags;
}
@Override
public String toString()
{
return String.format("SYN close=%b headers=%s", close, headers);
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.api.server;
import org.eclipse.jetty.spdy.api.Session;
import org.eclipse.jetty.spdy.api.SessionFrameListener;
/**
* <p>Specific, server-side, {@link SessionFrameListener}.</p>
* <p>In addition to {@link SessionFrameListener}, this listener adds method
* {@link #onConnect(Session)} that is called when a client first connects to the
* server and may be used by a server-side application to send a SETTINGS frame
* to configure the connection before the client can open any stream.</p>
*/
public interface ServerSessionFrameListener extends SessionFrameListener
{
/**
* <p>Callback invoked when a client opens a connection.</p>
*
* @param session the session
*/
public void onConnect(Session session);
/**
* <p>Empty implementation of {@link ServerSessionFrameListener}</p>
*/
public static class Adapter extends SessionFrameListener.Adapter implements ServerSessionFrameListener
{
@Override
public void onConnect(Session session)
{
}
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.frames;
public abstract class ControlFrame
{
public static final int HEADER_LENGTH = 8;
private final short version;
private final ControlFrameType type;
private final byte flags;
public ControlFrame(short version, ControlFrameType type, byte flags)
{
this.version = version;
this.type = type;
this.flags = flags;
}
public short getVersion()
{
return version;
}
public ControlFrameType getType()
{
return type;
}
public byte getFlags()
{
return flags;
}
@Override
public String toString()
{
return String.format("%s frame v%s", getType(), getVersion());
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.frames;
import java.util.HashMap;
import java.util.Map;
public enum ControlFrameType
{
SYN_STREAM((short)1),
SYN_REPLY((short)2),
RST_STREAM((short)3),
SETTINGS((short)4),
NOOP((short)5),
PING((short)6),
GO_AWAY((short)7),
HEADERS((short)8),
WINDOW_UPDATE((short)9);
public static ControlFrameType from(short code)
{
return Codes.codes.get(code);
}
private final short code;
private ControlFrameType(short code)
{
this.code = code;
Codes.codes.put(code, this);
}
public short getCode()
{
return code;
}
private static class Codes
{
private static final Map<Short, ControlFrameType> codes = new HashMap<>();
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.frames;
import org.eclipse.jetty.spdy.api.DataInfo;
public class DataFrame
{
public static final int HEADER_LENGTH = 8;
private final int streamId;
private final byte flags;
private final int length;
public DataFrame(int streamId, byte flags, int length)
{
this.streamId = streamId;
this.flags = flags;
this.length = length;
}
public int getStreamId()
{
return streamId;
}
public byte getFlags()
{
return flags;
}
public int getLength()
{
return length;
}
public boolean isClose()
{
return (flags & DataInfo.FLAG_CLOSE) == DataInfo.FLAG_CLOSE;
}
public boolean isCompress()
{
return (flags & DataInfo.FLAG_COMPRESS) == DataInfo.FLAG_COMPRESS;
}
@Override
public String toString()
{
return String.format("DATA frame stream=%d length=%d close=%b compress=%b", getStreamId(), getLength(), isClose(), isCompress());
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.frames;
import org.eclipse.jetty.spdy.api.SessionStatus;
public class GoAwayFrame extends ControlFrame
{
private final int lastStreamId;
private final int statusCode;
public GoAwayFrame(short version, int lastStreamId, int statusCode)
{
super(version, ControlFrameType.GO_AWAY, (byte)0);
this.lastStreamId = lastStreamId;
this.statusCode = statusCode;
}
public int getLastStreamId()
{
return lastStreamId;
}
public int getStatusCode()
{
return statusCode;
}
@Override
public String toString()
{
SessionStatus sessionStatus = SessionStatus.from(getStatusCode());
return String.format("%s last_stream=%d status=%s", super.toString(), getLastStreamId(), sessionStatus == null ? getStatusCode() : sessionStatus);
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.frames;
import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.HeadersInfo;
public class HeadersFrame extends ControlFrame
{
private final int streamId;
private final Headers headers;
public HeadersFrame(short version, byte flags, int streamId, Headers headers)
{
super(version, ControlFrameType.HEADERS, flags);
this.streamId = streamId;
this.headers = headers;
}
public int getStreamId()
{
return streamId;
}
public Headers getHeaders()
{
return headers;
}
public boolean isClose()
{
return (getFlags() & HeadersInfo.FLAG_CLOSE) == HeadersInfo.FLAG_CLOSE;
}
public boolean isResetCompression()
{
return (getFlags() & HeadersInfo.FLAG_RESET_COMPRESSION) == HeadersInfo.FLAG_RESET_COMPRESSION;
}
@Override
public String toString()
{
return String.format("%s stream=%d close=%b reset_compression=%b", super.toString(), getStreamId(), isClose(), isResetCompression());
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.frames;
import org.eclipse.jetty.spdy.api.SPDY;
public class NoOpFrame extends ControlFrame
{
public NoOpFrame()
{
super(SPDY.V2, ControlFrameType.NOOP, (byte)0);
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.frames;
public class PingFrame extends ControlFrame
{
private final int pingId;
public PingFrame(short version, int pingId)
{
super(version, ControlFrameType.PING, (byte)0);
this.pingId = pingId;
}
public int getPingId()
{
return pingId;
}
@Override
public String toString()
{
return String.format("%s ping=%d", super.toString(), getPingId());
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.frames;
import org.eclipse.jetty.spdy.api.StreamStatus;
public class RstStreamFrame extends ControlFrame
{
private final int streamId;
private final int statusCode;
public RstStreamFrame(short version, int streamId, int statusCode)
{
super(version, ControlFrameType.RST_STREAM, (byte)0);
this.streamId = streamId;
this.statusCode = statusCode;
}
public int getStreamId()
{
return streamId;
}
public int getStatusCode()
{
return statusCode;
}
@Override
public String toString()
{
StreamStatus streamStatus = StreamStatus.from(getVersion(), getStatusCode());
return String.format("%s stream=%d status=%s", super.toString(), getStreamId(), streamStatus == null ? getStatusCode() : streamStatus);
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.frames;
import org.eclipse.jetty.spdy.api.Settings;
import org.eclipse.jetty.spdy.api.SettingsInfo;
public class SettingsFrame extends ControlFrame
{
private final Settings settings;
public SettingsFrame(short version, byte flags, Settings settings)
{
super(version, ControlFrameType.SETTINGS, flags);
this.settings = settings;
}
public boolean isClearPersisted()
{
return (getFlags() & SettingsInfo.CLEAR_PERSISTED) == SettingsInfo.CLEAR_PERSISTED;
}
public Settings getSettings()
{
return settings;
}
@Override
public String toString()
{
return String.format("%s clear_persisted=%b settings=%s", super.toString(), isClearPersisted(), getSettings());
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.frames;
import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.ReplyInfo;
public class SynReplyFrame extends ControlFrame
{
private final int streamId;
private final Headers headers;
public SynReplyFrame(short version, byte flags, int streamId, Headers headers)
{
super(version, ControlFrameType.SYN_REPLY, flags);
this.streamId = streamId;
this.headers = headers;
}
public int getStreamId()
{
return streamId;
}
public Headers getHeaders()
{
return headers;
}
public boolean isClose()
{
return (getFlags() & ReplyInfo.FLAG_CLOSE) == ReplyInfo.FLAG_CLOSE;
}
@Override
public String toString()
{
return String.format("%s stream=%d close=%b", super.toString(), getStreamId(), isClose());
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.frames;
import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.SynInfo;
public class SynStreamFrame extends ControlFrame
{
private final int streamId;
private final int associatedStreamId;
private final byte priority;
private final Headers headers;
public SynStreamFrame(short version, byte flags, int streamId, int associatedStreamId, byte priority, Headers headers)
{
super(version, ControlFrameType.SYN_STREAM, flags);
this.streamId = streamId;
this.associatedStreamId = associatedStreamId;
this.priority = priority;
this.headers = headers;
}
public int getStreamId()
{
return streamId;
}
public int getAssociatedStreamId()
{
return associatedStreamId;
}
public byte getPriority()
{
return priority;
}
public Headers getHeaders()
{
return headers;
}
public boolean isClose()
{
return (getFlags() & SynInfo.FLAG_CLOSE) == SynInfo.FLAG_CLOSE;
}
public boolean isUnidirectional()
{
return (getFlags() & SynInfo.FLAG_UNIDIRECTIONAL) == SynInfo.FLAG_UNIDIRECTIONAL;
}
@Override
public String toString()
{
return String.format("%s stream=%d close=%b", super.toString(), getStreamId(), isClose());
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.frames;
public class WindowUpdateFrame extends ControlFrame
{
private final int streamId;
private final int windowDelta;
public WindowUpdateFrame(short version, int streamId, int windowDelta)
{
super(version, ControlFrameType.WINDOW_UPDATE, (byte)0);
this.streamId = streamId;
this.windowDelta = windowDelta;
}
public int getStreamId()
{
return streamId;
}
public int getWindowDelta()
{
return windowDelta;
}
@Override
public String toString()
{
return String.format("%s stream=%d delta=%d", super.toString(), getStreamId(), getWindowDelta());
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.generator;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.ByteBufferPool;
import org.eclipse.jetty.spdy.frames.ControlFrame;
public abstract class ControlFrameGenerator
{
private final ByteBufferPool bufferPool;
protected ControlFrameGenerator(ByteBufferPool bufferPool)
{
this.bufferPool = bufferPool;
}
protected ByteBufferPool getByteBufferPool()
{
return bufferPool;
}
public abstract ByteBuffer generate(ControlFrame frame);
protected void generateControlFrameHeader(ControlFrame frame, int frameLength, ByteBuffer buffer)
{
buffer.putShort((short)(0x8000 + frame.getVersion()));
buffer.putShort(frame.getType().getCode());
int flagsAndLength = frame.getFlags();
flagsAndLength <<= 24;
flagsAndLength += frameLength;
buffer.putInt(flagsAndLength);
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.generator;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.ByteBufferPool;
import org.eclipse.jetty.spdy.api.DataInfo;
import org.eclipse.jetty.spdy.frames.DataFrame;
public class DataFrameGenerator
{
private final ByteBufferPool bufferPool;
public DataFrameGenerator(ByteBufferPool bufferPool)
{
this.bufferPool = bufferPool;
}
public ByteBuffer generate(int streamId, int length, DataInfo dataInfo)
{
ByteBuffer buffer = bufferPool.acquire(DataFrame.HEADER_LENGTH + length, true);
buffer.position(DataFrame.HEADER_LENGTH);
// Guaranteed to always be >= 0
int read = dataInfo.readInto(buffer);
buffer.putInt(0, streamId & 0x7F_FF_FF_FF);
buffer.putInt(4, read & 0x00_FF_FF_FF);
byte flags = dataInfo.getFlags();
if (dataInfo.available() > 0)
flags &= ~DataInfo.FLAG_CLOSE;
buffer.put(4, flags);
buffer.flip();
return buffer;
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.generator;
import java.nio.ByteBuffer;
import java.util.EnumMap;
import org.eclipse.jetty.spdy.ByteBufferPool;
import org.eclipse.jetty.spdy.CompressionFactory;
import org.eclipse.jetty.spdy.api.DataInfo;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.ControlFrameType;
public class Generator
{
private final EnumMap<ControlFrameType, ControlFrameGenerator> generators = new EnumMap<>(ControlFrameType.class);
private final DataFrameGenerator dataFrameGenerator;
public Generator(ByteBufferPool bufferPool, CompressionFactory.Compressor compressor)
{
HeadersBlockGenerator headersBlockGenerator = new HeadersBlockGenerator(compressor);
generators.put(ControlFrameType.SYN_STREAM, new SynStreamGenerator(bufferPool, headersBlockGenerator));
generators.put(ControlFrameType.SYN_REPLY, new SynReplyGenerator(bufferPool, headersBlockGenerator));
generators.put(ControlFrameType.RST_STREAM, new RstStreamGenerator(bufferPool));
generators.put(ControlFrameType.SETTINGS, new SettingsGenerator(bufferPool));
generators.put(ControlFrameType.NOOP, new NoOpGenerator(bufferPool));
generators.put(ControlFrameType.PING, new PingGenerator(bufferPool));
generators.put(ControlFrameType.GO_AWAY, new GoAwayGenerator(bufferPool));
generators.put(ControlFrameType.HEADERS, new HeadersGenerator(bufferPool, headersBlockGenerator));
generators.put(ControlFrameType.WINDOW_UPDATE, new WindowUpdateGenerator(bufferPool));
dataFrameGenerator = new DataFrameGenerator(bufferPool);
}
public ByteBuffer control(ControlFrame frame)
{
ControlFrameGenerator generator = generators.get(frame.getType());
return generator.generate(frame);
}
public ByteBuffer data(int streamId, int length, DataInfo dataInfo)
{
return dataFrameGenerator.generate(streamId, length, dataInfo);
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.generator;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.ByteBufferPool;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.GoAwayFrame;
public class GoAwayGenerator extends ControlFrameGenerator
{
public GoAwayGenerator(ByteBufferPool bufferPool)
{
super(bufferPool);
}
@Override
public ByteBuffer generate(ControlFrame frame)
{
GoAwayFrame goAway = (GoAwayFrame)frame;
int frameBodyLength = 8;
int totalLength = ControlFrame.HEADER_LENGTH + frameBodyLength;
ByteBuffer buffer = getByteBufferPool().acquire(totalLength, true);
generateControlFrameHeader(goAway, frameBodyLength, buffer);
buffer.putInt(goAway.getLastStreamId() & 0x7F_FF_FF_FF);
writeStatusCode(goAway, buffer);
buffer.flip();
return buffer;
}
private void writeStatusCode(GoAwayFrame goAway, ByteBuffer buffer)
{
switch (goAway.getVersion())
{
case SPDY.V2:
break;
case SPDY.V3:
buffer.putInt(goAway.getStatusCode());
break;
default:
throw new IllegalStateException();
}
}
}

View File

@ -0,0 +1,145 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.generator;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import org.eclipse.jetty.spdy.CompressionDictionary;
import org.eclipse.jetty.spdy.CompressionFactory;
import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.SPDY;
public class HeadersBlockGenerator
{
private final CompressionFactory.Compressor compressor;
private boolean needsDictionary = true;
public HeadersBlockGenerator(CompressionFactory.Compressor compressor)
{
this.compressor = compressor;
}
public ByteBuffer generate(short version, Headers headers)
{
// TODO: ByteArrayOutputStream is quite inefficient, but grows on demand; optimize using ByteBuffer ?
Charset iso1 = Charset.forName("ISO-8859-1");
ByteArrayOutputStream buffer = new ByteArrayOutputStream(headers.size() * 64);
writeCount(version, buffer, headers.size());
for (Headers.Header header : headers)
{
String name = header.name();
byte[] nameBytes = name.getBytes(iso1);
writeNameLength(version, buffer, nameBytes.length);
buffer.write(nameBytes, 0, nameBytes.length);
// Most common path first
String value = header.value();
byte[] valueBytes = value.getBytes(iso1);
if (header.hasMultipleValues())
{
String[] values = header.values();
for (int i = 1; i < values.length; ++i)
{
byte[] moreValueBytes = values[i].getBytes(iso1);
byte[] newValueBytes = new byte[valueBytes.length + 1 + moreValueBytes.length];
System.arraycopy(valueBytes, 0, newValueBytes, 0, valueBytes.length);
newValueBytes[valueBytes.length] = 0;
System.arraycopy(moreValueBytes, 0, newValueBytes, valueBytes.length + 1, moreValueBytes.length);
valueBytes = newValueBytes;
}
}
writeValueLength(version, buffer, valueBytes.length);
buffer.write(valueBytes, 0, valueBytes.length);
}
return compress(version, buffer.toByteArray());
}
private ByteBuffer compress(short version, byte[] bytes)
{
ByteArrayOutputStream buffer = new ByteArrayOutputStream(bytes.length);
// The headers compression context is per-session, so we need to synchronize
synchronized (compressor)
{
if (needsDictionary)
{
compressor.setDictionary(CompressionDictionary.get(version));
needsDictionary = false;
}
compressor.setInput(bytes);
// Compressed bytes may be bigger than input bytes, so we need to loop and accumulate them
// Beware that the minimum amount of bytes generated by the compressor is few bytes, so we
// need to use an output buffer that is big enough to exit the compress loop
buffer.reset();
int compressed;
byte[] output = new byte[Math.max(256, bytes.length)];
while (true)
{
// SPDY uses the SYNC_FLUSH mode
compressed = compressor.compress(output);
buffer.write(output, 0, compressed);
if (compressed < output.length)
break;
}
}
return ByteBuffer.wrap(buffer.toByteArray());
}
private void writeCount(short version, ByteArrayOutputStream buffer, int value)
{
switch (version)
{
case SPDY.V2:
{
buffer.write((value & 0xFF_00) >>> 8);
buffer.write(value & 0x00_FF);
break;
}
case SPDY.V3:
{
buffer.write((value & 0xFF_00_00_00) >>> 24);
buffer.write((value & 0x00_FF_00_00) >>> 16);
buffer.write((value & 0x00_00_FF_00) >>> 8);
buffer.write(value & 0x00_00_00_FF);
break;
}
default:
{
// Here the version is trusted to be correct; if it's not
// then it's a bug rather than an application error
throw new IllegalStateException();
}
}
}
private void writeNameLength(short version, ByteArrayOutputStream buffer, int length)
{
writeCount(version, buffer, length);
}
private void writeValueLength(short version, ByteArrayOutputStream buffer, int length)
{
writeCount(version, buffer, length);
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.generator;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.ByteBufferPool;
import org.eclipse.jetty.spdy.SessionException;
import org.eclipse.jetty.spdy.api.SessionStatus;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.HeadersFrame;
public class HeadersGenerator extends ControlFrameGenerator
{
private final HeadersBlockGenerator headersBlockGenerator;
public HeadersGenerator(ByteBufferPool bufferPool, HeadersBlockGenerator headersBlockGenerator)
{
super(bufferPool);
this.headersBlockGenerator = headersBlockGenerator;
}
@Override
public ByteBuffer generate(ControlFrame frame)
{
HeadersFrame headers = (HeadersFrame)frame;
short version = headers.getVersion();
ByteBuffer headersBuffer = headersBlockGenerator.generate(version, headers.getHeaders());
int frameBodyLength = 4;
int frameLength = frameBodyLength + headersBuffer.remaining();
if (frameLength > 0xFF_FF_FF)
{
// Too many headers, but unfortunately we have already modified the compression
// context, so we have no other choice than tear down the connection.
throw new SessionException(SessionStatus.PROTOCOL_ERROR, "Too many headers");
}
int totalLength = ControlFrame.HEADER_LENGTH + frameLength;
ByteBuffer buffer = getByteBufferPool().acquire(totalLength, true);
generateControlFrameHeader(headers, frameLength, buffer);
buffer.putInt(headers.getStreamId() & 0x7F_FF_FF_FF);
buffer.put(headersBuffer);
buffer.flip();
return buffer;
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.generator;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.ByteBufferPool;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.NoOpFrame;
public class NoOpGenerator extends ControlFrameGenerator
{
public NoOpGenerator(ByteBufferPool bufferPool)
{
super(bufferPool);
}
@Override
public ByteBuffer generate(ControlFrame frame)
{
NoOpFrame noOp = (NoOpFrame)frame;
int frameBodyLength = 0;
int totalLength = ControlFrame.HEADER_LENGTH + frameBodyLength;
ByteBuffer buffer = getByteBufferPool().acquire(totalLength, true);
generateControlFrameHeader(noOp, frameBodyLength, buffer);
buffer.flip();
return buffer;
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.generator;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.ByteBufferPool;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.PingFrame;
public class PingGenerator extends ControlFrameGenerator
{
public PingGenerator(ByteBufferPool bufferPool)
{
super(bufferPool);
}
@Override
public ByteBuffer generate(ControlFrame frame)
{
PingFrame ping = (PingFrame)frame;
int frameBodyLength = 4;
int totalLength = ControlFrame.HEADER_LENGTH + frameBodyLength;
ByteBuffer buffer = getByteBufferPool().acquire(totalLength, true);
generateControlFrameHeader(ping, frameBodyLength, buffer);
buffer.putInt(ping.getPingId());
buffer.flip();
return buffer;
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.generator;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.ByteBufferPool;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.RstStreamFrame;
public class RstStreamGenerator extends ControlFrameGenerator
{
public RstStreamGenerator(ByteBufferPool bufferPool)
{
super(bufferPool);
}
@Override
public ByteBuffer generate(ControlFrame frame)
{
RstStreamFrame rstStream = (RstStreamFrame)frame;
int frameBodyLength = 8;
int totalLength = ControlFrame.HEADER_LENGTH + frameBodyLength;
ByteBuffer buffer = getByteBufferPool().acquire(totalLength, true);
generateControlFrameHeader(rstStream, frameBodyLength, buffer);
buffer.putInt(rstStream.getStreamId() & 0x7F_FF_FF_FF);
buffer.putInt(rstStream.getStatusCode());
buffer.flip();
return buffer;
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.generator;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.ByteBufferPool;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.Settings;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.SettingsFrame;
public class SettingsGenerator extends ControlFrameGenerator
{
public SettingsGenerator(ByteBufferPool bufferPool)
{
super(bufferPool);
}
@Override
public ByteBuffer generate(ControlFrame frame)
{
SettingsFrame settingsFrame = (SettingsFrame)frame;
Settings settings = settingsFrame.getSettings();
int size = settings.size();
int frameBodyLength = 4 + 8 * size;
int totalLength = ControlFrame.HEADER_LENGTH + frameBodyLength;
ByteBuffer buffer = getByteBufferPool().acquire(totalLength, true);
generateControlFrameHeader(settingsFrame, frameBodyLength, buffer);
buffer.putInt(size);
for (Settings.Setting setting : settings)
{
int id = setting.id().code();
byte flags = setting.flag().code();
int idAndFlags = convertIdAndFlags(frame.getVersion(), id, flags);
buffer.putInt(idAndFlags);
buffer.putInt(setting.value());
}
buffer.flip();
return buffer;
}
private int convertIdAndFlags(short version, int id, byte flags)
{
switch (version)
{
case SPDY.V2:
{
// In v2 the format is 24 bits of ID + 8 bits of flag
int idAndFlags = (id << 8) + flags;
// A bug in the Chromium implementation forces v2 to have
// the 3 ID bytes little endian, so we swap first and third
int result = idAndFlags & 0x00_FF_00_FF;
result += (idAndFlags & 0xFF_00_00_00) >>> 16;
result += (idAndFlags & 0x00_00_FF_00) << 16;
return result;
}
case SPDY.V3:
{
// In v3 the format is 8 bits of flags + 24 bits of ID
return (flags << 24) + (id & 0xFF_FF_FF);
}
default:
{
throw new IllegalStateException();
}
}
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.generator;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.ByteBufferPool;
import org.eclipse.jetty.spdy.SessionException;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.SessionStatus;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.SynReplyFrame;
public class SynReplyGenerator extends ControlFrameGenerator
{
private final HeadersBlockGenerator headersBlockGenerator;
public SynReplyGenerator(ByteBufferPool bufferPool, HeadersBlockGenerator headersBlockGenerator)
{
super(bufferPool);
this.headersBlockGenerator = headersBlockGenerator;
}
@Override
public ByteBuffer generate(ControlFrame frame)
{
SynReplyFrame synReply = (SynReplyFrame)frame;
short version = synReply.getVersion();
ByteBuffer headersBuffer = headersBlockGenerator.generate(version, synReply.getHeaders());
int frameBodyLength = getFrameDataLength(version);
int frameLength = frameBodyLength + headersBuffer.remaining();
if (frameLength > 0xFF_FF_FF)
{
// Too many headers, but unfortunately we have already modified the compression
// context, so we have no other choice than tear down the connection.
throw new SessionException(SessionStatus.PROTOCOL_ERROR, "Too many headers");
}
int totalLength = ControlFrame.HEADER_LENGTH + frameLength;
ByteBuffer buffer = getByteBufferPool().acquire(totalLength, true);
generateControlFrameHeader(synReply, frameLength, buffer);
buffer.putInt(synReply.getStreamId() & 0x7F_FF_FF_FF);
writeAdditional(version, buffer);
buffer.put(headersBuffer);
buffer.flip();
return buffer;
}
private int getFrameDataLength(short version)
{
switch (version)
{
case SPDY.V2:
return 6;
case SPDY.V3:
return 4;
default:
// Here the version is trusted to be correct; if it's not
// then it's a bug rather than an application error
throw new IllegalStateException();
}
}
private void writeAdditional(short version, ByteBuffer buffer)
{
switch (version)
{
case SPDY.V2:
buffer.putShort((short)0);
break;
case SPDY.V3:
break;
default:
// Here the version is trusted to be correct; if it's not
// then it's a bug rather than an application error
throw new IllegalStateException();
}
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.generator;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.ByteBufferPool;
import org.eclipse.jetty.spdy.SessionException;
import org.eclipse.jetty.spdy.StreamException;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.SessionStatus;
import org.eclipse.jetty.spdy.api.StreamStatus;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.SynStreamFrame;
public class SynStreamGenerator extends ControlFrameGenerator
{
private final HeadersBlockGenerator headersBlockGenerator;
public SynStreamGenerator(ByteBufferPool bufferPool, HeadersBlockGenerator headersBlockGenerator)
{
super(bufferPool);
this.headersBlockGenerator = headersBlockGenerator;
}
@Override
public ByteBuffer generate(ControlFrame frame)
{
SynStreamFrame synStream = (SynStreamFrame)frame;
short version = synStream.getVersion();
ByteBuffer headersBuffer = headersBlockGenerator.generate(version, synStream.getHeaders());
int frameBodyLength = 10;
int frameLength = frameBodyLength + headersBuffer.remaining();
if (frameLength > 0xFF_FF_FF)
{
// Too many headers, but unfortunately we have already modified the compression
// context, so we have no other choice than tear down the connection.
throw new SessionException(SessionStatus.PROTOCOL_ERROR, "Too many headers");
}
int totalLength = ControlFrame.HEADER_LENGTH + frameLength;
ByteBuffer buffer = getByteBufferPool().acquire(totalLength, true);
generateControlFrameHeader(synStream, frameLength, buffer);
int streamId = synStream.getStreamId();
buffer.putInt(streamId & 0x7F_FF_FF_FF);
buffer.putInt(synStream.getAssociatedStreamId() & 0x7F_FF_FF_FF);
writePriority(streamId, version, synStream.getPriority(), buffer);
buffer.put(headersBuffer);
buffer.flip();
return buffer;
}
private void writePriority(int streamId, short version, byte priority, ByteBuffer buffer)
{
switch (version)
{
case SPDY.V2:
priority <<= 6;
break;
case SPDY.V3:
priority <<= 5;
break;
default:
throw new StreamException(streamId, StreamStatus.UNSUPPORTED_VERSION);
}
buffer.put(priority);
buffer.put((byte)0);
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.generator;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.ByteBufferPool;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.WindowUpdateFrame;
public class WindowUpdateGenerator extends ControlFrameGenerator
{
public WindowUpdateGenerator(ByteBufferPool bufferPool)
{
super(bufferPool);
}
@Override
public ByteBuffer generate(ControlFrame frame)
{
WindowUpdateFrame windowUpdate = (WindowUpdateFrame)frame;
int frameBodyLength = 8;
int totalLength = ControlFrame.HEADER_LENGTH + frameBodyLength;
ByteBuffer buffer = getByteBufferPool().acquire(totalLength, true);
generateControlFrameHeader(windowUpdate, frameBodyLength, buffer);
buffer.putInt(windowUpdate.getStreamId() & 0x7F_FF_FF_FF);
buffer.putInt(windowUpdate.getWindowDelta() & 0x7F_FF_FF_FF);
buffer.flip();
return buffer;
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.parser;
import java.nio.ByteBuffer;
public abstract class ControlFrameBodyParser
{
public abstract boolean parse(ByteBuffer buffer);
}

View File

@ -0,0 +1,197 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.parser;
import java.nio.ByteBuffer;
import java.util.EnumMap;
import org.eclipse.jetty.spdy.CompressionFactory;
import org.eclipse.jetty.spdy.SessionException;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.SessionStatus;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.ControlFrameType;
public abstract class ControlFrameParser
{
private final EnumMap<ControlFrameType, ControlFrameBodyParser> parsers = new EnumMap<>(ControlFrameType.class);
private final ControlFrameBodyParser unknownParser = new UnknownControlFrameBodyParser(this);
private State state = State.VERSION;
private int cursor;
private short version;
private short type;
private byte flags;
private int length;
private ControlFrameBodyParser parser;
public ControlFrameParser(CompressionFactory.Decompressor decompressor)
{
parsers.put(ControlFrameType.SYN_STREAM, new SynStreamBodyParser(decompressor, this));
parsers.put(ControlFrameType.SYN_REPLY, new SynReplyBodyParser(decompressor, this));
parsers.put(ControlFrameType.RST_STREAM, new RstStreamBodyParser(this));
parsers.put(ControlFrameType.SETTINGS, new SettingsBodyParser(this));
parsers.put(ControlFrameType.NOOP, new NoOpBodyParser(this));
parsers.put(ControlFrameType.PING, new PingBodyParser(this));
parsers.put(ControlFrameType.GO_AWAY, new GoAwayBodyParser(this));
parsers.put(ControlFrameType.HEADERS, new HeadersBodyParser(decompressor, this));
parsers.put(ControlFrameType.WINDOW_UPDATE, new WindowUpdateBodyParser(this));
}
public short getVersion()
{
return version;
}
public byte getFlags()
{
return flags;
}
public int getLength()
{
return length;
}
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())
{
switch (state)
{
case VERSION:
{
if (buffer.remaining() >= 2)
{
version = (short)(buffer.getShort() & 0x7F_FF);
checkVersion(version);
state = State.TYPE;
}
else
{
state = State.VERSION_BYTES;
cursor = 2;
}
break;
}
case VERSION_BYTES:
{
byte currByte = buffer.get();
--cursor;
version += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
{
version &= 0x7F_FF;
checkVersion(version);
state = State.TYPE;
}
break;
}
case TYPE:
{
if (buffer.remaining() >= 2)
{
type = buffer.getShort();
state = State.FLAGS;
}
else
{
state = State.TYPE_BYTES;
cursor = 2;
}
break;
}
case TYPE_BYTES:
{
byte currByte = buffer.get();
--cursor;
type += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
state = State.FLAGS;
break;
}
case FLAGS:
{
flags = buffer.get();
cursor = 3;
state = State.LENGTH;
break;
}
case LENGTH:
{
byte currByte = buffer.get();
--cursor;
length += (currByte & 0xFF) << 8 * cursor;
if (cursor > 0)
break;
ControlFrameType controlFrameType = ControlFrameType.from(type);
// SPEC v3, 2.2.1: unrecognized control frames must be ignored
if (controlFrameType == null)
parser = unknownParser;
else
parser = parsers.get(controlFrameType);
state = State.BODY;
// We have to let it fall through the next switch:
// the NOOP frame has no body and we cannot break
// because the buffer may be consumed and we will
// never enter the BODY case.
}
case BODY:
{
if (parser.parse(buffer))
{
reset();
return true;
}
break;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
private void checkVersion(short version)
{
if (version != SPDY.V2 && version != SPDY.V3)
throw new SessionException(SessionStatus.PROTOCOL_ERROR, "Unrecognized version " + version);
}
private void reset()
{
state = State.VERSION;
cursor = 0;
version = 0;
type = 0;
flags = 0;
length = 0;
parser = null;
}
protected abstract void onControlFrame(ControlFrame frame);
private enum State
{
VERSION, VERSION_BYTES, TYPE, TYPE_BYTES, FLAGS, LENGTH, BODY
}
}

View File

@ -0,0 +1,153 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.api.DataInfo;
import org.eclipse.jetty.spdy.frames.DataFrame;
public abstract class DataFrameParser
{
private State state = State.STREAM_ID;
private int cursor;
private int streamId;
private byte flags;
private int length;
/**
* <p>Parses the given {@link ByteBuffer} for a data frame.</p>
*
* @param buffer the {@link ByteBuffer} to parse
* @return true if the data frame has been fully parsed, false otherwise
*/
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())
{
switch (state)
{
case STREAM_ID:
{
if (buffer.remaining() >= 4)
{
streamId = buffer.getInt() & 0x7F_FF_FF_FF;
state = State.FLAGS;
}
else
{
state = State.STREAM_ID_BYTES;
cursor = 4;
}
break;
}
case STREAM_ID_BYTES:
{
byte currByte = buffer.get();
--cursor;
streamId += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
state = State.FLAGS;
break;
}
case FLAGS:
{
flags = buffer.get();
cursor = 3;
state = State.LENGTH;
break;
}
case LENGTH:
{
byte currByte = buffer.get();
--cursor;
length += (currByte & 0xFF) << 8 * cursor;
if (cursor > 0)
break;
state = State.DATA;
// Fall down if length == 0: we can't loop because the buffer
// may be empty but we need to invoke the application anyway
if (length > 0)
break;
}
case DATA:
{
// Length can only be at most 3 bytes, which is 16_777_215 i.e. 16 MiB.
// However, compliant clients should implement flow control, so it's
// unlikely that we will get that 16 MiB chunk.
// However, TCP may further split the flow control window, so we may
// only have part of the data at this point.
int size = Math.min(length, buffer.remaining());
int limit = buffer.limit();
buffer.limit(buffer.position() + size);
ByteBuffer bytes = buffer.slice();
buffer.limit(limit);
buffer.position(buffer.position() + size);
length -= size;
if (length == 0)
{
onDataFrame(bytes);
return true;
}
else
{
// We got only part of the frame data bytes,
// so we generate a synthetic data frame
onDataFragment(bytes);
}
break;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
private void onDataFrame(ByteBuffer bytes)
{
DataFrame frame = new DataFrame(streamId, flags, bytes.remaining());
onDataFrame(frame, bytes);
reset();
}
private void onDataFragment(ByteBuffer bytes)
{
DataFrame frame = new DataFrame(streamId, (byte)(flags & ~DataInfo.FLAG_CLOSE), bytes.remaining());
onDataFrame(frame, bytes);
// Do not reset, we're expecting more data
}
protected abstract void onDataFrame(DataFrame frame, ByteBuffer data);
private void reset()
{
state = State.STREAM_ID;
cursor = 0;
streamId = 0;
flags = 0;
length = 0;
}
private enum State
{
STREAM_ID, STREAM_ID_BYTES, FLAGS, LENGTH, DATA
}
}

View File

@ -0,0 +1,149 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.frames.GoAwayFrame;
public class GoAwayBodyParser extends ControlFrameBodyParser
{
private final ControlFrameParser controlFrameParser;
private State state = State.LAST_STREAM_ID;
private int cursor;
private int lastStreamId;
private int statusCode;
public GoAwayBodyParser(ControlFrameParser controlFrameParser)
{
this.controlFrameParser = controlFrameParser;
}
@Override
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())
{
switch (state)
{
case LAST_STREAM_ID:
{
if (buffer.remaining() >= 4)
{
lastStreamId = buffer.getInt() & 0x7F_FF_FF_FF;
switch (controlFrameParser.getVersion())
{
case SPDY.V2:
{
onGoAway();
return true;
}
case SPDY.V3:
{
state = State.STATUS_CODE;
break;
}
}
}
else
{
state = State.LAST_STREAM_ID_BYTES;
cursor = 4;
}
break;
}
case LAST_STREAM_ID_BYTES:
{
byte currByte = buffer.get();
--cursor;
lastStreamId += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
{
lastStreamId &= 0x7F_FF_FF_FF;
switch (controlFrameParser.getVersion())
{
case SPDY.V2:
{
onGoAway();
return true;
}
case SPDY.V3:
{
state = State.STATUS_CODE;
break;
}
}
}
break;
}
case STATUS_CODE:
{
if (buffer.remaining() >= 4)
{
statusCode = buffer.getInt();
onGoAway();
return true;
}
else
{
state = State.STATUS_CODE_BYTES;
cursor = 4;
}
break;
}
case STATUS_CODE_BYTES:
{
byte currByte = buffer.get();
--cursor;
statusCode += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
{
onGoAway();
return true;
}
break;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
private void onGoAway()
{
GoAwayFrame frame = new GoAwayFrame(controlFrameParser.getVersion(), lastStreamId, statusCode);
controlFrameParser.onControlFrame(frame);
reset();
}
private void reset()
{
state = State.LAST_STREAM_ID;
cursor = 0;
lastStreamId = 0;
statusCode = 0;
}
private enum State
{
LAST_STREAM_ID, LAST_STREAM_ID_BYTES, STATUS_CODE, STATUS_CODE_BYTES
}
}

View File

@ -0,0 +1,231 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.parser;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.zip.ZipException;
import org.eclipse.jetty.spdy.CompressionDictionary;
import org.eclipse.jetty.spdy.CompressionFactory;
import org.eclipse.jetty.spdy.SessionException;
import org.eclipse.jetty.spdy.StreamException;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.SessionStatus;
import org.eclipse.jetty.spdy.api.StreamStatus;
public abstract class HeadersBlockParser
{
private final CompressionFactory.Decompressor decompressor;
private byte[] data;
private boolean needsDictionary = true;
protected HeadersBlockParser(CompressionFactory.Decompressor decompressor)
{
this.decompressor = decompressor;
}
public boolean parse(int streamId, short version, int length, ByteBuffer buffer)
{
// Need to be sure that all the compressed data has arrived
// Because SPDY uses SYNC_FLUSH mode, and the Java API
// does not expose when decompression is finished with this mode
// (but only when using NO_FLUSH), then we need to
// accumulate the compressed bytes until we have all of them
boolean accumulated = accumulate(length, buffer);
if (!accumulated)
return false;
byte[] compressedHeaders = data;
data = null;
ByteBuffer decompressedHeaders = decompress(version, compressedHeaders);
Charset iso1 = Charset.forName("ISO-8859-1");
// We know the decoded bytes contain the full headers,
// so optimize instead of looping byte by byte
int count = readCount(version, decompressedHeaders);
for (int i = 0; i < count; ++i)
{
int nameLength = readNameLength(version, decompressedHeaders);
if (nameLength == 0)
throw new StreamException(streamId, StreamStatus.PROTOCOL_ERROR, "Invalid header name length");
byte[] nameBytes = new byte[nameLength];
decompressedHeaders.get(nameBytes);
String name = new String(nameBytes, iso1);
int valueLength = readValueLength(version, decompressedHeaders);
if (valueLength == 0)
throw new StreamException(streamId, StreamStatus.PROTOCOL_ERROR, "Invalid header value length");
byte[] valueBytes = new byte[valueLength];
decompressedHeaders.get(valueBytes);
String value = new String(valueBytes, iso1);
// Multi valued headers are separate by NUL
String[] values = value.split("\u0000");
// Check if there are multiple NULs (section 2.6.9)
for (String v : values)
if (v.length() == 0)
throw new StreamException(streamId, StreamStatus.PROTOCOL_ERROR, "Invalid multi valued header");
onHeader(name, values);
}
return true;
}
private boolean accumulate(int length, ByteBuffer buffer)
{
int remaining = buffer.remaining();
if (data == null)
{
if (remaining < length)
{
data = new byte[remaining];
buffer.get(data);
return false;
}
else
{
data = new byte[length];
buffer.get(data);
return true;
}
}
else
{
int accumulated = data.length;
int needed = length - accumulated;
if (remaining < needed)
{
byte[] local = new byte[accumulated + remaining];
System.arraycopy(data, 0, local, 0, accumulated);
buffer.get(local, accumulated, remaining);
data = local;
return false;
}
else
{
byte[] local = new byte[length];
System.arraycopy(data, 0, local, 0, accumulated);
buffer.get(local, accumulated, needed);
data = local;
return true;
}
}
}
private int readCount(int version, ByteBuffer buffer)
{
switch (version)
{
case SPDY.V2:
return buffer.getShort();
case SPDY.V3:
return buffer.getInt();
default:
throw new IllegalStateException();
}
}
private int readNameLength(int version, ByteBuffer buffer)
{
return readCount(version, buffer);
}
private int readValueLength(int version, ByteBuffer buffer)
{
return readCount(version, buffer);
}
protected abstract void onHeader(String name, String[] values);
private ByteBuffer decompress(short version, byte[] compressed)
{
// Differently from compression, decompression always happens
// non-concurrently because we read and parse with a single
// thread, and therefore there is no need for synchronization.
try
{
byte[] decompressed = null;
byte[] buffer = new byte[compressed.length * 2];
decompressor.setInput(compressed);
while (true)
{
int count = decompressor.decompress(buffer);
if (count == 0)
{
if (decompressed != null)
{
return ByteBuffer.wrap(decompressed);
}
else if (needsDictionary)
{
decompressor.setDictionary(CompressionDictionary.get(version));
needsDictionary = false;
}
else
{
throw new IllegalStateException();
}
}
else
{
if (count < buffer.length)
{
if (decompressed == null)
{
// Only one pass was needed to decompress
return ByteBuffer.wrap(buffer, 0, count);
}
else
{
// Last pass needed to decompress, merge decompressed bytes
byte[] result = new byte[decompressed.length + count];
System.arraycopy(decompressed, 0, result, 0, decompressed.length);
System.arraycopy(buffer, 0, result, decompressed.length, count);
return ByteBuffer.wrap(result);
}
}
else
{
if (decompressed == null)
{
decompressed = buffer;
buffer = new byte[buffer.length];
}
else
{
byte[] result = new byte[decompressed.length + buffer.length];
System.arraycopy(decompressed, 0, result, 0, decompressed.length);
System.arraycopy(buffer, 0, result, decompressed.length, buffer.length);
decompressed = result;
}
}
}
}
}
catch (ZipException x)
{
// We had a compression problem, and since the compression context
// is per-connection, we need to tear down the connection
throw new SessionException(SessionStatus.PROTOCOL_ERROR, x);
}
}
}

View File

@ -0,0 +1,129 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.CompressionFactory;
import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.HeadersInfo;
import org.eclipse.jetty.spdy.frames.ControlFrameType;
import org.eclipse.jetty.spdy.frames.HeadersFrame;
public class HeadersBodyParser extends ControlFrameBodyParser
{
private final Headers headers = new Headers();
private final ControlFrameParser controlFrameParser;
private final HeadersBlockParser headersBlockParser;
private State state = State.STREAM_ID;
private int cursor;
private int streamId;
public HeadersBodyParser(CompressionFactory.Decompressor decompressor, ControlFrameParser controlFrameParser)
{
this.controlFrameParser = controlFrameParser;
this.headersBlockParser = new HeadersHeadersBlockParser(decompressor);
}
@Override
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())
{
switch (state)
{
case STREAM_ID:
{
if (buffer.remaining() >= 4)
{
streamId = buffer.getInt() & 0x7F_FF_FF_FF;
state = State.HEADERS;
}
else
{
state = State.STREAM_ID_BYTES;
cursor = 4;
}
break;
}
case STREAM_ID_BYTES:
{
byte currByte = buffer.get();
--cursor;
streamId += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
{
streamId &= 0x7F_FF_FF_FF;
state = State.HEADERS;
}
break;
}
case HEADERS:
{
short version = controlFrameParser.getVersion();
int length = controlFrameParser.getLength() - 4;
if (headersBlockParser.parse(streamId, version, length, buffer))
{
byte flags = controlFrameParser.getFlags();
if (flags != 0 && flags != HeadersInfo.FLAG_CLOSE && flags != HeadersInfo.FLAG_RESET_COMPRESSION)
throw new IllegalArgumentException("Invalid flag " + flags + " for frame " + ControlFrameType.HEADERS);
HeadersFrame frame = new HeadersFrame(version, flags, streamId, new Headers(headers, true));
controlFrameParser.onControlFrame(frame);
reset();
return true;
}
break;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
private void reset()
{
headers.clear();
state = State.STREAM_ID;
cursor = 0;
streamId = 0;
}
private enum State
{
STREAM_ID, STREAM_ID_BYTES, HEADERS
}
private class HeadersHeadersBlockParser extends HeadersBlockParser
{
public HeadersHeadersBlockParser(CompressionFactory.Decompressor decompressor)
{
super(decompressor);
}
@Override
protected void onHeader(String name, String[] values)
{
for (String value : values)
headers.add(name, value);
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.frames.NoOpFrame;
public class NoOpBodyParser extends ControlFrameBodyParser
{
private final ControlFrameParser controlFrameParser;
public NoOpBodyParser(ControlFrameParser controlFrameParser)
{
this.controlFrameParser = controlFrameParser;
}
@Override
public boolean parse(ByteBuffer buffer)
{
NoOpFrame frame = new NoOpFrame();
controlFrameParser.onControlFrame(frame);
return true;
}
}

View File

@ -0,0 +1,229 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.parser;
import java.nio.ByteBuffer;
import java.util.EventListener;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jetty.spdy.CompressionFactory;
import org.eclipse.jetty.spdy.SessionException;
import org.eclipse.jetty.spdy.StreamException;
import org.eclipse.jetty.spdy.api.SessionStatus;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.DataFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Parser
{
private static final Logger logger = LoggerFactory.getLogger(Parser.class);
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
private final ControlFrameParser controlFrameParser;
private final DataFrameParser dataFrameParser;
private State state = State.CONTROL_BIT;
public Parser(CompressionFactory.Decompressor decompressor)
{
// It is important to allocate one decompression context per
// SPDY session for the control frames (to decompress the headers)
controlFrameParser = new ControlFrameParser(decompressor)
{
@Override
protected void onControlFrame(ControlFrame frame)
{
logger.debug("Parsed {}", frame);
notifyControlFrame(frame);
}
};
dataFrameParser = new DataFrameParser()
{
@Override
protected void onDataFrame(DataFrame frame, ByteBuffer data)
{
logger.debug("Parsed {}, {} data bytes", frame, data.remaining());
notifyDataFrame(frame, data);
}
};
}
public void addListener(Listener listener)
{
listeners.add(listener);
}
public void removeListener(Listener listener)
{
listeners.remove(listener);
}
protected void notifyControlFrame(ControlFrame frame)
{
for (Listener listener : listeners)
{
try
{
listener.onControlFrame(frame);
}
catch (Exception x)
{
logger.info("Exception while notifying listener " + listener, x);
}
}
}
protected void notifyDataFrame(DataFrame frame, ByteBuffer data)
{
for (Listener listener : listeners)
{
try
{
listener.onDataFrame(frame, data);
}
catch (Exception x)
{
logger.info("Exception while notifying listener " + listener, x);
}
}
}
protected void notifyStreamException(StreamException x)
{
for (Listener listener : listeners)
{
listener.onStreamException(x);
}
}
protected void notifySessionException(SessionException x)
{
logger.debug("SPDY session exception", x);
for (Listener listener : listeners)
{
try
{
listener.onSessionException(x);
}
catch (Exception xx)
{
logger.debug("Could not notify listener " + listener, xx);
}
}
}
public void parse(ByteBuffer buffer)
{
try
{
logger.debug("Parsing {} bytes", buffer.remaining());
while (buffer.hasRemaining())
{
switch (state)
{
case CONTROL_BIT:
{
// We must only peek the first byte and not advance the buffer
// because the 7 least significant bits may be relevant in data frames
int currByte = buffer.get(buffer.position());
boolean isControlFrame = (currByte & 0x80) == 0x80;
state = isControlFrame ? State.CONTROL_FRAME : State.DATA_FRAME;
break;
}
case CONTROL_FRAME:
{
if (controlFrameParser.parse(buffer))
reset();
break;
}
case DATA_FRAME:
{
if (dataFrameParser.parse(buffer))
reset();
break;
}
default:
{
throw new IllegalStateException();
}
}
}
}
catch (SessionException x)
{
notifySessionException(x);
}
catch (StreamException x)
{
notifyStreamException(x);
}
catch (Throwable x)
{
notifySessionException(new SessionException(SessionStatus.PROTOCOL_ERROR, x));
}
finally
{
// Be sure to consume after exceptions
buffer.position(buffer.limit());
}
}
private void reset()
{
state = State.CONTROL_BIT;
}
public interface Listener extends EventListener
{
public void onControlFrame(ControlFrame frame);
public void onDataFrame(DataFrame frame, ByteBuffer data);
public void onStreamException(StreamException x);
public void onSessionException(SessionException x);
public static class Adapter implements Listener
{
@Override
public void onControlFrame(ControlFrame frame)
{
}
@Override
public void onDataFrame(DataFrame frame, ByteBuffer data)
{
}
@Override
public void onStreamException(StreamException x)
{
}
@Override
public void onSessionException(SessionException x)
{
}
}
}
private enum State
{
CONTROL_BIT, CONTROL_FRAME, DATA_FRAME
}
}

View File

@ -0,0 +1,96 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.frames.PingFrame;
public class PingBodyParser extends ControlFrameBodyParser
{
private final ControlFrameParser controlFrameParser;
private State state = State.PING_ID;
private int cursor;
private int pingId;
public PingBodyParser(ControlFrameParser controlFrameParser)
{
this.controlFrameParser = controlFrameParser;
}
@Override
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())
{
switch (state)
{
case PING_ID:
{
if (buffer.remaining() >= 4)
{
pingId = buffer.getInt() & 0x7F_FF_FF_FF;
onPing();
return true;
}
else
{
state = State.PING_ID_BYTES;
cursor = 4;
}
break;
}
case PING_ID_BYTES:
{
byte currByte = buffer.get();
--cursor;
pingId += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
{
onPing();
return true;
}
break;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
private void onPing()
{
PingFrame frame = new PingFrame(controlFrameParser.getVersion(), pingId);
controlFrameParser.onControlFrame(frame);
reset();
}
private void reset()
{
state = State.PING_ID;
cursor = 0;
pingId = 0;
}
private enum State
{
PING_ID, PING_ID_BYTES
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.frames.RstStreamFrame;
public class RstStreamBodyParser extends ControlFrameBodyParser
{
private final ControlFrameParser controlFrameParser;
private State state = State.STREAM_ID;
private int cursor;
private int streamId;
private int statusCode;
public RstStreamBodyParser(ControlFrameParser controlFrameParser)
{
this.controlFrameParser = controlFrameParser;
}
@Override
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())
{
switch (state)
{
case STREAM_ID:
{
if (buffer.remaining() >= 4)
{
streamId = buffer.getInt() & 0x7F_FF_FF_FF;
state = State.STATUS_CODE;
}
else
{
state = State.STREAM_ID_BYTES;
cursor = 4;
}
break;
}
case STREAM_ID_BYTES:
{
byte currByte = buffer.get();
--cursor;
streamId += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
{
streamId &= 0x7F_FF_FF_FF;
state = State.STATUS_CODE;
}
break;
}
case STATUS_CODE:
{
if (buffer.remaining() >= 4)
{
statusCode = buffer.getInt();
onRstStream();
return true;
}
else
{
state = State.STATUS_CODE_BYTES;
cursor = 4;
}
break;
}
case STATUS_CODE_BYTES:
{
byte currByte = buffer.get();
--cursor;
statusCode += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
{
onRstStream();
return true;
}
break;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
private void onRstStream()
{
// TODO: check that statusCode is not 0
RstStreamFrame frame = new RstStreamFrame(controlFrameParser.getVersion(), streamId, statusCode);
controlFrameParser.onControlFrame(frame);
reset();
}
private void reset()
{
state = State.STREAM_ID;
cursor = 0;
streamId = 0;
statusCode = 0;
}
private enum State
{
STREAM_ID, STREAM_ID_BYTES, STATUS_CODE, STATUS_CODE_BYTES
}
}

View File

@ -0,0 +1,198 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.Settings;
import org.eclipse.jetty.spdy.frames.SettingsFrame;
public class SettingsBodyParser extends ControlFrameBodyParser
{
private final Settings settings = new Settings();
private final ControlFrameParser controlFrameParser;
private State state = State.COUNT;
private int cursor;
private int count;
private int idAndFlags;
private int value;
public SettingsBodyParser(ControlFrameParser controlFrameParser)
{
this.controlFrameParser = controlFrameParser;
}
@Override
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())
{
switch (state)
{
case COUNT:
{
if (buffer.remaining() >= 4)
{
count = buffer.getInt();
state = State.ID_FLAGS;
}
else
{
state = State.COUNT_BYTES;
cursor = 4;
}
break;
}
case COUNT_BYTES:
{
byte currByte = buffer.get();
--cursor;
count += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
state = State.ID_FLAGS;
break;
}
case ID_FLAGS:
{
if (buffer.remaining() >= 4)
{
idAndFlags = convertIdAndFlags(controlFrameParser.getVersion(), buffer.getInt());
state = State.VALUE;
}
else
{
state = State.ID_FLAGS_BYTES;
cursor = 4;
}
break;
}
case ID_FLAGS_BYTES:
{
byte currByte = buffer.get();
--cursor;
value += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
{
idAndFlags = convertIdAndFlags(controlFrameParser.getVersion(), value);
state = State.VALUE;
}
break;
}
case VALUE:
{
if (buffer.remaining() >= 4)
{
value = buffer.getInt();
if (onPair())
return true;
}
else
{
state = State.VALUE_BYTES;
cursor = 4;
value = 0;
}
break;
}
case VALUE_BYTES:
{
byte currByte = buffer.get();
--cursor;
value += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
{
if (onPair())
return true;
}
break;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
private int convertIdAndFlags(short version, int idAndFlags)
{
switch (version)
{
case SPDY.V2:
{
// A bug in the Chromium implementation forces v2 to have
// 3 ID bytes little endian + 1 byte of flags
// Here we normalize this to conform with v3, which is
// 1 bytes of flag + 3 ID bytes big endian
int result = (idAndFlags & 0x00_00_00_FF) << 24;
result += (idAndFlags & 0x00_00_FF_00) << 8;
result += (idAndFlags & 0x00_FF_00_00) >>> 8;
result += (idAndFlags & 0xFF_00_00_00) >>> 24;
return result;
}
case SPDY.V3:
{
return idAndFlags;
}
default:
{
throw new IllegalStateException();
}
}
}
private boolean onPair()
{
int id = idAndFlags & 0x00_FF_FF_FF;
byte flags = (byte)((idAndFlags & 0xFF_00_00_00) >>> 24);
settings.put(new Settings.Setting(Settings.ID.from(id), Settings.Flag.from(flags), value));
state = State.ID_FLAGS;
idAndFlags = 0;
value = 0;
--count;
if (count == 0)
{
onSettings();
return true;
}
return false;
}
private void onSettings()
{
SettingsFrame frame = new SettingsFrame(controlFrameParser.getVersion(), controlFrameParser.getFlags(), new Settings(settings, true));
controlFrameParser.onControlFrame(frame);
reset();
}
private void reset()
{
settings.clear();
state = State.COUNT;
cursor = 0;
count = 0;
idAndFlags = 0;
value = 0;
}
private enum State
{
COUNT, COUNT_BYTES, ID_FLAGS, ID_FLAGS_BYTES, VALUE, VALUE_BYTES
}
}

View File

@ -0,0 +1,182 @@
/*
* Copyright (c) 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.jetty.spdy.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.CompressionFactory;
import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.ReplyInfo;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.frames.ControlFrameType;
import org.eclipse.jetty.spdy.frames.SynReplyFrame;
public class SynReplyBodyParser extends ControlFrameBodyParser
{
private final Headers headers = new Headers();
private final ControlFrameParser controlFrameParser;
private final HeadersBlockParser headersBlockParser;
private State state = State.STREAM_ID;
private int cursor;
private int streamId;
public SynReplyBodyParser(CompressionFactory.Decompressor decompressor, ControlFrameParser controlFrameParser)
{
this.controlFrameParser = controlFrameParser;
this.headersBlockParser = new SynReplyHeadersBlockParser(decompressor);
}
@Override
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())
{
switch (state)
{
case STREAM_ID:
{
if (buffer.remaining() >= 4)
{
streamId = buffer.getInt() & 0x7F_FF_FF_FF;
state = State.ADDITIONAL;
}
else
{
state = State.STREAM_ID_BYTES;
cursor = 4;
}
break;
}
case STREAM_ID_BYTES:
{
byte currByte = buffer.get();
--cursor;
streamId += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
{
streamId &= 0x7F_FF_FF_FF;
state = State.ADDITIONAL;
}
break;
}
case ADDITIONAL:
{
switch (controlFrameParser.getVersion())
{
case SPDY.V2:
{
if (buffer.remaining() >= 2)
{
buffer.getShort();
state = State.HEADERS;
}
else
{
state = State.ADDITIONAL_BYTES;
cursor = 2;
}
break;
}
case SPDY.V3:
{
state = State.HEADERS;
break;
}
default:
{
throw new IllegalStateException();
}
}
break;
}
case ADDITIONAL_BYTES:
{
assert controlFrameParser.getVersion() == SPDY.V2;
buffer.get();
--cursor;
if (cursor == 0)
state = State.HEADERS;
break;
}
case HEADERS:
{
short version = controlFrameParser.getVersion();
int length = controlFrameParser.getLength() - getSynReplyDataLength(version);
if (headersBlockParser.parse(streamId, version, length, buffer))
{
byte flags = controlFrameParser.getFlags();
if (flags != 0 && flags != ReplyInfo.FLAG_CLOSE)
throw new IllegalArgumentException("Invalid flag " + flags + " for frame " + ControlFrameType.SYN_REPLY);
SynReplyFrame frame = new SynReplyFrame(version, flags, streamId, new Headers(headers, true));
controlFrameParser.onControlFrame(frame);
reset();
return true;
}
break;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
private int getSynReplyDataLength(short version)
{
switch (version)
{
case 2:
return 6;
case 3:
return 4;
default:
throw new IllegalStateException();
}
}
private void reset()
{
headers.clear();
state = State.STREAM_ID;
cursor = 0;
streamId = 0;
}
private enum State
{
STREAM_ID, STREAM_ID_BYTES, ADDITIONAL, ADDITIONAL_BYTES, HEADERS
}
private class SynReplyHeadersBlockParser extends HeadersBlockParser
{
public SynReplyHeadersBlockParser(CompressionFactory.Decompressor decompressor)
{
super(decompressor);
}
@Override
protected void onHeader(String name, String[] values)
{
for (String value : values)
headers.add(name, value);
}
}
}

Some files were not shown because too many files have changed in this diff Show More