SPDY refactorings.

This commit is contained in:
Simone Bordet 2012-05-08 18:20:21 +02:00
parent 2b26d2f3ee
commit f0421723b8
152 changed files with 19098 additions and 0 deletions

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

@ -0,0 +1,63 @@
<?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>9.0.0-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>
<properties>
<npn.version>1.0.0.v20120402</npn.version>
</properties>
<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,42 @@
<?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>9.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spdy-core</artifactId>
<name>Jetty :: SPDY :: Core</name>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</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,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.util.Callback;
public interface Controller<T>
{
public int write(ByteBuffer buffer, Callback<T> callback, T context);
public void close(boolean onlyOutput);
}

View File

@ -0,0 +1,38 @@
/*
* 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.Session;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.util.Callback;
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, Callback<C> callback, C context);
public <C> void data(IStream stream, DataInfo dataInfo, long timeout, TimeUnit unit, Callback<C> callback, C context);
}

View File

@ -0,0 +1,115 @@
/*
* 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
* @param local whether the close is local or remote
*/
public void updateCloseState(boolean close, boolean local);
/**
* <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 given 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);
/**
* <p>Associate the given {@link IStream} to this {@link IStream}.</p>
*
* @param stream the stream to associate with this stream
*/
public void associate(IStream stream);
/**
* <p>remove the given associated {@link IStream} from this stream</p>
*
* @param stream the stream to be removed
*/
public void disassociate(IStream stream);
/**
* <p>Overrides Stream.getAssociatedStream() to return an instance of IStream instead of Stream
*
* @see Stream#getAssociatedStream()
*/
@Override
public IStream getAssociatedStream();
}

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,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;
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.util.Callback;
/**
* <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 Callback<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
{
boolean elapsed = !latch.await(timeout, unit);
if (elapsed)
throw new TimeoutException();
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 @@
package org.eclipse.jetty.spdy;
import org.eclipse.jetty.spdy.api.SynInfo;
/* ------------------------------------------------------------ */
/**
* <p>A subclass container of {@link SynInfo} for unidirectional streams</p>
*/
public class PushSynInfo extends SynInfo
{
public static final byte FLAG_UNIDIRECTIONAL = 2;
private int associatedStreamId;
public PushSynInfo(int associatedStreamId, SynInfo synInfo){
super(synInfo.getHeaders(), synInfo.isClose(), synInfo.getPriority());
this.associatedStreamId = associatedStreamId;
}
/**
* @return the close and unidirectional flags as integer
* @see #FLAG_CLOSE
* @see #FLAG_UNIDIRECTIONAL
*/
@Override
public byte getFlags()
{
byte flags = super.getFlags();
flags += FLAG_UNIDIRECTIONAL;
return flags;
}
/**
* @return the id of the associated stream
*/
public int getAssociatedStreamId()
{
return associatedStreamId;
}
}

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;
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;
}
public SessionStatus getSessionStatus()
{
return sessionStatus;
}
}

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,486 @@
/*
* 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.Collections;
import java.util.Map;
import java.util.Set;
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.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.api.SynInfo;
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.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class StandardStream implements IStream
{
private static final Logger logger = Log.getLogger(Stream.class);
private final Map<String, Object> attributes = new ConcurrentHashMap<>();
private final IStream associatedStream;
private final SynStreamFrame frame;
private final ISession session;
private final AtomicInteger windowSize;
private final Set<Stream> pushedStreams = Collections.newSetFromMap(new ConcurrentHashMap<Stream, Boolean>());
private volatile StreamFrameListener listener;
private volatile OpenState openState = OpenState.SYN_SENT;
private volatile CloseState closeState = CloseState.OPENED;
private volatile boolean reset = false;
public StandardStream(SynStreamFrame frame, ISession session, int windowSize, IStream associatedStream)
{
this.frame = frame;
this.session = session;
this.windowSize = new AtomicInteger(windowSize);
this.associatedStream = associatedStream;
}
@Override
public int getId()
{
return frame.getStreamId();
}
@Override
public IStream getAssociatedStream()
{
return associatedStream;
}
@Override
public Set<Stream> getPushedStreams()
{
return pushedStreams;
}
@Override
public void associate(IStream stream)
{
pushedStreams.add(stream);
}
@Override
public void disassociate(IStream stream)
{
pushedStreams.remove(stream);
}
@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;
}
@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, boolean local)
{
if (close)
{
switch (closeState)
{
case OPENED:
{
closeState = local?CloseState.LOCALLY_CLOSED:CloseState.REMOTELY_CLOSED;
break;
}
case LOCALLY_CLOSED:
{
if (local)
throw new IllegalStateException();
else
closeState = CloseState.CLOSED;
break;
}
case REMOTELY_CLOSED:
{
if (local)
closeState = CloseState.CLOSED;
else
throw new IllegalStateException();
break;
}
default:
{
throw new IllegalStateException();
}
}
}
}
@Override
public void process(ControlFrame frame)
{
switch (frame.getType())
{
case SYN_STREAM:
{
openState = OpenState.SYN_RECV;
break;
}
case SYN_REPLY:
{
openState = OpenState.REPLY_RECV;
SynReplyFrame synReply = (SynReplyFrame)frame;
updateCloseState(synReply.isClose(),false);
ReplyInfo replyInfo = new ReplyInfo(synReply.getHeaders(),synReply.isClose());
notifyOnReply(replyInfo);
break;
}
case HEADERS:
{
HeadersFrame headers = (HeadersFrame)frame;
updateCloseState(headers.isClose(),false);
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:
{
reset = true;
break;
}
default:
{
throw new IllegalStateException();
}
}
session.flush();
}
@Override
public void process(DataFrame frame, ByteBuffer data)
{
// TODO: in v3 we need to send a rst instead of just ignoring
// ignore data frame if this stream is remotelyClosed already
if (isHalfClosed() && !isLocallyClosed())
{
logger.debug("Ignoring received dataFrame as this stream is remotely closed: " + frame);
return;
}
if (!canReceive())
{
logger.debug("Can't receive. Sending rst: " + frame);
session.rst(new RstInfo(getId(),StreamStatus.PROTOCOL_ERROR));
return;
}
updateCloseState(frame.isClose(),false);
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<Stream> syn(SynInfo synInfo)
{
Promise<Stream> result = new Promise<>();
syn(synInfo,0,TimeUnit.MILLISECONDS,result);
return result;
}
@Override
public void syn(SynInfo synInfo, long timeout, TimeUnit unit, Callback<Stream> callback)
{
if (isClosed() || isReset())
{
callback.failed(new StreamException(getId(),StreamStatus.STREAM_ALREADY_CLOSED));
return;
}
PushSynInfo pushSynInfo = new PushSynInfo(getId(),synInfo);
session.syn(pushSynInfo,null,timeout,unit, callback);
}
@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, Callback<Void> callback)
{
if (isUnidirectional())
throw new IllegalStateException("Protocol violation: cannot send SYN_REPLY frames in unidirectional streams");
openState = OpenState.REPLY_SENT;
updateCloseState(replyInfo.isClose(),true);
SynReplyFrame frame = new SynReplyFrame(session.getVersion(),replyInfo.getFlags(),getId(),replyInfo.getHeaders());
session.control(this,frame,timeout,unit, callback,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, Callback<Void> callback)
{
if (!canSend())
{
session.rst(new RstInfo(getId(),StreamStatus.PROTOCOL_ERROR));
throw new IllegalStateException("Protocol violation: cannot send a DATA frame before a SYN_REPLY frame");
}
if (isLocallyClosed())
{
session.rst(new RstInfo(getId(),StreamStatus.PROTOCOL_ERROR));
throw new IllegalStateException("Protocol violation: cannot send a DATA frame on a closed stream");
}
// 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, callback,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, Callback<Void> callback)
{
if (!canSend())
{
session.rst(new RstInfo(getId(),StreamStatus.PROTOCOL_ERROR));
throw new IllegalStateException("Protocol violation: cannot send a HEADERS frame before a SYN_REPLY frame");
}
if (isLocallyClosed())
{
session.rst(new RstInfo(getId(),StreamStatus.PROTOCOL_ERROR));
throw new IllegalStateException("Protocol violation: cannot send a HEADERS frame on a closed stream");
}
updateCloseState(headersInfo.isClose(),true);
HeadersFrame frame = new HeadersFrame(session.getVersion(),headersInfo.getFlags(),getId(),headersInfo.getHeaders());
session.control(this,frame,timeout,unit, callback,null);
}
@Override
public boolean isUnidirectional()
{
return associatedStream != null;
}
@Override
public boolean isReset()
{
return reset;
}
@Override
public boolean isHalfClosed()
{
CloseState closeState = this.closeState;
return closeState == CloseState.LOCALLY_CLOSED || closeState == CloseState.REMOTELY_CLOSED || closeState == CloseState.CLOSED;
}
@Override
public boolean isClosed()
{
return closeState == CloseState.CLOSED;
}
private boolean isLocallyClosed()
{
CloseState closeState = this.closeState;
return closeState == CloseState.LOCALLY_CLOSED || closeState == CloseState.CLOSED;
}
@Override
public String toString()
{
return String.format("stream=%d v%d %s",getId(),session.getVersion(),closeState);
}
private boolean canSend()
{
OpenState openState = this.openState;
return openState == OpenState.SYN_SENT || openState == OpenState.REPLY_RECV || openState == OpenState.REPLY_SENT;
}
private boolean canReceive()
{
OpenState openState = this.openState;
return openState == OpenState.SYN_RECV || openState == OpenState.REPLY_RECV || openState == OpenState.REPLY_SENT;
}
private enum OpenState
{
SYN_SENT, SYN_RECV, REPLY_SENT, REPLY_RECV
}
private enum CloseState
{
OPENED, LOCALLY_CLOSED, REMOTELY_CLOSED, CLOSED
}
}

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;
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 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 the 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,285 @@
/*
* 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)
{
if (header != null)
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,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.api;
import java.util.EventListener;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.Callback;
/**
* <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, Callback)
*/
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 callback the completion handler that gets notified of stream creation
* @see #syn(SynInfo, StreamFrameListener)
*/
public void syn(SynInfo synInfo, StreamFrameListener listener, long timeout, TimeUnit unit, Callback<Stream> callback);
/**
* <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, Callback)
*/
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 callback the completion handler that gets notified of reset's send
* @see #rst(RstInfo)
*/
public void rst(RstInfo rstInfo, long timeout, TimeUnit unit, Callback<Void> callback);
/**
* <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, Callback)
*/
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 callback the completion handler that gets notified of settings' send
* @see #settings(SettingsInfo)
*/
public void settings(SettingsInfo settingsInfo, long timeout, TimeUnit unit, Callback<Void> callback);
/**
* <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, Callback)
*/
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 callback the completion handler that gets notified of ping's send
* @see #ping()
*/
public void ping(long timeout, TimeUnit unit, Callback<PingInfo> callback);
/**
* <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, Callback)
*/
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 callback the completion handler that gets notified of go away's send
* @see #goAway()
*/
public void goAway(long timeout, TimeUnit unit, Callback<Void> callback);
/**
* @return the streams currently active in this session
*/
public Set<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,159 @@
/*
* 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 org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* <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>Callback invoked when an exception is thrown during the processing of an event on a
* SPDY session.</p>
* <p>Examples of such conditions are invalid frames received, corrupted headers compression state, etc.</p>
*
* @param x the exception that caused the event processing failure
*/
public void onException(Throwable x);
/**
* <p>Empty implementation of {@link SessionFrameListener}</p>
*/
public static class Adapter implements SessionFrameListener
{
private static final Logger logger = Log.getLogger(Adapter.class);
@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)
{
}
@Override
public void onException(Throwable x)
{
logger.info("", x);
}
}
}

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,244 @@
/*
* 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.Set;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.Callback;
/**
* <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, Callback)} 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>Initiate a unidirectional spdy pushstream associated to this stream asynchronously<p>
* <p>Callers may use the returned future to get the pushstream once it got created</p>
*
* @param synInfo the metadata to send on stream creation
* @return a future containing the stream once it got established
* @see #syn(SynInfo, long, TimeUnit, Callback)
*/
public Future<Stream> syn(SynInfo synInfo);
/**
* <p>Initiate a unidirectional spdy pushstream associated to this stream asynchronously<p>
* <p>Callers may pass a non-null completion handler to be notified of when the
* pushstream has been established.</p>
*
* @param synInfo the metadata to send on stream creation
* @param timeout the operation's timeout
* @param unit the timeout's unit
* @param callback the completion handler that gets notified once the pushstream is established
* @see #syn(SynInfo)
*/
public void syn(SynInfo synInfo, long timeout, TimeUnit unit, Callback<Stream> callback);
/**
* <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, Callback)
* @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 callback the completion handler that gets notified of reply sent
* @see #reply(ReplyInfo)
*/
public void reply(ReplyInfo replyInfo, long timeout, TimeUnit unit, Callback<Void> callback);
/**
* <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, Callback)
* @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 callback the completion handler that gets notified of data sent
* @see #data(DataInfo)
*/
public void data(DataInfo dataInfo, long timeout, TimeUnit unit, Callback<Void> callback);
/**
* <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, Callback)
* @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 callback the completion handler that gets notified of headers sent
* @see #headers(HeadersInfo)
*/
public void headers(HeadersInfo headersInfo, long timeout, TimeUnit unit, Callback<Void> callback);
/**
* @return whether this stream is unidirectional or not
*/
public boolean isUnidirectional();
/**
* @return whether this stream has been reset
*/
public boolean isReset();
/**
* @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()
*/
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);
/**
* @return the associated parent stream or null if this is not an associated stream
*/
public Stream getAssociatedStream();
/**
* @return associated child streams or an empty set if no associated streams exist
*/
public Set<Stream> getPushedStreams();
}

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, 6),
/**
* <p>The stream status indicating a flow control error</p>
*/
FLOW_CONTROL_ERROR(7, 7),
/**
* <p>The stream status indicating a stream opened more than once</p>
*/
STREAM_IN_USE(-1, 8),
/**
* <p>The stream status indicating data on a stream already closed</p>
*/
STREAM_ALREADY_CLOSED(-1, 9),
/**
* <p>The stream status indicating credentials not valid</p>
*/
INVALID_CREDENTIALS(-1, 10),
/**
* <p>The stream status indicating that the implementation could not support a frame too large</p>
*/
FRAME_TOO_LARGE(-1, 11);
/**
* @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,116 @@
/*
* 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;
private final boolean close;
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, (byte)0);
}
/**
* <p>
* Creates a {@link ReplyInfo} instance with the given headers, the given close flag and with the given priority.
* </p>
*
* @param headers
* the {@link Headers}
* @param close
* the value of the close flag
* @param priority
* the priority
*/
public SynInfo(Headers headers, boolean close, byte priority)
{
this.close = close;
this.priority = priority;
this.headers = headers;
}
/**
* @return the value of the close flag
*/
public boolean isClose()
{
return close;
}
/**
* @return the priority
*/
public byte getPriority()
{
return priority;
}
/**
* @return the {@link Headers}
*/
public Headers getHeaders()
{
return headers;
}
/**
* @return the close flag as integer
* @see #FLAG_CLOSE
*/
public byte getFlags()
{
return isClose() ? FLAG_CLOSE : 0;
}
@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,74 @@
/*
* 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.PushSynInfo;
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() & PushSynInfo.FLAG_UNIDIRECTIONAL) == PushSynInfo.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 & 0xFF);
// 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,186 @@
/*
* 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.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);
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;
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 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,157 @@
/*
* 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;
}
default:
{
throw new IllegalStateException();
}
}
}
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;
}
default:
{
throw new IllegalStateException();
}
}
}
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.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class Parser
{
private static final Logger logger = Log.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);
}
}
}

View File

@ -0,0 +1,208 @@
/*
* 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.PushSynInfo;
import org.eclipse.jetty.spdy.StreamException;
import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.StreamStatus;
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.spdy.frames.ControlFrameType;
import org.eclipse.jetty.spdy.frames.SynStreamFrame;
public class SynStreamBodyParser 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;
private int associatedStreamId;
private byte priority;
public SynStreamBodyParser(CompressionFactory.Decompressor decompressor, ControlFrameParser controlFrameParser)
{
this.controlFrameParser = controlFrameParser;
this.headersBlockParser = new SynStreamHeadersBlockParser(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.ASSOCIATED_STREAM_ID;
}
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.ASSOCIATED_STREAM_ID;
}
break;
}
case ASSOCIATED_STREAM_ID:
{
// Now we know the streamId, we can do the version check
// and if it is wrong, issue a RST_STREAM
checkVersion(controlFrameParser.getVersion(), streamId);
if (buffer.remaining() >= 4)
{
associatedStreamId = buffer.getInt() & 0x7F_FF_FF_FF;
state = State.PRIORITY;
}
else
{
state = State.ASSOCIATED_STREAM_ID_BYTES;
cursor = 4;
}
break;
}
case ASSOCIATED_STREAM_ID_BYTES:
{
byte currByte = buffer.get();
--cursor;
associatedStreamId += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
{
associatedStreamId &= 0x7F_FF_FF_FF;
state = State.PRIORITY;
}
break;
}
case PRIORITY:
{
byte currByte = buffer.get();
++cursor;
if (cursor == 1)
{
priority = readPriority(controlFrameParser.getVersion(), currByte);
}
else
{
// Unused byte after priority, skip it
cursor = 0;
state = State.HEADERS;
}
break;
}
case HEADERS:
{
short version = controlFrameParser.getVersion();
int length = controlFrameParser.getLength() - 10;
if (headersBlockParser.parse(streamId, version, length, buffer))
{
byte flags = controlFrameParser.getFlags();
if (flags > (SynInfo.FLAG_CLOSE | PushSynInfo.FLAG_UNIDIRECTIONAL))
throw new IllegalArgumentException("Invalid flag " + flags + " for frame " + ControlFrameType.SYN_STREAM);
SynStreamFrame frame = new SynStreamFrame(version, flags, streamId, associatedStreamId, priority, new Headers(headers, true));
controlFrameParser.onControlFrame(frame);
reset();
return true;
}
break;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
private void checkVersion(short version, int streamId)
{
if (version != SPDY.V2 && version != SPDY.V3)
throw new StreamException(streamId, StreamStatus.UNSUPPORTED_VERSION);
}
private byte readPriority(short version, byte currByte)
{
// Right shift retains the sign bit when operated on a byte,
// so we use an int to perform the shifts
switch (version)
{
case SPDY.V2:
int p2 = currByte & 0b1100_0000;
p2 >>>= 6;
return (byte)p2;
case SPDY.V3:
int p3 = currByte & 0b1110_0000;
p3 >>>= 5;
return (byte)p3;
default:
throw new IllegalStateException();
}
}
private void reset()
{
headers.clear();
state = State.STREAM_ID;
cursor = 0;
streamId = 0;
associatedStreamId = 0;
priority = 0;
}
private enum State
{
STREAM_ID, STREAM_ID_BYTES, ASSOCIATED_STREAM_ID, ASSOCIATED_STREAM_ID_BYTES, PRIORITY, HEADERS
}
private class SynStreamHeadersBlockParser extends HeadersBlockParser
{
public SynStreamHeadersBlockParser(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,70 @@
/*
* 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 class UnknownControlFrameBodyParser extends ControlFrameBodyParser
{
private final ControlFrameParser controlFrameParser;
private State state = State.BODY;
private int remaining;
public UnknownControlFrameBodyParser(ControlFrameParser controlFrameParser)
{
this.controlFrameParser = controlFrameParser;
}
@Override
public boolean parse(ByteBuffer buffer)
{
switch (state)
{
case BODY:
{
remaining = controlFrameParser.getLength();
state = State.CONSUME;
// Fall down
}
case CONSUME:
{
int consume = Math.min(remaining, buffer.remaining());
buffer.position(buffer.position() + consume);
remaining -= consume;
if (remaining > 0)
return false;
reset();
return true;
}
default:
{
throw new IllegalStateException();
}
}
}
private void reset()
{
state = State.BODY;
remaining = 0;
}
private enum State
{
BODY, CONSUME
}
}

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.WindowUpdateFrame;
public class WindowUpdateBodyParser extends ControlFrameBodyParser
{
private final ControlFrameParser controlFrameParser;
private State state = State.STREAM_ID;
private int cursor;
private int streamId;
private int windowDelta;
public WindowUpdateBodyParser(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.WINDOW_DELTA;
}
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.WINDOW_DELTA;
}
break;
}
case WINDOW_DELTA:
{
if (buffer.remaining() >= 4)
{
windowDelta = buffer.getInt() & 0x7F_FF_FF_FF;
onWindowUpdate();
return true;
}
else
{
state = State.WINDOW_DELTA_BYTES;
cursor = 4;
}
break;
}
case WINDOW_DELTA_BYTES:
{
byte currByte = buffer.get();
--cursor;
windowDelta += (currByte & 0xFF) << 8 * cursor;
if (cursor == 0)
{
windowDelta &= 0x7F_FF_FF_FF;
onWindowUpdate();
return true;
}
break;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
private void onWindowUpdate()
{
WindowUpdateFrame frame = new WindowUpdateFrame(controlFrameParser.getVersion(), streamId, windowDelta);
controlFrameParser.onControlFrame(frame);
reset();
}
private void reset()
{
state = State.STREAM_ID;
cursor = 0;
streamId = 0;
windowDelta = 0;
}
private enum State
{
STREAM_ID, STREAM_ID_BYTES, WINDOW_DELTA, WINDOW_DELTA_BYTES;
}
}

View File

@ -0,0 +1,146 @@
/*
* 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.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.SPDYException;
import org.eclipse.jetty.spdy.api.Session;
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StringDataInfo;
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.spdy.generator.Generator;
import org.eclipse.jetty.util.Callback;
import org.junit.Assert;
import org.junit.Test;
public class AsyncTimeoutTest
{
@Test
public void testAsyncTimeoutInControlFrames() throws Exception
{
final long timeout = 1000;
final TimeUnit unit = TimeUnit.MILLISECONDS;
ByteBufferPool bufferPool = new StandardByteBufferPool();
Executor threadPool = Executors.newCachedThreadPool();
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory.StandardCompressor());
Session session = new StandardSession(SPDY.V2, bufferPool, threadPool, scheduler, new TestController(), null, 1, null, generator)
{
@Override
public void flush()
{
try
{
unit.sleep(2 * timeout);
super.flush();
}
catch (InterruptedException x)
{
throw new SPDYException(x);
}
}
};
final CountDownLatch failedLatch = new CountDownLatch(1);
session.syn(new SynInfo(true), null, timeout, unit, new Callback<Stream>()
{
@Override
public void completed(Stream stream)
{
}
@Override
public void failed(Throwable x)
{
failedLatch.countDown();
}
});
Assert.assertTrue(failedLatch.await(2 * timeout, unit));
}
@Test
public void testAsyncTimeoutInDataFrames() throws Exception
{
final long timeout = 1000;
final TimeUnit unit = TimeUnit.MILLISECONDS;
ByteBufferPool bufferPool = new StandardByteBufferPool();
Executor threadPool = Executors.newCachedThreadPool();
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory.StandardCompressor());
Session session = new StandardSession(SPDY.V2, bufferPool, threadPool, scheduler, new TestController(), null, 1, null, generator)
{
@Override
protected void write(ByteBuffer buffer, Callback<FrameBytes> handler, FrameBytes frameBytes)
{
try
{
// Wait if we're writing the data frame (control frame's first byte is 0x80)
if (buffer.get(0) == 0)
unit.sleep(2 * timeout);
super.write(buffer, handler, frameBytes);
}
catch (InterruptedException x)
{
throw new SPDYException(x);
}
}
};
Stream stream = session.syn(new SynInfo(false), null).get(5, TimeUnit.SECONDS);
final CountDownLatch failedLatch = new CountDownLatch(1);
stream.data(new StringDataInfo("data", true), timeout, unit, new Callback<Void>()
{
@Override
public void completed(Void context)
{
}
@Override
public void failed(Throwable x)
{
failedLatch.countDown();
}
});
Assert.assertTrue(failedLatch.await(2 * timeout, unit));
}
private static class TestController implements Controller<StandardSession.FrameBytes>
{
@Override
public int write(ByteBuffer buffer, Callback<StandardSession.FrameBytes> callback, StandardSession.FrameBytes context)
{
callback.completed(context);
return buffer.remaining();
}
@Override
public void close(boolean onlyOutput)
{
}
}
}

View File

@ -0,0 +1,457 @@
/*
* 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.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.spdy.api.DataInfo;
import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.HeadersInfo;
import org.eclipse.jetty.spdy.api.RstInfo;
import org.eclipse.jetty.spdy.api.SPDY;
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.api.StringDataInfo;
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.spdy.frames.DataFrame;
import org.eclipse.jetty.spdy.frames.SynReplyFrame;
import org.eclipse.jetty.spdy.frames.SynStreamFrame;
import org.eclipse.jetty.spdy.generator.Generator;
import org.eclipse.jetty.util.Callback;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@RunWith(MockitoJUnitRunner.class)
public class StandardSessionTest
{
@Mock
private ISession sessionMock;
private ByteBufferPool bufferPool;
private Executor threadPool;
private StandardSession session;
private Generator generator;
private ScheduledExecutorService scheduler;
private Headers headers;
@Before
public void setUp() throws Exception
{
bufferPool = new StandardByteBufferPool();
threadPool = Executors.newCachedThreadPool();
scheduler = Executors.newSingleThreadScheduledExecutor();
generator = new Generator(new StandardByteBufferPool(),new StandardCompressionFactory.StandardCompressor());
session = new StandardSession(SPDY.V2,bufferPool,threadPool,scheduler,new TestController(),null,1,null,generator);
headers = new Headers();
}
@Test
public void testStreamIsRemovedFromSessionWhenReset() throws InterruptedException, ExecutionException, TimeoutException
{
IStream stream = createStream();
assertThatStreamIsInSession(stream);
assertThat("stream is not reset",stream.isReset(),is(false));
session.rst(new RstInfo(stream.getId(),StreamStatus.STREAM_ALREADY_CLOSED));
assertThatStreamIsNotInSession(stream);
assertThatStreamIsReset(stream);
}
@Test
public void testStreamIsAddedAndRemovedFromSession() throws InterruptedException, ExecutionException, TimeoutException
{
IStream stream = createStream();
assertThatStreamIsInSession(stream);
stream.updateCloseState(true,true);
session.onControlFrame(new SynReplyFrame(SPDY.V2,SynInfo.FLAG_CLOSE,stream.getId(),null));
assertThatStreamIsClosed(stream);
assertThatStreamIsNotInSession(stream);
}
@Test
public void testStreamIsRemovedWhenHeadersWithCloseFlagAreSent() throws InterruptedException, ExecutionException, TimeoutException
{
IStream stream = createStream();
assertThatStreamIsInSession(stream);
stream.updateCloseState(true,false);
stream.headers(new HeadersInfo(headers,true));
assertThatStreamIsClosed(stream);
assertThatStreamIsNotInSession(stream);
}
@Test
public void testStreamIsUnidirectional() throws InterruptedException, ExecutionException, TimeoutException
{
IStream stream = createStream();
assertThat("stream is not unidirectional",stream.isUnidirectional(),not(true));
Stream pushStream = createPushStream(stream);
assertThat("pushStream is unidirectional",pushStream.isUnidirectional(),is(true));
}
@Test
public void testPushStreamCreation() throws InterruptedException, ExecutionException, TimeoutException
{
Stream stream = createStream();
IStream pushStream = createPushStream(stream);
assertThat("Push stream must be associated to the first stream created",pushStream.getAssociatedStream().getId(),is(stream.getId()));
assertThat("streamIds need to be monotonic",pushStream.getId(),greaterThan(stream.getId()));
}
@Test
public void testPushStreamIsNotClosedWhenAssociatedStreamIsClosed() throws InterruptedException, ExecutionException, TimeoutException
{
IStream stream = createStream();
Stream pushStream = createPushStream(stream);
assertThatStreamIsNotHalfClosed(stream);
assertThatStreamIsNotClosed(stream);
assertThatPushStreamIsHalfClosed(pushStream);
assertThatPushStreamIsNotClosed(pushStream);
stream.updateCloseState(true,true);
assertThatStreamIsHalfClosed(stream);
assertThatStreamIsNotClosed(stream);
assertThatPushStreamIsHalfClosed(pushStream);
assertThatPushStreamIsNotClosed(pushStream);
session.onControlFrame(new SynReplyFrame(SPDY.V2,SynInfo.FLAG_CLOSE,stream.getId(),null));
assertThatStreamIsClosed(stream);
assertThatPushStreamIsNotClosed(pushStream);
}
@Test
public void testCreatePushStreamOnClosedStream() throws InterruptedException, ExecutionException, TimeoutException
{
IStream stream = createStream();
stream.updateCloseState(true,true);
assertThatStreamIsHalfClosed(stream);
stream.updateCloseState(true,false);
assertThatStreamIsClosed(stream);
createPushStreamAndMakeSureItFails(stream);
}
private void createPushStreamAndMakeSureItFails(IStream stream) throws InterruptedException
{
final CountDownLatch failedLatch = new CountDownLatch(1);
SynInfo synInfo = new SynInfo(headers,false,stream.getPriority());
stream.syn(synInfo,5,TimeUnit.SECONDS,new Callback<Stream>()
{
@Override
public void completed(Stream context)
{
}
@Override
public void failed(Throwable x)
{
failedLatch.countDown();
}
});
assertThat("pushStream creation failed",failedLatch.await(5,TimeUnit.SECONDS),is(true));
}
@Test
public void testPushStreamIsAddedAndRemovedFromParentAndSessionWhenClosed() throws InterruptedException, ExecutionException, TimeoutException
{
IStream stream = createStream();
IStream pushStream = createPushStream(stream);
assertThatPushStreamIsHalfClosed(pushStream);
assertThatPushStreamIsInSession(pushStream);
assertThatStreamIsAssociatedWithPushStream(stream,pushStream);
session.data(pushStream,new StringDataInfo("close",true),5,TimeUnit.SECONDS,null,null);
assertThatPushStreamIsClosed(pushStream);
assertThatPushStreamIsNotInSession(pushStream);
assertThatStreamIsNotAssociatedWithPushStream(stream,pushStream);
}
@Test
public void testPushStreamIsRemovedWhenReset() throws InterruptedException, ExecutionException, TimeoutException
{
IStream stream = createStream();
IStream pushStream = (IStream)stream.syn(new SynInfo(false)).get();
assertThatPushStreamIsInSession(pushStream);
session.rst(new RstInfo(pushStream.getId(),StreamStatus.INVALID_STREAM));
assertThatPushStreamIsNotInSession(pushStream);
assertThatStreamIsNotAssociatedWithPushStream(stream,pushStream);
assertThatStreamIsReset(pushStream);
}
@Test
public void testPushStreamWithSynInfoClosedTrue() throws InterruptedException, ExecutionException, TimeoutException
{
IStream stream = createStream();
SynInfo synInfo = new SynInfo(headers,true,stream.getPriority());
IStream pushStream = (IStream)stream.syn(synInfo).get(5,TimeUnit.SECONDS);
assertThatPushStreamIsHalfClosed(pushStream);
assertThatPushStreamIsClosed(pushStream);
assertThatStreamIsNotAssociatedWithPushStream(stream,pushStream);
assertThatStreamIsNotInSession(pushStream);
}
@Test
public void testPushStreamSendHeadersWithCloseFlagIsRemovedFromSessionAndDisassociateFromParent() throws InterruptedException, ExecutionException,
TimeoutException
{
IStream stream = createStream();
SynInfo synInfo = new SynInfo(headers,false,stream.getPriority());
IStream pushStream = (IStream)stream.syn(synInfo).get(5,TimeUnit.SECONDS);
assertThatStreamIsAssociatedWithPushStream(stream,pushStream);
assertThatPushStreamIsInSession(pushStream);
pushStream.headers(new HeadersInfo(headers,true));
assertThatPushStreamIsNotInSession(pushStream);
assertThatPushStreamIsHalfClosed(pushStream);
assertThatPushStreamIsClosed(pushStream);
assertThatStreamIsNotAssociatedWithPushStream(stream,pushStream);
}
@Test
public void testCreatedAndClosedListenersAreCalledForNewStream() throws InterruptedException, ExecutionException, TimeoutException
{
final CountDownLatch createdListenerCalledLatch = new CountDownLatch(1);
final CountDownLatch closedListenerCalledLatch = new CountDownLatch(1);
session.addListener(new TestStreamListener(createdListenerCalledLatch,closedListenerCalledLatch));
IStream stream = createStream();
session.onDataFrame(new DataFrame(stream.getId(),SynInfo.FLAG_CLOSE,128),ByteBuffer.allocate(128));
stream.data(new StringDataInfo("close",true));
assertThat("onStreamCreated listener has been called",createdListenerCalledLatch.await(5,TimeUnit.SECONDS),is(true));
assertThatOnStreamClosedListenerHasBeenCalled(closedListenerCalledLatch);
}
@Test
public void testListenerIsCalledForResetStream() throws InterruptedException, ExecutionException, TimeoutException
{
final CountDownLatch closedListenerCalledLatch = new CountDownLatch(1);
session.addListener(new TestStreamListener(null,closedListenerCalledLatch));
IStream stream = createStream();
session.rst(new RstInfo(stream.getId(),StreamStatus.CANCEL_STREAM));
assertThatOnStreamClosedListenerHasBeenCalled(closedListenerCalledLatch);
}
@Test
public void testCreatedAndClosedListenersAreCalledForNewPushStream() throws InterruptedException, ExecutionException, TimeoutException
{
final CountDownLatch createdListenerCalledLatch = new CountDownLatch(2);
final CountDownLatch closedListenerCalledLatch = new CountDownLatch(1);
session.addListener(new TestStreamListener(createdListenerCalledLatch,closedListenerCalledLatch));
IStream stream = createStream();
IStream pushStream = createPushStream(stream);
session.data(pushStream,new StringDataInfo("close",true),5,TimeUnit.SECONDS,null,null);
assertThat("onStreamCreated listener has been called twice. Once for the stream and once for the pushStream",
createdListenerCalledLatch.await(5,TimeUnit.SECONDS),is(true));
assertThatOnStreamClosedListenerHasBeenCalled(closedListenerCalledLatch);
}
@Test
public void testListenerIsCalledForResetPushStream() throws InterruptedException, ExecutionException, TimeoutException
{
final CountDownLatch closedListenerCalledLatch = new CountDownLatch(1);
session.addListener(new TestStreamListener(null,closedListenerCalledLatch));
IStream stream = createStream();
IStream pushStream = createPushStream(stream);
session.rst(new RstInfo(pushStream.getId(),StreamStatus.CANCEL_STREAM));
assertThatOnStreamClosedListenerHasBeenCalled(closedListenerCalledLatch);
}
private class TestStreamListener extends Session.StreamListener.Adapter
{
private CountDownLatch createdListenerCalledLatch;
private CountDownLatch closedListenerCalledLatch;
public TestStreamListener(CountDownLatch createdListenerCalledLatch, CountDownLatch closedListenerCalledLatch)
{
this.createdListenerCalledLatch = createdListenerCalledLatch;
this.closedListenerCalledLatch = closedListenerCalledLatch;
}
@Override
public void onStreamCreated(Stream stream)
{
if (createdListenerCalledLatch != null)
createdListenerCalledLatch.countDown();
super.onStreamCreated(stream);
}
@Override
public void onStreamClosed(Stream stream)
{
if (closedListenerCalledLatch != null)
closedListenerCalledLatch.countDown();
super.onStreamClosed(stream);
}
}
@SuppressWarnings("unchecked")
@Test(expected = IllegalStateException.class)
public void testSendDataOnHalfClosedStream() throws InterruptedException, ExecutionException, TimeoutException
{
SynStreamFrame synStreamFrame = new SynStreamFrame(SPDY.V2,SynInfo.FLAG_CLOSE,1,0,(byte)0,null);
IStream stream = new StandardStream(synStreamFrame,sessionMock,8184,null);
stream.updateCloseState(synStreamFrame.isClose(),true);
assertThat("stream is half closed",stream.isHalfClosed(),is(true));
stream.data(new StringDataInfo("data on half closed stream",true));
verify(sessionMock,never()).data(any(IStream.class),any(DataInfo.class),anyInt(),any(TimeUnit.class),any(Callback.class),any(void.class));
}
@Test
@Ignore("In V3 we need to rst the stream if we receive data on a remotely half closed stream.")
public void receiveDataOnRemotelyHalfClosedStreamResetsStreamInV3() throws InterruptedException, ExecutionException
{
IStream stream = (IStream)session.syn(new SynInfo(false),new StreamFrameListener.Adapter()).get();
stream.updateCloseState(true,false);
assertThat("stream is half closed from remote side",stream.isHalfClosed(),is(true));
stream.process(new DataFrame(stream.getId(),(byte)0,256),ByteBuffer.allocate(256));
}
@Test
public void testReceiveDataOnRemotelyClosedStreamIsIgnored() throws InterruptedException, ExecutionException, TimeoutException
{
final CountDownLatch onDataCalledLatch = new CountDownLatch(1);
Stream stream = session.syn(new SynInfo(false),new StreamFrameListener.Adapter()
{
@Override
public void onData(Stream stream, DataInfo dataInfo)
{
onDataCalledLatch.countDown();
super.onData(stream,dataInfo);
}
}).get(5,TimeUnit.SECONDS);
session.onControlFrame(new SynReplyFrame(SPDY.V2,SynInfo.FLAG_CLOSE,stream.getId(),headers));
session.onDataFrame(new DataFrame(stream.getId(),(byte)0,0),ByteBuffer.allocate(128));
assertThat("onData is never called",onDataCalledLatch.await(1,TimeUnit.SECONDS),not(true));
}
private IStream createStream() throws InterruptedException, ExecutionException, TimeoutException
{
SynInfo synInfo = new SynInfo(headers,false,(byte)0);
return (IStream)session.syn(synInfo,new StreamFrameListener.Adapter()).get(5,TimeUnit.SECONDS);
}
private IStream createPushStream(Stream stream) throws InterruptedException, ExecutionException, TimeoutException
{
SynInfo synInfo = new SynInfo(headers,false,stream.getPriority());
return (IStream)stream.syn(synInfo).get(5,TimeUnit.SECONDS);
}
private static class TestController implements Controller<StandardSession.FrameBytes>
{
@Override
public int write(ByteBuffer buffer, Callback<StandardSession.FrameBytes> callback, StandardSession.FrameBytes context)
{
callback.completed(context);
return buffer.remaining();
}
@Override
public void close(boolean onlyOutput)
{
}
}
private void assertThatStreamIsClosed(IStream stream)
{
assertThat("stream is closed",stream.isClosed(),is(true));
}
private void assertThatStreamIsReset(IStream stream)
{
assertThat("stream is reset",stream.isReset(),is(true));
}
private void assertThatStreamIsNotInSession(IStream stream)
{
assertThat("stream is not in session",session.getStreams().contains(stream),not(true));
}
private void assertThatStreamIsInSession(IStream stream)
{
assertThat("stream is in session",session.getStreams().contains(stream),is(true));
}
private void assertThatStreamIsNotClosed(IStream stream)
{
assertThat("stream is not closed",stream.isClosed(),not(true));
}
private void assertThatStreamIsNotHalfClosed(IStream stream)
{
assertThat("stream is not halfClosed",stream.isHalfClosed(),not(true));
}
private void assertThatPushStreamIsNotClosed(Stream pushStream)
{
assertThat("pushStream is not closed",pushStream.isClosed(),not(true));
}
private void assertThatStreamIsHalfClosed(IStream stream)
{
assertThat("stream is halfClosed",stream.isHalfClosed(),is(true));
}
private void assertThatStreamIsNotAssociatedWithPushStream(IStream stream, IStream pushStream)
{
assertThat("pushStream is removed from parent",stream.getPushedStreams().contains(pushStream),not(true));
}
private void assertThatPushStreamIsNotInSession(Stream pushStream)
{
assertThat("pushStream is not in session",session.getStreams().contains(pushStream.getId()),not(true));
}
private void assertThatPushStreamIsInSession(Stream pushStream)
{
assertThat("pushStream is in session",session.getStreams().contains(pushStream),is(true));
}
private void assertThatStreamIsAssociatedWithPushStream(IStream stream, Stream pushStream)
{
assertThat("stream is associated with pushStream",stream.getPushedStreams().contains(pushStream),is(true));
}
private void assertThatPushStreamIsClosed(Stream pushStream)
{
assertThat("pushStream is closed",pushStream.isClosed(),is(true));
}
private void assertThatPushStreamIsHalfClosed(Stream pushStream)
{
assertThat("pushStream is half closed ",pushStream.isHalfClosed(),is(true));
}
private void assertThatOnStreamClosedListenerHasBeenCalled(final CountDownLatch closedListenerCalledLatch) throws InterruptedException
{
assertThat("onStreamClosed listener has been called",closedListenerCalledLatch.await(5,TimeUnit.SECONDS),is(true));
}
}

View File

@ -0,0 +1,112 @@
// ========================================================================
// Copyright (c) 2009-2009 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
package org.eclipse.jetty.spdy;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
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.SynStreamFrame;
import org.eclipse.jetty.util.Callback;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/* ------------------------------------------------------------ */
/**
*/
@RunWith(MockitoJUnitRunner.class)
public class StandardStreamTest
{
@Mock private ISession session;
@Mock private SynStreamFrame synStreamFrame;
/**
* Test method for {@link org.eclipse.jetty.spdy.StandardStream#syn(org.eclipse.jetty.spdy.api.SynInfo)}.
*/
@SuppressWarnings("unchecked")
@Test
public void testSyn()
{
Stream stream = new StandardStream(synStreamFrame,session,0,null);
Set<Stream> streams = new HashSet<>();
streams.add(stream);
when(synStreamFrame.isClose()).thenReturn(false);
SynInfo synInfo = new SynInfo(false);
when(session.getStreams()).thenReturn(streams);
stream.syn(synInfo);
verify(session).syn(argThat(new PushSynInfoMatcher(stream.getId(),synInfo)),any(StreamFrameListener.class),anyLong(),any(TimeUnit.class),any(Callback.class));
}
private class PushSynInfoMatcher extends ArgumentMatcher<PushSynInfo>{
int associatedStreamId;
SynInfo synInfo;
public PushSynInfoMatcher(int associatedStreamId, SynInfo synInfo)
{
this.associatedStreamId = associatedStreamId;
this.synInfo = synInfo;
}
@Override
public boolean matches(Object argument)
{
PushSynInfo pushSynInfo = (PushSynInfo)argument;
if(pushSynInfo.getAssociatedStreamId() != associatedStreamId){
System.out.println("streamIds do not match!");
return false;
}
if(pushSynInfo.isClose() != synInfo.isClose()){
System.out.println("isClose doesn't match");
return false;
}
return true;
}
}
@Test
public void testSynOnClosedStream(){
IStream stream = new StandardStream(synStreamFrame,session,0,null);
stream.updateCloseState(true,true);
stream.updateCloseState(true,false);
assertThat("stream expected to be closed",stream.isClosed(),is(true));
final CountDownLatch failedLatch = new CountDownLatch(1);
stream.syn(new SynInfo(false),1,TimeUnit.SECONDS,new Callback.Adapter<Stream>()
{
@Override
public void failed(Throwable x)
{
failedLatch.countDown();
}
});
assertThat("PushStream creation failed", failedLatch.getCount(), equalTo(0L));
}
}

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.api;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.spdy.StandardSession;
import org.eclipse.jetty.util.Callback;
import org.junit.Ignore;
import org.junit.Test;
@Ignore
public class ClientUsageTest
{
@Test
public void testClientRequestResponseNoBody() throws Exception
{
Session session = new StandardSession(SPDY.V2, null, null, null, null, null, 1, null, null);
session.syn(new SynInfo(true), new StreamFrameListener.Adapter()
{
@Override
public void onReply(Stream stream, ReplyInfo replyInfo)
{
// Do something with the response
replyInfo.getHeaders().get("host");
// Then issue another similar request
stream.getSession().syn(new SynInfo(true), this);
}
});
}
@Test
public void testClientRequestWithBodyResponseNoBody() throws Exception
{
Session session = new StandardSession(SPDY.V2, null, null, null, null, null, 1, null, null);
Stream stream = session.syn(new SynInfo(false), new StreamFrameListener.Adapter()
{
@Override
public void onReply(Stream stream, ReplyInfo replyInfo)
{
// Do something with the response
replyInfo.getHeaders().get("host");
// Then issue another similar request
stream.getSession().syn(new SynInfo(true), this);
}
}).get(5, TimeUnit.SECONDS);
// Send-and-forget the data
stream.data(new StringDataInfo("data", true));
}
@Test
public void testAsyncClientRequestWithBodyResponseNoBody() throws Exception
{
Session session = new StandardSession(SPDY.V2, null, null, null, null, null, 1, null, null);
final String context = "context";
session.syn(new SynInfo(false), new StreamFrameListener.Adapter()
{
@Override
public void onReply(Stream stream, ReplyInfo replyInfo)
{
// Do something with the response
replyInfo.getHeaders().get("host");
// Then issue another similar request
stream.getSession().syn(new SynInfo(true), this);
}
}, 0, TimeUnit.MILLISECONDS, new Callback.Adapter<Stream>()
{
@Override
public void completed(Stream stream)
{
// Differently from JDK 7 AIO, there is no need to
// have an explicit parameter for the context since
// that is captured while the handler is created anyway,
// and it is used only by the handler as parameter
// The style below is fire-and-forget, since
// we do not pass the handler nor we call get()
// to wait for the data to be sent
stream.data(new StringDataInfo(context, true));
}
});
}
@Test
public void testAsyncClientRequestWithBodyAndResponseWithBody() throws Exception
{
Session session = new StandardSession(SPDY.V2, null, null, null, null, null, 1, null, null);
session.syn(new SynInfo(false), new StreamFrameListener.Adapter()
{
// The good of passing the listener to syn() is that applications can safely
// accumulate info from the reply headers to be used in the data callback,
// e.g. content-type, charset, etc.
@Override
public void onReply(Stream stream, ReplyInfo replyInfo)
{
// Do something with the response
Headers headers = replyInfo.getHeaders();
int contentLength = headers.get("content-length").valueAsInt();
stream.setAttribute("content-length", contentLength);
if (!replyInfo.isClose())
stream.setAttribute("builder", new StringBuilder());
// May issue another similar request while waiting for data
stream.getSession().syn(new SynInfo(true), this);
}
@Override
public void onData(Stream stream, DataInfo dataInfo)
{
StringBuilder builder = (StringBuilder)stream.getAttribute("builder");
builder.append(dataInfo.asString("UTF-8", true));
if (dataInfo.isClose())
{
int receivedLength = builder.toString().getBytes(Charset.forName("UTF-8")).length;
assert receivedLength == (Integer)stream.getAttribute("content-length");
}
}
}, 0, TimeUnit.MILLISECONDS, new Callback.Adapter<Stream>()
{
@Override
public void completed(Stream stream)
{
stream.data(new BytesDataInfo("wee".getBytes(Charset.forName("UTF-8")), false));
stream.data(new StringDataInfo("foo", false));
stream.data(new ByteBufferDataInfo(Charset.forName("UTF-8").encode("bar"), true));
}
});
}
}

