Merged branch 'jetty-9.3.x' into 'master'.

This commit is contained in:
Simone Bordet 2015-11-25 14:00:19 +01:00
commit 50041395f9
20 changed files with 397 additions and 231 deletions

View File

@ -73,7 +73,7 @@ public class MultiPartContentProvider extends AbstractTypedContentProvider imple
private final ByteBuffer onlyBoundary; private final ByteBuffer onlyBoundary;
private final ByteBuffer lastBoundary; private final ByteBuffer lastBoundary;
private Listener listener; private Listener listener;
private long length; private long length=-1;
public MultiPartContentProvider() public MultiPartContentProvider()
{ {
@ -151,6 +151,7 @@ public class MultiPartContentProvider extends AbstractTypedContentProvider imple
private void addPart(Part part) private void addPart(Part part)
{ {
parts.add(part); parts.add(part);
length=-1;
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Added {}", part); LOG.debug("Added {}", part);
} }
@ -159,7 +160,11 @@ public class MultiPartContentProvider extends AbstractTypedContentProvider imple
public void setListener(Listener listener) public void setListener(Listener listener)
{ {
this.listener = listener; this.listener = listener;
calculateLength();
}
private void calculateLength()
{
// Compute the length, if possible. // Compute the length, if possible.
if (parts.isEmpty()) if (parts.isEmpty())
{ {
@ -187,6 +192,8 @@ public class MultiPartContentProvider extends AbstractTypedContentProvider imple
@Override @Override
public long getLength() public long getLength()
{ {
if (length<=0)
calculateLength();
return length; return length;
} }

View File

@ -22,6 +22,8 @@ import java.io.IOException;
import java.nio.BufferOverflowException; import java.nio.BufferOverflowException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jetty.http.HttpTokens.EndOfContent; import org.eclipse.jetty.http.HttpTokens.EndOfContent;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
@ -67,8 +69,8 @@ public class HttpGenerator
private final int _send; private final int _send;
private final static int SEND_SERVER = 0x01; private final static int SEND_SERVER = 0x01;
private final static int SEND_XPOWEREDBY = 0x02; private final static int SEND_XPOWEREDBY = 0x02;
private final static Set<String> __assumedContentMethods = new HashSet<>(Arrays.asList(new String[]{HttpMethod.POST.asString(),HttpMethod.PUT.asString()}));
/* ------------------------------------------------------------------------------- */ /* ------------------------------------------------------------------------------- */
public static void setJettyVersion(String serverVersion) public static void setJettyVersion(String serverVersion)
{ {
@ -206,7 +208,6 @@ public class HttpGenerator
if (info==null) if (info==null)
return Result.NEED_INFO; return Result.NEED_INFO;
// Do we need a request header
if (header==null) if (header==null)
return Result.NEED_HEADER; return Result.NEED_HEADER;
@ -226,9 +227,9 @@ public class HttpGenerator
generateRequestLine(info,header); generateRequestLine(info,header);
if (info.getVersion()==HttpVersion.HTTP_0_9) if (info.getVersion()==HttpVersion.HTTP_0_9)
_noContent=true; throw new IllegalArgumentException("HTTP/0.9 not supported");
else
generateHeaders(info,header,content,last); generateHeaders(info,header,content,last);
boolean expect100 = info.getFields().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()); boolean expect100 = info.getFields().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString());
@ -341,26 +342,27 @@ public class HttpGenerator
{ {
if (info==null) if (info==null)
return Result.NEED_INFO; return Result.NEED_INFO;
// Handle 0.9 switch(info.getVersion())
if (info.getVersion() == HttpVersion.HTTP_0_9)
{ {
_persistent = false; case HTTP_1_0:
_endOfContent=EndOfContent.EOF_CONTENT; if (_persistent==null)
if (BufferUtil.hasContent(content)) _persistent=Boolean.FALSE;
_contentPrepared+=content.remaining(); break;
_state = last?State.COMPLETING:State.COMMITTED;
return Result.FLUSH; case HTTP_1_1:
if (_persistent==null)
_persistent=Boolean.TRUE;
break;
default:
throw new IllegalArgumentException(info.getVersion()+" not supported");
} }
// Do we need a response header // Do we need a response header
if (header==null) if (header==null)
return Result.NEED_HEADER; return Result.NEED_HEADER;
// If we have not been told our persistence, set the default
if (_persistent==null)
_persistent=(info.getVersion().ordinal() > HttpVersion.HTTP_1_0.ordinal());
// prepare the header // prepare the header
int pos=BufferUtil.flipToFill(header); int pos=BufferUtil.flipToFill(header);
try try
@ -513,16 +515,8 @@ public class HttpGenerator
header.put(StringUtil.getBytes(request.getMethod())); header.put(StringUtil.getBytes(request.getMethod()));
header.put((byte)' '); header.put((byte)' ');
header.put(StringUtil.getBytes(request.getURIString())); header.put(StringUtil.getBytes(request.getURIString()));
switch(request.getVersion()) header.put((byte)' ');
{ header.put(request.getVersion().toBytes());
case HTTP_1_0:
case HTTP_1_1:
header.put((byte)' ');
header.put(request.getVersion().toBytes());
break;
default:
throw new IllegalStateException();
}
header.put(HttpTokens.CRLF); header.put(HttpTokens.CRLF);
} }
@ -589,126 +583,132 @@ public class HttpGenerator
boolean close=false; boolean close=false;
boolean content_type=false; boolean content_type=false;
StringBuilder connection = null; StringBuilder connection = null;
long content_length = _info.getContentLength();
// Generate fields // Generate fields
if (_info.getFields() != null) HttpFields fields = _info.getFields();
if (fields != null)
{ {
for (HttpField field : _info.getFields()) int n=fields.size();
for (int f=0;f<n;f++)
{ {
HttpField field = fields.getField(f);
String v = field.getValue(); String v = field.getValue();
if (v==null || v.length()==0) if (v==null || v.length()==0)
continue; // rfc7230 does not allow no value continue; // rfc7230 does not allow no value
HttpHeader h = field.getHeader(); HttpHeader h = field.getHeader();
if (h==null)
switch (h==null?HttpHeader.UNKNOWN:h) putTo(field,header);
else
{ {
case CONTENT_LENGTH: switch (h)
// handle specially below {
if (_info.getContentLength()>=0) case CONTENT_LENGTH:
_endOfContent=EndOfContent.CONTENT_LENGTH; _endOfContent=EndOfContent.CONTENT_LENGTH;
break; if (content_length<0)
content_length=Long.valueOf(field.getValue());
// handle setting the field specially below
break;
case CONTENT_TYPE: case CONTENT_TYPE:
{ {
if (field.getValue().startsWith(MimeTypes.Type.MULTIPART_BYTERANGES.toString())) // write the field to the header
_endOfContent=EndOfContent.SELF_DEFINING_CONTENT; content_type=true;
// write the field to the header
content_type=true;
putTo(field,header);
break;
}
case TRANSFER_ENCODING:
{
if (_info.getVersion() == HttpVersion.HTTP_1_1)
transfer_encoding = field;
// Do NOT add yet!
break;
}
case CONNECTION:
{
if (request!=null)
putTo(field,header); putTo(field,header);
break;
// Lookup and/or split connection value field
HttpHeaderValue[] values = HttpHeaderValue.CLOSE.is(field.getValue())?CLOSE:new HttpHeaderValue[]{HttpHeaderValue.CACHE.get(field.getValue())};
String[] split = null;
if (values[0]==null)
{
split = StringUtil.csvSplit(field.getValue());
if (split.length>0)
{
values=new HttpHeaderValue[split.length];
for (int i=0;i<split.length;i++)
values[i]=HttpHeaderValue.CACHE.get(split[i]);
}
} }
// Handle connection values case TRANSFER_ENCODING:
for (int i=0;i<values.length;i++)
{ {
HttpHeaderValue value=values[i]; if (_info.getVersion() == HttpVersion.HTTP_1_1)
switch (value==null?HttpHeaderValue.UNKNOWN:value) transfer_encoding = field;
// Do NOT add yet!
break;
}
case CONNECTION:
{
if (request!=null)
putTo(field,header);
// Lookup and/or split connection value field
HttpHeaderValue[] values = HttpHeaderValue.CLOSE.is(field.getValue())?CLOSE:new HttpHeaderValue[]{HttpHeaderValue.CACHE.get(field.getValue())};
String[] split = null;
if (values[0]==null)
{ {
case UPGRADE: split = StringUtil.csvSplit(field.getValue());
if (split.length>0)
{ {
// special case for websocket connection ordering values=new HttpHeaderValue[split.length];
header.put(HttpHeader.CONNECTION.getBytesColonSpace()).put(HttpHeader.UPGRADE.getBytes()); for (int i=0;i<split.length;i++)
header.put(CRLF); values[i]=HttpHeaderValue.CACHE.get(split[i]);
break;
} }
}
case CLOSE: // Handle connection values
for (int i=0;i<values.length;i++)
{
HttpHeaderValue value=values[i];
switch (value==null?HttpHeaderValue.UNKNOWN:value)
{ {
close=true; case UPGRADE:
_persistent=false;
if (response!=null)
{ {
if (_endOfContent == EndOfContent.UNKNOWN_CONTENT) // special case for websocket connection ordering
_endOfContent=EndOfContent.EOF_CONTENT; header.put(HttpHeader.CONNECTION.getBytesColonSpace()).put(HttpHeader.UPGRADE.getBytes());
header.put(CRLF);
break;
} }
break;
}
case KEEP_ALIVE: case CLOSE:
{
if (_info.getVersion() == HttpVersion.HTTP_1_0)
{ {
keep_alive = true; close=true;
_persistent=false;
if (response!=null) if (response!=null)
_persistent=true; {
if (_endOfContent == EndOfContent.UNKNOWN_CONTENT)
_endOfContent=EndOfContent.EOF_CONTENT;
}
break;
} }
break;
}
default: case KEEP_ALIVE:
{ {
if (connection==null) if (_info.getVersion() == HttpVersion.HTTP_1_0)
connection=new StringBuilder(); {
else keep_alive = true;
connection.append(','); if (response!=null)
connection.append(split==null?field.getValue():split[i]); _persistent=true;
}
break;
}
default:
{
if (connection==null)
connection=new StringBuilder();
else
connection.append(',');
connection.append(split==null?field.getValue():split[i]);
}
} }
} }
// Do NOT add yet!
break;
} }
// Do NOT add yet! case SERVER:
break; {
} send=send&~SEND_SERVER;
putTo(field,header);
break;
}
case SERVER: default:
{ putTo(field,header);
send=send&~SEND_SERVER;
putTo(field,header);
break;
} }
default:
putTo(field,header);
} }
} }
} }
@ -716,13 +716,15 @@ public class HttpGenerator
// Calculate how to end _content and connection, _content length and transfer encoding // Calculate how to end _content and connection, _content length and transfer encoding
// settings. // settings.
// From http://tools.ietf.org/html/rfc7230#section-3.3.3
// From RFC 2616 4.4: // From RFC 2616 4.4:
// 1. No body for 1xx, 204, 304 & HEAD response // 1. No body for 1xx, 204, 304 & HEAD response
// 2. Force _content-length? // 3. If Transfer-Encoding==(.*,)?chunked && HTTP/1.1 && !HttpConnection==close then chunk
// 3. If Transfer-Encoding!=identity && HTTP/1.1 && !HttpConnection==close then chunk // 5. Content-Length without Transfer-Encoding
// 4. Content-Length // 6. Request and none over the above, then Content-Length=0 if POST/PUT
// 5. multipart/byteranges // 7. close
// 6. close
int status=response!=null?response.getStatus():-1; int status=response!=null?response.getStatus():-1;
switch (_endOfContent) switch (_endOfContent)
{ {
@ -731,13 +733,12 @@ public class HttpGenerator
// written yet? // written yet?
// Response known not to have a body // Response known not to have a body
if (_contentPrepared == 0 && response!=null && (status < 200 || status == 204 || status == 304)) if (_contentPrepared == 0 && response!=null && _noContent)
_endOfContent=EndOfContent.NO_CONTENT; _endOfContent=EndOfContent.NO_CONTENT;
else if (_info.getContentLength()>0) else if (_info.getContentLength()>0)
{ {
// we have been given a content length // we have been given a content length
_endOfContent=EndOfContent.CONTENT_LENGTH; _endOfContent=EndOfContent.CONTENT_LENGTH;
long content_length = _info.getContentLength();
if ((response!=null || content_length>0 || content_type ) && !_noContent) if ((response!=null || content_length>0 || content_type ) && !_noContent)
{ {
// known length but not actually set. // known length but not actually set.
@ -750,15 +751,13 @@ public class HttpGenerator
{ {
// we have seen all the _content there is, so we can be content-length limited. // we have seen all the _content there is, so we can be content-length limited.
_endOfContent=EndOfContent.CONTENT_LENGTH; _endOfContent=EndOfContent.CONTENT_LENGTH;
long content_length = _contentPrepared+BufferUtil.length(content); long actual_length = _contentPrepared+BufferUtil.length(content);
if (content_length>=0 && content_length!=actual_length)
throw new IllegalArgumentException("Content-Length header("+content_length+") != actual("+actual_length+")");
// Do we need to tell the headers about it // Do we need to tell the headers about it
if ((response!=null || content_length>0 || content_type ) && !_noContent) putContentLength(header,actual_length,content_type,request,response);
{
header.put(HttpHeader.CONTENT_LENGTH.getBytesColonSpace());
BufferUtil.putDecLong(header, content_length);
header.put(HttpTokens.CRLF);
}
} }
else else
{ {
@ -775,32 +774,12 @@ public class HttpGenerator
case CONTENT_LENGTH: case CONTENT_LENGTH:
{ {
long content_length = _info.getContentLength(); putContentLength(header,content_length,content_type,request,response);
if ((response!=null || content_length>0 || content_type ) && !_noContent)
{
header.put(HttpHeader.CONTENT_LENGTH.getBytesColonSpace());
BufferUtil.putDecLong(header, content_length);
header.put(HttpTokens.CRLF);
}
break; break;
} }
case SELF_DEFINING_CONTENT:
{
// TODO - Should we do this? Why was it not required before?
long content_length = _info.getContentLength();
if (content_length>0)
{
header.put(HttpHeader.CONTENT_LENGTH.getBytesColonSpace());
BufferUtil.putDecLong(header, content_length);
header.put(HttpTokens.CRLF);
}
break;
}
case NO_CONTENT: case NO_CONTENT:
if (response!=null && status >= 200 && status != 204 && status != 304) throw new IllegalStateException();
header.put(CONTENT_LENGTH_0);
break;
case EOF_CONTENT: case EOF_CONTENT:
_persistent = request!=null; _persistent = request!=null;
@ -878,6 +857,22 @@ public class HttpGenerator
header.put(HttpTokens.CRLF); header.put(HttpTokens.CRLF);
} }
/* ------------------------------------------------------------------------------- */
private void putContentLength(ByteBuffer header, long contentLength, boolean contentType, MetaData.Request request, MetaData.Response response)
{
if (contentLength>0)
{
header.put(HttpHeader.CONTENT_LENGTH.getBytesColonSpace());
BufferUtil.putDecLong(header, contentLength);
header.put(HttpTokens.CRLF);
}
else if (!_noContent)
{
if (contentType || response!=null || (request!=null && __assumedContentMethods.contains(request.getMethod())))
header.put(CONTENT_LENGTH_0);
}
}
/* ------------------------------------------------------------------------------- */ /* ------------------------------------------------------------------------------- */
public static byte[] getReasonBuffer(int code) public static byte[] getReasonBuffer(int code)
{ {

View File

@ -32,7 +32,7 @@ public interface HttpTokens
static final byte[] CRLF = {CARRIAGE_RETURN,LINE_FEED}; static final byte[] CRLF = {CARRIAGE_RETURN,LINE_FEED};
static final byte SEMI_COLON= (byte)';'; static final byte SEMI_COLON= (byte)';';
public enum EndOfContent { UNKNOWN_CONTENT,NO_CONTENT,EOF_CONTENT,CONTENT_LENGTH,CHUNKED_CONTENT,SELF_DEFINING_CONTENT } public enum EndOfContent { UNKNOWN_CONTENT,NO_CONTENT,EOF_CONTENT,CONTENT_LENGTH,CHUNKED_CONTENT }
} }

View File

@ -43,7 +43,7 @@ public class HttpGeneratorClientTest
} }
@Test @Test
public void testRequestNoContent() throws Exception public void testGETRequestNoContent() throws Exception
{ {
ByteBuffer header=BufferUtil.allocate(2048); ByteBuffer header=BufferUtil.allocate(2048);
HttpGenerator gen = new HttpGenerator(); HttpGenerator gen = new HttpGenerator();
@ -77,7 +77,43 @@ public class HttpGeneratorClientTest
Assert.assertEquals(0, gen.getContentPrepared()); Assert.assertEquals(0, gen.getContentPrepared());
Assert.assertThat(out, Matchers.containsString("GET /index.html HTTP/1.1")); Assert.assertThat(out, Matchers.containsString("GET /index.html HTTP/1.1"));
Assert.assertThat(out, Matchers.not(Matchers.containsString("Content-Length"))); Assert.assertThat(out, Matchers.not(Matchers.containsString("Content-Length")));
}
@Test
public void testPOSTRequestNoContent() throws Exception
{
ByteBuffer header=BufferUtil.allocate(2048);
HttpGenerator gen = new HttpGenerator();
HttpGenerator.Result
result=gen.generateRequest(null,null,null,null, true);
Assert.assertEquals(HttpGenerator.Result.NEED_INFO, result);
Assert.assertEquals(HttpGenerator.State.START, gen.getState());
Info info = new Info("POST","/index.html");
info.getFields().add("Host","something");
info.getFields().add("User-Agent","test");
Assert.assertTrue(!gen.isChunking());
result=gen.generateRequest(info,null,null,null, true);
Assert.assertEquals(HttpGenerator.Result.NEED_HEADER, result);
Assert.assertEquals(HttpGenerator.State.START, gen.getState());
result=gen.generateRequest(info,header,null,null, true);
Assert.assertEquals(HttpGenerator.Result.FLUSH, result);
Assert.assertEquals(HttpGenerator.State.COMPLETING, gen.getState());
Assert.assertTrue(!gen.isChunking());
String out = BufferUtil.toString(header);
BufferUtil.clear(header);
result=gen.generateResponse(null,null,null,null, false);
Assert.assertEquals(HttpGenerator.Result.DONE, result);
Assert.assertEquals(HttpGenerator.State.END, gen.getState());
Assert.assertTrue(!gen.isChunking());
Assert.assertEquals(0, gen.getContentPrepared());
Assert.assertThat(out, Matchers.containsString("POST /index.html HTTP/1.1"));
Assert.assertThat(out, Matchers.containsString("Content-Length: 0"));
} }
@Test @Test

View File

@ -59,14 +59,6 @@ public class HttpGeneratorServerHTTPTest
String response = run.result.build(run.httpVersion, gen, "OK\r\nTest", run.connection.val, null, run.chunks); String response = run.result.build(run.httpVersion, gen, "OK\r\nTest", run.connection.val, null, run.chunks);
if (run.httpVersion == 9)
{
assertFalse(t, gen.isPersistent());
if (run.result._body != null)
assertEquals(t, run.result._body, response);
return;
}
HttpParser parser = new HttpParser(handler); HttpParser parser = new HttpParser(handler);
parser.setHeadResponse(run.result._head); parser.setHeadResponse(run.result._head);
@ -80,8 +72,7 @@ public class HttpGeneratorServerHTTPTest
else else
assertTrue(t, gen.isPersistent() || EnumSet.of(ConnectionType.CLOSE, ConnectionType.TE_CLOSE).contains(run.connection)); assertTrue(t, gen.isPersistent() || EnumSet.of(ConnectionType.CLOSE, ConnectionType.TE_CLOSE).contains(run.connection));
if (run.httpVersion > 9) assertEquals("OK??Test", _reason);
assertEquals("OK??Test", _reason);
if (_content == null) if (_content == null)
assertTrue(t, run.result._body == null); assertTrue(t, run.result._body == null);
@ -346,7 +337,7 @@ public class HttpGeneratorServerHTTPTest
for (Result result : results) for (Result result : results)
{ {
// Loop over HTTP versions // Loop over HTTP versions
for (int v = 9; v <= 11; v++) for (int v = 10; v <= 11; v++)
{ {
// Loop over chunks // Loop over chunks
for (int chunks = 1; chunks <= 6; chunks++) for (int chunks = 1; chunks <= 6; chunks++)

View File

@ -395,7 +395,7 @@ public class HttpGeneratorServerTest
assertEquals(HttpGenerator.Result.NEED_INFO, result); assertEquals(HttpGenerator.Result.NEED_INFO, result);
assertEquals(HttpGenerator.State.START, gen.getState()); assertEquals(HttpGenerator.State.START, gen.getState());
MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), 59); MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, 200, null, new HttpFields(), BufferUtil.length(content0)+BufferUtil.length(content1));
info.getFields().add("Last-Modified", DateGenerator.__01Jan1970); info.getFields().add("Last-Modified", DateGenerator.__01Jan1970);
result = gen.generateResponse(info, null, null, content0, false); result = gen.generateResponse(info, null, null, content0, false);
assertEquals(HttpGenerator.Result.NEED_HEADER, result); assertEquals(HttpGenerator.Result.NEED_HEADER, result);

View File

@ -22,6 +22,8 @@ import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.WritePendingException; import java.nio.channels.WritePendingException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -43,8 +45,10 @@ import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.DataFrame; import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.GoAwayFrame; import org.eclipse.jetty.http2.frames.GoAwayFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame; import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.Jetty; import org.eclipse.jetty.util.Jetty;
import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.Promise;
import org.junit.Assert; import org.junit.Assert;
@ -289,6 +293,131 @@ public class HTTP2Test extends AbstractTest
Assert.assertTrue(closeLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(closeLatch.await(5, TimeUnit.SECONDS));
} }
@Test
public void testMaxConcurrentStreams() throws Exception
{
int maxStreams = 2;
start(new ServerSessionListener.Adapter()
{
@Override
public Map<Integer, Integer> onPreface(Session session)
{
Map<Integer, Integer> settings = new HashMap<>(1);
settings.put(SettingsFrame.MAX_CONCURRENT_STREAMS, maxStreams);
return settings;
}
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields(), 0);
stream.headers(new HeadersFrame(stream.getId(), response, null, true), Callback.NOOP);
return null;
}
});
CountDownLatch settingsLatch = new CountDownLatch(1);
Session session = newClient(new Session.Listener.Adapter()
{
@Override
public void onSettings(Session session, SettingsFrame frame)
{
settingsLatch.countDown();
}
});
Assert.assertTrue(settingsLatch.await(5, TimeUnit.SECONDS));
MetaData.Request request1 = newRequest("GET", new HttpFields());
FuturePromise<Stream> promise1 = new FuturePromise<>();
CountDownLatch exchangeLatch1 = new CountDownLatch(2);
session.newStream(new HeadersFrame(request1, null, false), promise1, new Stream.Listener.Adapter()
{
@Override
public void onHeaders(Stream stream, HeadersFrame frame)
{
if (frame.isEndStream())
exchangeLatch1.countDown();
}
});
Stream stream1 = promise1.get(5, TimeUnit.SECONDS);
MetaData.Request request2 = newRequest("GET", new HttpFields());
FuturePromise<Stream> promise2 = new FuturePromise<>();
CountDownLatch exchangeLatch2 = new CountDownLatch(2);
session.newStream(new HeadersFrame(request2, null, false), promise2, new Stream.Listener.Adapter()
{
@Override
public void onHeaders(Stream stream, HeadersFrame frame)
{
if (frame.isEndStream())
exchangeLatch2.countDown();
}
});
Stream stream2 = promise2.get(5, TimeUnit.SECONDS);
// The third stream must not be created.
MetaData.Request request3 = newRequest("GET", new HttpFields());
CountDownLatch maxStreamsLatch = new CountDownLatch(1);
session.newStream(new HeadersFrame(request3, null, false), new Promise.Adapter<Stream>()
{
@Override
public void failed(Throwable x)
{
if (x instanceof IllegalStateException)
maxStreamsLatch.countDown();
}
}, new Stream.Listener.Adapter());
Assert.assertTrue(maxStreamsLatch.await(5, TimeUnit.SECONDS));
Assert.assertEquals(2, session.getStreams().size());
// End the second stream.
stream2.data(new DataFrame(stream2.getId(), BufferUtil.EMPTY_BUFFER, true), new Callback()
{
@Override
public void succeeded()
{
exchangeLatch2.countDown();
}
});
Assert.assertTrue(exchangeLatch2.await(5, TimeUnit.SECONDS));
Assert.assertEquals(1, session.getStreams().size());
// Create a fourth stream.
MetaData.Request request4 = newRequest("GET", new HttpFields());
CountDownLatch exchangeLatch4 = new CountDownLatch(2);
session.newStream(new HeadersFrame(request4, null, true), new Promise.Adapter<Stream>()
{
@Override
public void succeeded(Stream result)
{
exchangeLatch4.countDown();
}
}, new Stream.Listener.Adapter()
{
@Override
public void onHeaders(Stream stream, HeadersFrame frame)
{
if (frame.isEndStream())
exchangeLatch4.countDown();
}
});
Assert.assertTrue(exchangeLatch4.await(5, TimeUnit.SECONDS));
Assert.assertEquals(1, session.getStreams().size());
// End the first stream.
stream1.data(new DataFrame(stream1.getId(), BufferUtil.EMPTY_BUFFER, true), new Callback()
{
@Override
public void succeeded()
{
exchangeLatch1.countDown();
}
});
Assert.assertTrue(exchangeLatch2.await(5, TimeUnit.SECONDS));
Assert.assertEquals(0, session.getStreams().size());
}
@Test @Test
public void testInvalidAPIUsageOnClient() throws Exception public void testInvalidAPIUsageOnClient() throws Exception
{ {
@ -383,19 +512,6 @@ public class HTTP2Test extends AbstractTest
Assert.assertTrue(completeLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(completeLatch.await(5, TimeUnit.SECONDS));
} }
private static void sleep(long time)
{
try
{
Thread.sleep(time);
}
catch (InterruptedException x)
{
throw new RuntimeException();
}
}
}
/*
@Test @Test
public void testInvalidAPIUsageOnServer() throws Exception public void testInvalidAPIUsageOnServer() throws Exception
{ {
@ -469,4 +585,15 @@ public class HTTP2Test extends AbstractTest
Assert.assertTrue(completeLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(completeLatch.await(5, TimeUnit.SECONDS));
} }
*/ private static void sleep(long time)
{
try
{
Thread.sleep(time);
}
catch (InterruptedException x)
{
throw new RuntimeException();
}
}
}

View File

@ -47,14 +47,14 @@ public abstract class AbstractFlowControlStrategy implements FlowControlStrategy
} }
@Override @Override
public void onStreamCreated(IStream stream, boolean local) public void onStreamCreated(IStream stream)
{ {
stream.updateSendWindow(initialStreamSendWindow); stream.updateSendWindow(initialStreamSendWindow);
stream.updateRecvWindow(initialStreamRecvWindow); stream.updateRecvWindow(initialStreamRecvWindow);
} }
@Override @Override
public void onStreamDestroyed(IStream stream, boolean local) public void onStreamDestroyed(IStream stream)
{ {
} }

View File

@ -68,17 +68,17 @@ public class BufferingFlowControlStrategy extends AbstractFlowControlStrategy
} }
@Override @Override
public void onStreamCreated(IStream stream, boolean local) public void onStreamCreated(IStream stream)
{ {
super.onStreamCreated(stream, local); super.onStreamCreated(stream);
streamLevels.put(stream, new AtomicInteger()); streamLevels.put(stream, new AtomicInteger());
} }
@Override @Override
public void onStreamDestroyed(IStream stream, boolean local) public void onStreamDestroyed(IStream stream)
{ {
streamLevels.remove(stream); streamLevels.remove(stream);
super.onStreamDestroyed(stream, local); super.onStreamDestroyed(stream);
} }
@Override @Override

View File

@ -24,9 +24,9 @@ public interface FlowControlStrategy
{ {
public static int DEFAULT_WINDOW_SIZE = 65535; public static int DEFAULT_WINDOW_SIZE = 65535;
public void onStreamCreated(IStream stream, boolean local); public void onStreamCreated(IStream stream);
public void onStreamDestroyed(IStream stream, boolean local); public void onStreamDestroyed(IStream stream);
public void updateInitialStreamWindow(ISession session, int initialStreamWindow, boolean local); public void updateInitialStreamWindow(ISession session, int initialStreamWindow, boolean local);

View File

@ -366,7 +366,7 @@ public class HTTP2Flusher extends IteratingCallback
if (stream != null) if (stream != null)
{ {
stream.close(); stream.close();
stream.getSession().removeStream(stream, true); stream.getSession().removeStream(stream);
} }
callback.failed(x); callback.failed(x);
} }

View File

@ -618,11 +618,11 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
break; break;
} }
IStream stream = newStream(streamId); IStream stream = newStream(streamId, true);
if (streams.putIfAbsent(streamId, stream) == null) if (streams.putIfAbsent(streamId, stream) == null)
{ {
stream.setIdleTimeout(getStreamIdleTimeout()); stream.setIdleTimeout(getStreamIdleTimeout());
flowControl.onStreamCreated(stream, true); flowControl.onStreamCreated(stream);
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Created local {}", stream); LOG.debug("Created local {}", stream);
return stream; return stream;
@ -650,14 +650,14 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
break; break;
} }
IStream stream = newStream(streamId); IStream stream = newStream(streamId, false);
// SPEC: duplicate stream is treated as connection error. // SPEC: duplicate stream is treated as connection error.
if (streams.putIfAbsent(streamId, stream) == null) if (streams.putIfAbsent(streamId, stream) == null)
{ {
updateLastStreamId(streamId); updateLastStreamId(streamId);
stream.setIdleTimeout(getStreamIdleTimeout()); stream.setIdleTimeout(getStreamIdleTimeout());
flowControl.onStreamCreated(stream, false); flowControl.onStreamCreated(stream);
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Created remote {}", stream); LOG.debug("Created remote {}", stream);
return stream; return stream;
@ -669,28 +669,29 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
} }
} }
protected IStream newStream(int streamId) protected IStream newStream(int streamId, boolean local)
{ {
return new HTTP2Stream(scheduler, this, streamId); return new HTTP2Stream(scheduler, this, streamId, local);
} }
@Override @Override
public void removeStream(IStream stream, boolean local) public void removeStream(IStream stream)
{ {
IStream removed = streams.remove(stream.getId()); IStream removed = streams.remove(stream.getId());
if (removed != null) if (removed != null)
{ {
assert removed == stream; assert removed == stream;
boolean local = stream.isLocal();
if (local) if (local)
localStreamCount.decrementAndGet(); localStreamCount.decrementAndGet();
else else
remoteStreamCount.decrementAndGet(); remoteStreamCount.decrementAndGet();
flowControl.onStreamDestroyed(stream, local); flowControl.onStreamDestroyed(stream);
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Removed {}", stream); LOG.debug("Removed {} {}", local ? "local" : "remote", stream);
} }
} }
@ -1058,7 +1059,7 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
{ {
HeadersFrame headersFrame = (HeadersFrame)frame; HeadersFrame headersFrame = (HeadersFrame)frame;
if (stream.updateClose(headersFrame.isEndStream(), true)) if (stream.updateClose(headersFrame.isEndStream(), true))
removeStream(stream, true); removeStream(stream);
break; break;
} }
case RST_STREAM: case RST_STREAM:
@ -1066,7 +1067,7 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
if (stream != null) if (stream != null)
{ {
stream.close(); stream.close();
removeStream(stream, true); removeStream(stream);
} }
break; break;
} }
@ -1174,7 +1175,7 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
// Only now we can update the close state // Only now we can update the close state
// and eventually remove the stream. // and eventually remove the stream.
if (stream.updateClose(dataFrame.isEndStream(), true)) if (stream.updateClose(dataFrame.isEndStream(), true))
removeStream(stream, true); removeStream(stream);
callback.succeeded(); callback.succeeded();
} }
} }