View File

@ -0,0 +1,115 @@
/*
* 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.concurrent.TimeUnit;
import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener;
import org.eclipse.jetty.util.Callback;
import org.junit.Ignore;
import org.junit.Test;
@Ignore
public class ServerUsageTest
{
@Test
public void testServerSynAndReplyWithData() throws Exception
{
new ServerSessionFrameListener.Adapter()
{
@Override
public StreamFrameListener onSyn(Stream stream, SynInfo streamInfo)
{
Headers synHeaders = streamInfo.getHeaders();
// Do something with headers, for example extract them and
// perform an http request via Jetty's LocalConnector
// Get the http response, fill headers and data
Headers replyHeaders = new Headers();
replyHeaders.put(synHeaders.get("host"));
// Sends a reply
stream.reply(new ReplyInfo(replyHeaders, false));
// Sends data
StringDataInfo dataInfo = new StringDataInfo("foo", false);
stream.data(dataInfo);
// Stream is now closed
return null;
}
};
}
@Test
public void testServerInitiatesStreamAndPushesData() throws Exception
{
new ServerSessionFrameListener.Adapter()
{
@Override
public void onConnect(Session session)
{
// SPDY does not allow the server to initiate a stream without an existing stream
// being opened by the client already.
// Correct SPDY sequence will be:
// C --- SYN_STREAM(id=1) --> S
// C <-- SYN_REPLY(id=1) --- S
// C <-- SYN_STREAM(id=2,uni,assId=1) --- S
//
// However, the API may allow to initiate the stream
session.syn(new SynInfo(false), null, 0, TimeUnit.MILLISECONDS, new Callback.Adapter<Stream>()
{
@Override
public void completed(Stream stream)
{
// The point here is that we have no idea if the client accepted our stream
// So we return a stream, we may be able to send the headers frame, but later
// the client sends a rst frame.
// We have to atomically set some flag on the stream to signal it's closed
// and any operation on it will throw
stream.headers(new HeadersInfo(new Headers(), true));
}
});
}
};
}
@Test
public void testServerPush() throws Exception
{
new ServerSessionFrameListener.Adapter()
{
@Override
public StreamFrameListener onSyn(Stream stream, SynInfo streamInfo)
{
// Need to send the reply first
stream.reply(new ReplyInfo(false));
Session session = stream.getSession();
// Since it's unidirectional, no need to pass the listener
session.syn(new SynInfo(new Headers(), false, (byte)0), null, 0, TimeUnit.MILLISECONDS, new Callback.Adapter<Stream>()
{
@Override
public void completed(Stream pushStream)
{
pushStream.data(new StringDataInfo("foo", false));
}
});
return null;
}
};
}
}

View File

@ -0,0 +1,142 @@
/*
* 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.nio.ByteBuffer;
import org.eclipse.jetty.spdy.StandardByteBufferPool;
import org.eclipse.jetty.spdy.StandardCompressionFactory;
import org.eclipse.jetty.spdy.api.DataInfo;
import org.eclipse.jetty.spdy.api.StringDataInfo;
import org.eclipse.jetty.spdy.generator.Generator;
import org.eclipse.jetty.spdy.parser.Parser;
import org.junit.Assert;
import org.junit.Test;
public class DataGenerateParseTest
{
@Test
public void testGenerateParse() throws Exception
{
testGenerateParse("test1");
}
@Test
public void testGenerateParseZeroLength() throws Exception
{
testGenerateParse("");
}
private void testGenerateParse(String content) throws Exception
{
int length = content.length();
DataInfo data = new StringDataInfo(content, true);
int streamId = 13;
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.data(streamId, 2 * length, data);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
parser.parse(buffer);
DataFrame frame2 = listener.getDataFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(streamId, frame2.getStreamId());
Assert.assertEquals(DataInfo.FLAG_CLOSE, frame2.getFlags());
Assert.assertEquals(length, frame2.getLength());
Assert.assertEquals(length, listener.getData().remaining());
}
@Test
public void testGenerateParseOneByteAtATime() throws Exception
{
String content = "test2";
int length = content.length();
DataInfo data = new StringDataInfo(content, true);
int streamId = 13;
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.data(streamId, 2 * length, data);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
while (buffer.hasRemaining())
{
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
if (buffer.remaining() < length)
{
DataFrame frame2 = listener.getDataFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(streamId, frame2.getStreamId());
Assert.assertEquals(buffer.hasRemaining() ? 0 : DataInfo.FLAG_CLOSE, frame2.getFlags());
Assert.assertEquals(1, frame2.getLength());
Assert.assertEquals(1, listener.getData().remaining());
}
}
}
@Test
public void testGenerateParseWithSyntheticFrames() throws Exception
{
String content = "0123456789ABCDEF";
int length = content.length();
DataInfo data = new StringDataInfo(content, true);
int streamId = 13;
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.data(streamId, 2 * length, data);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
// Split the buffer to simulate a split boundary in receiving the bytes
int split = 3;
ByteBuffer buffer1 = ByteBuffer.allocate(buffer.remaining() - split);
buffer.limit(buffer.limit() - split);
buffer1.put(buffer);
buffer1.flip();
ByteBuffer buffer2 = ByteBuffer.allocate(split);
buffer.limit(buffer.limit() + split);
buffer2.put(buffer);
buffer2.flip();
parser.parse(buffer1);
DataFrame frame2 = listener.getDataFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(streamId, frame2.getStreamId());
Assert.assertEquals(0, frame2.getFlags());
Assert.assertEquals(length - split, frame2.getLength());
Assert.assertEquals(length - split, listener.getData().remaining());
parser.parse(buffer2);
DataFrame frame3 = listener.getDataFrame();
Assert.assertNotNull(frame3);
Assert.assertEquals(streamId, frame3.getStreamId());
Assert.assertEquals(DataInfo.FLAG_CLOSE, frame3.getFlags());
Assert.assertEquals(split, frame3.getLength());
Assert.assertEquals(split, listener.getData().remaining());
}
}

View File

@ -0,0 +1,83 @@
/*
* 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.nio.ByteBuffer;
import org.eclipse.jetty.spdy.StandardByteBufferPool;
import org.eclipse.jetty.spdy.StandardCompressionFactory;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.generator.Generator;
import org.eclipse.jetty.spdy.parser.Parser;
import org.junit.Assert;
import org.junit.Test;
public class GoAwayGenerateParseTest
{
@Test
public void testGenerateParse() throws Exception
{
int lastStreamId = 13;
int statusCode = 1;
GoAwayFrame frame1 = new GoAwayFrame(SPDY.V3, lastStreamId, statusCode);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
parser.parse(buffer);
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.GO_AWAY, frame2.getType());
GoAwayFrame goAway = (GoAwayFrame)frame2;
Assert.assertEquals(SPDY.V3, goAway.getVersion());
Assert.assertEquals(lastStreamId, goAway.getLastStreamId());
Assert.assertEquals(0, goAway.getFlags());
Assert.assertEquals(statusCode, goAway.getStatusCode());
}
@Test
public void testGenerateParseOneByteAtATime() throws Exception
{
int lastStreamId = 13;
int statusCode = 1;
GoAwayFrame frame1 = new GoAwayFrame(SPDY.V3, lastStreamId, statusCode);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
while (buffer.hasRemaining())
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.GO_AWAY, frame2.getType());
GoAwayFrame goAway = (GoAwayFrame)frame2;
Assert.assertEquals(SPDY.V3, goAway.getVersion());
Assert.assertEquals(lastStreamId, goAway.getLastStreamId());
Assert.assertEquals(0, goAway.getFlags());
Assert.assertEquals(statusCode, goAway.getStatusCode());
}
}

View File

@ -0,0 +1,89 @@
/*
* 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.nio.ByteBuffer;
import org.eclipse.jetty.spdy.StandardByteBufferPool;
import org.eclipse.jetty.spdy.StandardCompressionFactory;
import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.HeadersInfo;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.generator.Generator;
import org.eclipse.jetty.spdy.parser.Parser;
import org.junit.Assert;
import org.junit.Test;
public class HeadersGenerateParseTest
{
@Test
public void testGenerateParse() throws Exception
{
byte flags = HeadersInfo.FLAG_RESET_COMPRESSION;
int streamId = 13;
Headers headers = new Headers();
headers.put("a", "b");
HeadersFrame frame1 = new HeadersFrame(SPDY.V2, flags, streamId, headers);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
parser.parse(buffer);
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.HEADERS, frame2.getType());
HeadersFrame headersFrame = (HeadersFrame)frame2;
Assert.assertEquals(SPDY.V2, headersFrame.getVersion());
Assert.assertEquals(streamId, headersFrame.getStreamId());
Assert.assertEquals(flags, headersFrame.getFlags());
Assert.assertEquals(headers, headersFrame.getHeaders());
}
@Test
public void testGenerateParseOneByteAtATime() throws Exception
{
byte flags = HeadersInfo.FLAG_RESET_COMPRESSION;
int streamId = 13;
Headers headers = new Headers();
headers.put("a", "b");
HeadersFrame frame1 = new HeadersFrame(SPDY.V2, flags, streamId, headers);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
while (buffer.hasRemaining())
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.HEADERS, frame2.getType());
HeadersFrame headersFrame = (HeadersFrame)frame2;
Assert.assertEquals(SPDY.V2, headersFrame.getVersion());
Assert.assertEquals(streamId, headersFrame.getStreamId());
Assert.assertEquals(flags, headersFrame.getFlags());
Assert.assertEquals(headers, headersFrame.getHeaders());
}
}

View File

@ -0,0 +1,72 @@
/*
* 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.nio.ByteBuffer;
import org.eclipse.jetty.spdy.StandardByteBufferPool;
import org.eclipse.jetty.spdy.StandardCompressionFactory;
import org.eclipse.jetty.spdy.generator.Generator;
import org.eclipse.jetty.spdy.parser.Parser;
import org.junit.Assert;
import org.junit.Test;
public class NoOpGenerateParseTest
{
@Test
public void testGenerateParse() throws Exception
{
NoOpFrame frame1 = new NoOpFrame();
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
parser.parse(buffer);
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.NOOP, frame2.getType());
NoOpFrame noOp = (NoOpFrame)frame2;
Assert.assertEquals(0, noOp.getFlags());
}
@Test
public void testGenerateParseOneByteAtATime() throws Exception
{
NoOpFrame frame1 = new NoOpFrame();
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
while (buffer.hasRemaining())
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.NOOP, frame2.getType());
NoOpFrame noOp = (NoOpFrame)frame2;
Assert.assertEquals(0, noOp.getFlags());
}
}

View File

@ -0,0 +1,79 @@
/*
* 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.nio.ByteBuffer;
import org.eclipse.jetty.spdy.StandardByteBufferPool;
import org.eclipse.jetty.spdy.StandardCompressionFactory;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.generator.Generator;
import org.eclipse.jetty.spdy.parser.Parser;
import org.junit.Assert;
import org.junit.Test;
public class PingGenerateParseTest
{
@Test
public void testGenerateParse() throws Exception
{
int pingId = 13;
PingFrame frame1 = new PingFrame(SPDY.V2, pingId);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
parser.parse(buffer);
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.PING, frame2.getType());
PingFrame ping = (PingFrame)frame2;
Assert.assertEquals(SPDY.V2, ping.getVersion());
Assert.assertEquals(pingId, ping.getPingId());
Assert.assertEquals(0, ping.getFlags());
}
@Test
public void testGenerateParseOneByteAtATime() throws Exception
{
int pingId = 13;
PingFrame frame1 = new PingFrame(SPDY.V2, pingId);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
while (buffer.hasRemaining())
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.PING, frame2.getType());
PingFrame ping = (PingFrame)frame2;
Assert.assertEquals(SPDY.V2, ping.getVersion());
Assert.assertEquals(pingId, ping.getPingId());
Assert.assertEquals(0, ping.getFlags());
}
}

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.frames;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.StandardByteBufferPool;
import org.eclipse.jetty.spdy.StandardCompressionFactory;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.StreamStatus;
import org.eclipse.jetty.spdy.generator.Generator;
import org.eclipse.jetty.spdy.parser.Parser;
import org.junit.Assert;
import org.junit.Test;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
public class RstStreamGenerateParseTest
{
@Test
public void testGenerateParse() throws Exception
{
int streamId = 13;
int streamStatus = StreamStatus.UNSUPPORTED_VERSION.getCode(SPDY.V2);
RstStreamFrame frame1 = new RstStreamFrame(SPDY.V2, streamId, streamStatus);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
assertThat("buffer is not null", buffer, not(nullValue()));
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
parser.parse(buffer);
ControlFrame frame2 = listener.getControlFrame();
assertThat("frame2 is not null", frame2, not(nullValue()));
assertThat("frame2 is type RST_STREAM",ControlFrameType.RST_STREAM, equalTo(frame2.getType()));
RstStreamFrame rstStream = (RstStreamFrame)frame2;
assertThat("rstStream version is SPDY.V2",SPDY.V2, equalTo(rstStream.getVersion()));
assertThat("rstStream id is equal to streamId",streamId, equalTo(rstStream.getStreamId()));
assertThat("rstStream flags are 0",(byte)0, equalTo(rstStream.getFlags()));
assertThat("stream status is equal to rstStream statuscode",streamStatus, is(rstStream.getStatusCode()));
}
@Test
public void testGenerateParseOneByteAtATime() throws Exception
{
int streamId = 13;
int streamStatus = StreamStatus.UNSUPPORTED_VERSION.getCode(SPDY.V2);
RstStreamFrame frame1 = new RstStreamFrame(SPDY.V2, streamId, streamStatus);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
while (buffer.hasRemaining())
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.RST_STREAM, frame2.getType());
RstStreamFrame rstStream = (RstStreamFrame)frame2;
Assert.assertEquals(SPDY.V2, rstStream.getVersion());
Assert.assertEquals(streamId, rstStream.getStreamId());
Assert.assertEquals(0, rstStream.getFlags());
Assert.assertEquals(streamStatus, rstStream.getStatusCode());
}
}

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.frames;
import java.nio.ByteBuffer;
import org.eclipse.jetty.spdy.StandardByteBufferPool;
import org.eclipse.jetty.spdy.StandardCompressionFactory;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.Settings;
import org.eclipse.jetty.spdy.api.SettingsInfo;
import org.eclipse.jetty.spdy.generator.Generator;
import org.eclipse.jetty.spdy.parser.Parser;
import org.junit.Assert;
import org.junit.Test;
public class SettingsGenerateParseTest
{
@Test
public void testGenerateParse() throws Exception
{
byte flags = SettingsInfo.CLEAR_PERSISTED;
Settings settings = new Settings();
settings.put(new Settings.Setting(Settings.ID.MAX_CONCURRENT_STREAMS, Settings.Flag.PERSIST, 100));
settings.put(new Settings.Setting(Settings.ID.ROUND_TRIP_TIME, Settings.Flag.PERSISTED, 500));
SettingsFrame frame1 = new SettingsFrame(SPDY.V2, flags, settings);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
parser.parse(buffer);
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.SETTINGS, frame2.getType());
SettingsFrame settingsFrame = (SettingsFrame)frame2;
Assert.assertEquals(SPDY.V2, settingsFrame.getVersion());
Assert.assertEquals(flags, settingsFrame.getFlags());
Assert.assertEquals(settings, settingsFrame.getSettings());
}
@Test
public void testGenerateParseOneByteAtATime() throws Exception
{
byte flags = SettingsInfo.CLEAR_PERSISTED;
Settings settings = new Settings();
settings.put(new Settings.Setting(Settings.ID.DOWNLOAD_RETRANSMISSION_RATE, 100));
settings.put(new Settings.Setting(Settings.ID.ROUND_TRIP_TIME, 500));
SettingsFrame frame1 = new SettingsFrame(SPDY.V2, flags, settings);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
while (buffer.hasRemaining())
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.SETTINGS, frame2.getType());
SettingsFrame settingsFrame = (SettingsFrame)frame2;
Assert.assertEquals(SPDY.V2, settingsFrame.getVersion());
Assert.assertEquals(flags, settingsFrame.getFlags());
Assert.assertEquals(settings, settingsFrame.getSettings());
}
}

View File

@ -0,0 +1,89 @@
/*
* 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.nio.ByteBuffer;
import org.eclipse.jetty.spdy.StandardByteBufferPool;
import org.eclipse.jetty.spdy.StandardCompressionFactory;
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.generator.Generator;
import org.eclipse.jetty.spdy.parser.Parser;
import org.junit.Assert;
import org.junit.Test;
public class SynReplyGenerateParseTest
{
@Test
public void testGenerateParse() throws Exception
{
byte flags = ReplyInfo.FLAG_CLOSE;
int streamId = 13;
Headers headers = new Headers();
headers.put("a", "b");
SynReplyFrame frame1 = new SynReplyFrame(SPDY.V2, flags, streamId, headers);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
parser.parse(buffer);
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.SYN_REPLY, frame2.getType());
SynReplyFrame synReply = (SynReplyFrame)frame2;
Assert.assertEquals(SPDY.V2, synReply.getVersion());
Assert.assertEquals(flags, synReply.getFlags());
Assert.assertEquals(streamId, synReply.getStreamId());
Assert.assertEquals(headers, synReply.getHeaders());
}
@Test
public void testGenerateParseOneByteAtATime() throws Exception
{
byte flags = ReplyInfo.FLAG_CLOSE;
int streamId = 13;
Headers headers = new Headers();
headers.put("a", "b");
SynReplyFrame frame1 = new SynReplyFrame(SPDY.V2, flags, streamId, headers);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
while (buffer.hasRemaining())
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.SYN_REPLY, frame2.getType());
SynReplyFrame synReply = (SynReplyFrame)frame2;
Assert.assertEquals(SPDY.V2, synReply.getVersion());
Assert.assertEquals(flags, synReply.getFlags());
Assert.assertEquals(streamId, synReply.getStreamId());
Assert.assertEquals(headers, synReply.getHeaders());
}
}

View File

@ -0,0 +1,99 @@
/*
* 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.nio.ByteBuffer;
import org.eclipse.jetty.spdy.StandardByteBufferPool;
import org.eclipse.jetty.spdy.StandardCompressionFactory;
import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.spdy.generator.Generator;
import org.eclipse.jetty.spdy.parser.Parser;
import org.junit.Assert;
import org.junit.Test;
public class SynStreamGenerateParseTest
{
@Test
public void testGenerateParse() throws Exception
{
byte flags = SynInfo.FLAG_CLOSE;
int streamId = 13;
int associatedStreamId = 11;
byte priority = 3;
Headers headers = new Headers();
headers.put("a", "b");
headers.put("c", "d");
SynStreamFrame frame1 = new SynStreamFrame(SPDY.V2, flags, streamId, associatedStreamId, priority, headers);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
parser.parse(buffer);
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.SYN_STREAM, frame2.getType());
SynStreamFrame synStream = (SynStreamFrame)frame2;
Assert.assertEquals(SPDY.V2, synStream.getVersion());
Assert.assertEquals(streamId, synStream.getStreamId());
Assert.assertEquals(associatedStreamId, synStream.getAssociatedStreamId());
Assert.assertEquals(flags, synStream.getFlags());
Assert.assertEquals(priority, synStream.getPriority());
Assert.assertEquals(headers, synStream.getHeaders());
}
@Test
public void testGenerateParseOneByteAtATime() throws Exception
{
byte flags = SynInfo.FLAG_CLOSE;
int streamId = 13;
int associatedStreamId = 11;
byte priority = 3;
Headers headers = new Headers();
headers.put("a", "b");
headers.put("c", "d");
SynStreamFrame frame1 = new SynStreamFrame(SPDY.V2, flags, streamId, associatedStreamId, priority, headers);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
while (buffer.hasRemaining())
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.SYN_STREAM, frame2.getType());
SynStreamFrame synStream = (SynStreamFrame)frame2;
Assert.assertEquals(SPDY.V2, synStream.getVersion());
Assert.assertEquals(streamId, synStream.getStreamId());
Assert.assertEquals(associatedStreamId, synStream.getAssociatedStreamId());
Assert.assertEquals(flags, synStream.getFlags());
Assert.assertEquals(priority, synStream.getPriority());
Assert.assertEquals(headers, synStream.getHeaders());
}
}

View File

@ -0,0 +1,68 @@
/*
* 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.nio.ByteBuffer;
import org.eclipse.jetty.spdy.SessionException;
import org.eclipse.jetty.spdy.StreamException;
import org.eclipse.jetty.spdy.parser.Parser;
public class TestSPDYParserListener implements Parser.Listener
{
private ControlFrame controlFrame;
private DataFrame dataFrame;
private ByteBuffer data;
@Override
public void onControlFrame(ControlFrame frame)
{
this.controlFrame = frame;
}
@Override
public void onDataFrame(DataFrame frame, ByteBuffer data)
{
this.dataFrame = frame;
this.data = data;
}
@Override
public void onStreamException(StreamException x)
{
}
@Override
public void onSessionException(SessionException x)
{
}
public ControlFrame getControlFrame()
{
return controlFrame;
}
public DataFrame getDataFrame()
{
return dataFrame;
}
public ByteBuffer getData()
{
return data;
}
}

View File

@ -0,0 +1,83 @@
/*
* 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.nio.ByteBuffer;
import org.eclipse.jetty.spdy.StandardByteBufferPool;
import org.eclipse.jetty.spdy.StandardCompressionFactory;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.generator.Generator;
import org.eclipse.jetty.spdy.parser.Parser;
import org.junit.Assert;
import org.junit.Test;
public class WindowUpdateGenerateParseTest
{
@Test
public void testGenerateParse() throws Exception
{
int streamId = 13;
int windowDelta = 17;
WindowUpdateFrame frame1 = new WindowUpdateFrame(SPDY.V2, streamId, windowDelta);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
parser.parse(buffer);
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.WINDOW_UPDATE, frame2.getType());
WindowUpdateFrame windowUpdate = (WindowUpdateFrame)frame2;
Assert.assertEquals(SPDY.V2, windowUpdate.getVersion());
Assert.assertEquals(streamId, windowUpdate.getStreamId());
Assert.assertEquals(0, windowUpdate.getFlags());
Assert.assertEquals(windowDelta, windowUpdate.getWindowDelta());
}
@Test
public void testGenerateParseOneByteAtATime() throws Exception
{
int streamId = 13;
int windowDelta = 17;
WindowUpdateFrame frame1 = new WindowUpdateFrame(SPDY.V2, streamId, windowDelta);
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory().newCompressor());
ByteBuffer buffer = generator.control(frame1);
Assert.assertNotNull(buffer);
TestSPDYParserListener listener = new TestSPDYParserListener();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(listener);
while (buffer.hasRemaining())
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
ControlFrame frame2 = listener.getControlFrame();
Assert.assertNotNull(frame2);
Assert.assertEquals(ControlFrameType.WINDOW_UPDATE, frame2.getType());
WindowUpdateFrame windowUpdate = (WindowUpdateFrame)frame2;
Assert.assertEquals(SPDY.V2, windowUpdate.getVersion());
Assert.assertEquals(streamId, windowUpdate.getStreamId());
Assert.assertEquals(0, windowUpdate.getFlags());
Assert.assertEquals(windowDelta, windowUpdate.getWindowDelta());
}
}

View File

@ -0,0 +1,98 @@
/*
* 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.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.spdy.StandardCompressionFactory;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.ControlFrameType;
import org.eclipse.jetty.spdy.frames.SynStreamFrame;
import org.junit.Assert;
import org.junit.Test;
public class LiveChromiumRequestParserTest
{
@Test
public void testSynStream() throws Exception
{
// Bytes taken with wireshark from a live chromium request
byte[] bytes1 = toBytes("" +
"800200010100011a0000000100000000000038eadfa251b262e0626083a41706" +
"7bb80b75302cd6ae4017cdcdb12eb435d0b3d4d1d2d702b32c18f850732c036f" +
"68889bae850e44da94811f2d0b3308821ca80375a14e714a72065c0d2cd619f8" +
"52f37443837552f3a076b080b234033f28de73404c2b43630b135306b65c6059" +
"929fc2c0ecee1ac2c0560c4c7eb9a940b52525050ccc206f32ea337021f22643" +
"bb6f7e55664e4ea2bea99e81824684a1a135400a3e9979a5150a151666f16626" +
"9a0a8e40afa686a726796796e89b1a9bea992b68787b84f8fae828e46466a72a" +
"b8a72667e76b2a842695e69594ea1b0203d640c1390358e06496e6ea1b9ae901" +
"c3c5d048cfdc1c22988a22149c98965894093195811d1a150c1cb01802000000" +
"ffff");
byte[] bytes2 = toBytes("" +
"800200010100002700000003000000008000428a106660d00ee640e5d14f4b2c" +
"cb0466313d203154c217000000ffff");
final AtomicReference<ControlFrame> frameRef = new AtomicReference<>();
Parser parser = new Parser(new StandardCompressionFactory().newDecompressor());
parser.addListener(new Parser.Listener.Adapter()
{
@Override
public void onControlFrame(ControlFrame frame)
{
frameRef.set(frame);
}
});
parser.parse(ByteBuffer.wrap(bytes1));
ControlFrame frame = frameRef.get();
Assert.assertNotNull(frame);
Assert.assertEquals(ControlFrameType.SYN_STREAM, frame.getType());
SynStreamFrame synStream = (SynStreamFrame)frame;
Assert.assertEquals(2, synStream.getVersion());
Assert.assertEquals(1, synStream.getStreamId());
Assert.assertEquals(0, synStream.getAssociatedStreamId());
Assert.assertEquals(0, synStream.getPriority());
Assert.assertNotNull(synStream.getHeaders());
Assert.assertFalse(synStream.getHeaders().isEmpty());
frameRef.set(null);
parser.parse(ByteBuffer.wrap(bytes2));
frame = frameRef.get();
Assert.assertNotNull(frame);
Assert.assertEquals(ControlFrameType.SYN_STREAM, frame.getType());
synStream = (SynStreamFrame)frame;
Assert.assertEquals(2, synStream.getVersion());
Assert.assertEquals(3, synStream.getStreamId());
Assert.assertEquals(0, synStream.getAssociatedStreamId());
Assert.assertEquals(2, synStream.getPriority());
Assert.assertNotNull(synStream.getHeaders());
Assert.assertFalse(synStream.getHeaders().isEmpty());
}
private byte[] toBytes(String hexs)
{
byte[] bytes = new byte[hexs.length() / 2];
for (int i = 0; i < hexs.length(); i += 2)
{
String hex = hexs.substring(i, i + 2);
bytes[i / 2] = (byte)Integer.parseInt(hex, 16);
}
return bytes;
}
}

View File

@ -0,0 +1,89 @@
/*
* 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.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
public class ParseVersusCacheBenchmarkTest
{
@Ignore
@Test
public void testParseVersusCache() throws Exception
{
// The parser knows the header name and value lengths, so it creates strings
// out of the bytes; however, this involves creating a byte[] copy the bytes,
// and creating a new String.
// The alternative is to use a cache<ByteBuffer, String>. Is that faster ?
// See also: http://jeremymanson.blogspot.com/2008/04/immutability-in-java.html
String name = "Content-Type";
String value = "application/octect-stream";
Charset charset = Charset.forName("ISO-8859-1");
ByteBuffer buffer = ByteBuffer.wrap((name + value).getBytes(charset));
int iterations = 100_000_000;
long begin = System.nanoTime();
for (int i = 0; i < iterations; ++i)
{
byte[] nameBytes = new byte[name.length()];
buffer.get(nameBytes);
String name2 = new String(nameBytes, charset);
Assert.assertEquals(name2, name);
byte[] valueBytes = new byte[value.length()];
buffer.get(valueBytes);
String value2 = new String(valueBytes, charset);
Assert.assertEquals(value2, value);
buffer.flip();
}
long end = System.nanoTime();
System.err.printf("parse time: %d%n", TimeUnit.NANOSECONDS.toMillis(end - begin));
Map<ByteBuffer, String> map = new HashMap<>();
map.put(ByteBuffer.wrap(name.getBytes(charset)), name);
map.put(ByteBuffer.wrap(value.getBytes(charset)), value);
final Map<ByteBuffer, String> cache = Collections.unmodifiableMap(map);
begin = System.nanoTime();
for (int i = 0; i < iterations; ++i)
{
buffer.limit(buffer.position() + name.length());
String name2 = cache.get(buffer);
Assert.assertEquals(name2, name);
buffer.position(buffer.limit());
buffer.limit(buffer.position() + value.length());
String value2 = cache.get(buffer);
Assert.assertEquals(value2, value);
buffer.position(buffer.limit());
buffer.flip();
}
end = System.nanoTime();
System.err.printf("cache time: %d%n", TimeUnit.NANOSECONDS.toMillis(end - begin));
}
}

View File

@ -0,0 +1,64 @@
package org.eclipse.jetty.spdy.parser;
import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.spdy.SessionException;
import org.eclipse.jetty.spdy.StandardByteBufferPool;
import org.eclipse.jetty.spdy.StandardCompressionFactory;
import org.eclipse.jetty.spdy.StreamException;
import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.spdy.frames.ControlFrame;
import org.eclipse.jetty.spdy.frames.DataFrame;
import org.eclipse.jetty.spdy.frames.SynStreamFrame;
import org.eclipse.jetty.spdy.generator.Generator;
import org.junit.Assert;
import org.junit.Test;
public class UnknownControlFrameTest
{
@Test
public void testUnknownControlFrame() throws Exception
{
SynStreamFrame frame = new SynStreamFrame(SPDY.V2, SynInfo.FLAG_CLOSE, 1, 0, (byte)0, new Headers());
Generator generator = new Generator(new StandardByteBufferPool(), new StandardCompressionFactory.StandardCompressor());
ByteBuffer buffer = generator.control(frame);
// Change the frame type to unknown
buffer.putShort(2, (short)0);
final CountDownLatch latch = new CountDownLatch(1);
Parser parser = new Parser(new StandardCompressionFactory.StandardDecompressor());
parser.addListener(new Parser.Listener.Adapter()
{
@Override
public void onControlFrame(ControlFrame frame)
{
latch.countDown();
}
@Override
public void onDataFrame(DataFrame frame, ByteBuffer data)
{
latch.countDown();
}
@Override
public void onStreamException(StreamException x)
{
latch.countDown();
}
@Override
public void onSessionException(SessionException x)
{
latch.countDown();
}
});
parser.parse(buffer);
Assert.assertFalse(latch.await(1, TimeUnit.SECONDS));
}
}

View File

@ -0,0 +1,14 @@
# LOG4J levels: OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL
#
log4j.rootLogger=ALL,CONSOLE
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
#log4j.appender.CONSOLE.threshold=INFO
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
#log4j.appender.CONSOLE.layout.ConversionPattern=%d %t [%5p][%c{1}] %m%n
log4j.appender.CONSOLE.layout.ConversionPattern=%d %t [%5p][%c{1}] %m%n
log4j.appender.CONSOLE.target=System.err
# Level tuning
log4j.logger.org.eclipse.jetty=INFO
#log4j.logger.org.eclipse.jetty.spdy=DEBUG

View File

@ -0,0 +1,64 @@
<?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>9.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spdy-jetty-http-webapp</artifactId>
<packaging>war</packaging>
<name>Jetty :: SPDY :: Jetty HTTP Web Application</name>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptorRefs>
<descriptorRef>config</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
<!--
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>${project.version}</version>
<configuration>
<stopPort>8888</stopPort>
<stopKey>quit</stopKey>
<jvmArgs>
-Dlog4j.configuration=file://${basedir}/src/main/resources/log4j.properties
-Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${project.version}/npn-boot-${project.version}.jar
</jvmArgs>
<jettyXml>${basedir}/src/main/config/etc/jetty-spdy.xml</jettyXml>
<contextPath>/</contextPath>
</configuration>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.spdy</groupId>
<artifactId>spdy-jetty-http</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j-version}</version>
</dependency>
</dependencies>
</plugin>
-->
</plugins>
</build>
</project>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<New id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory">
<Set name="keyStorePath">src/main/resources/keystore.jks</Set>
<Set name="keyStorePassword">storepwd</Set>
<Set name="trustStore">src/main/resources/truststore.jks</Set>
<Set name="trustStorePassword">storepwd</Set>
<Set name="protocol">TLSv1</Set>
</New>
<Call name="addConnector">
<Arg>
<New class="org.eclipse.jetty.spdy.http.HTTPSPDYServerConnector">
<Set name="Port">8080</Set>
</New>
</Arg>
</Call>
<Call name="addConnector">
<Arg>
<New class="org.eclipse.jetty.spdy.http.HTTPSPDYServerConnector">
<Arg>
<Ref id="sslContextFactory" />
</Arg>
<Set name="Port">8443</Set>
</New>
</Arg>
</Call>
</Configure>

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