View File

@ -51,15 +51,17 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback
private final AtomicInteger recvWindow = new AtomicInteger(); private final AtomicInteger recvWindow = new AtomicInteger();
private final ISession session; private final ISession session;
private final int streamId; private final int streamId;
private final boolean local;
private volatile Listener listener; private volatile Listener listener;
private volatile boolean localReset; private volatile boolean localReset;
private volatile boolean remoteReset; private volatile boolean remoteReset;
public HTTP2Stream(Scheduler scheduler, ISession session, int streamId) public HTTP2Stream(Scheduler scheduler, ISession session, int streamId, boolean local)
{ {
super(scheduler); super(scheduler);
this.session = session; this.session = session;
this.streamId = streamId; this.streamId = streamId;
this.local = local;
} }
@Override @Override
@ -68,6 +70,12 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback
return streamId; return streamId;
} }
@Override
public boolean isLocal()
{
return local;
}
@Override @Override
public ISession getSession() public ISession getSession()
{ {
@ -242,7 +250,7 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback
private void onHeaders(HeadersFrame frame, Callback callback) private void onHeaders(HeadersFrame frame, Callback callback)
{ {
if (updateClose(frame.isEndStream(), false)) if (updateClose(frame.isEndStream(), false))
session.removeStream(this, false); session.removeStream(this);
callback.succeeded(); callback.succeeded();
} }
@ -273,7 +281,7 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback
} }
if (updateClose(frame.isEndStream(), false)) if (updateClose(frame.isEndStream(), false))
session.removeStream(this, false); session.removeStream(this);
notifyData(this, frame, callback); notifyData(this, frame, callback);
} }
@ -281,7 +289,7 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback
{ {
remoteReset = true; remoteReset = true;
close(); close();
session.removeStream(this, false); session.removeStream(this);
callback.succeeded(); callback.succeeded();
notifyReset(this, frame); notifyReset(this, frame);
} }

View File

@ -41,9 +41,8 @@ public interface ISession extends Session
* <p>Removes the given {@code stream}.</p> * <p>Removes the given {@code stream}.</p>
* *
* @param stream the stream to remove * @param stream the stream to remove
* @param local whether the stream is local or remote
*/ */
public void removeStream(IStream stream, boolean local); public void removeStream(IStream stream);
/** /**
* <p>Enqueues the given frames to be written to the connection.</p> * <p>Enqueues the given frames to be written to the connection.</p>

View File

@ -39,6 +39,11 @@ public interface IStream extends Stream, Closeable
*/ */
public static final String CHANNEL_ATTRIBUTE = IStream.class.getName() + ".channel"; public static final String CHANNEL_ATTRIBUTE = IStream.class.getName() + ".channel";
/**
* @return whether this stream is local or remote
*/
public boolean isLocal();
@Override @Override
public ISession getSession(); public ISession getSession();

View File

@ -134,11 +134,18 @@ public interface Session
public interface Listener public interface Listener
{ {
/** /**
* <p>Callback method invoked when the preface has been received.</p> * <p>Callback method invoked:</p>
* <ul>
* <li>for clients, just before the preface is sent, to gather the
* SETTINGS configuration options the client wants to send to the server;</li>
* <li>for servers, just after having received the preface, to gather
* the SETTINGS configuration options the server wants to send to the
* client.</li>
* </ul>
* *
* @param session the session * @param session the session
* @return a (possibly empty or null) map containing SETTINGS configuration * @return a (possibly empty or null) map containing SETTINGS configuration
* options that are sent after the preface. * options to send.
*/ */
public Map<Integer, Integer> onPreface(Session session); public Map<Integer, Integer> onPreface(Session session);

View File

@ -635,9 +635,11 @@ public abstract class AbstractProxyServlet extends HttpServlet
else else
{ {
proxyResponse.resetBuffer(); proxyResponse.resetBuffer();
failure.printStackTrace();
int status = failure instanceof TimeoutException ? int status = failure instanceof TimeoutException ?
HttpStatus.GATEWAY_TIMEOUT_504 : HttpStatus.GATEWAY_TIMEOUT_504 :
HttpStatus.BAD_GATEWAY_502; HttpStatus.BAD_GATEWAY_502;
System.err.println("STATUS="+status);
sendProxyResponseError(clientRequest, proxyResponse, status); sendProxyResponseError(clientRequest, proxyResponse, status);
} }
} }

View File

@ -305,7 +305,12 @@ public class ProxyServletFailureTest
.content(new BytesContentProvider(content)) .content(new BytesContentProvider(content))
.send(); .send();
Assert.assertEquals(500, response.getStatus()); // TODO which is correct?
if (proxyServlet instanceof AsyncProxyServlet)
Assert.assertEquals(502, response.getStatus());
else
Assert.assertEquals(500, response.getStatus());
} }
@Test(expected = TimeoutException.class) @Test(expected = TimeoutException.class)

View File

@ -583,7 +583,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
if (handler!=null) if (handler!=null)
content=handler.badMessageError(status,reason,fields); content=handler.badMessageError(status,reason,fields);
sendResponse(new MetaData.Response(HttpVersion.HTTP_1_1,status,reason,fields,0),content ,true); sendResponse(new MetaData.Response(HttpVersion.HTTP_1_1,status,reason,fields,BufferUtil.length(content)),content ,true);
} }
} }
catch (IOException e) catch (IOException e)

View File

@ -689,24 +689,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
{ {
case NEED_HEADER: case NEED_HEADER:
{ {
// Look for optimisation to avoid allocating a _header buffer _header = _bufferPool.acquire(_config.getResponseHeaderSize(), HEADER_BUFFER_DIRECT);
/*
Cannot use this optimisation unless we work out how not to overwrite data in user passed arrays.
if (_lastContent && _content!=null && !_content.isReadOnly() && _content.hasArray() && BufferUtil.space(_content)>_config.getResponseHeaderSize() )
{
// use spare space in content buffer for header buffer
int p=_content.position();
int l=_content.limit();
_content.position(l);
_content.limit(l+_config.getResponseHeaderSize());
_header=_content.slice();
_header.limit(0);
_content.position(p);
_content.limit(l);
}
else
*/
_header = _bufferPool.acquire(_config.getResponseHeaderSize(), HEADER_BUFFER_DIRECT);
continue; continue;
} }