@Override public int onDataRead( ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { FullHttpMessage msg = messageMap.get(streamId); if (msg == null) { throw connectionError( PROTOCOL_ERROR, "Data Frame received for unknown stream id %d", streamId); } ByteBuf content = msg.content(); final int dataReadableBytes = data.readableBytes(); if (content.readableBytes() > maxContentLength - dataReadableBytes) { throw connectionError( INTERNAL_ERROR, "Content length exceeded max of %d for stream id %d", maxContentLength, streamId); } content.writeBytes(data, data.readerIndex(), dataReadableBytes); if (endOfStream) { fireChannelRead(ctx, msg, streamId); } // All bytes have been processed. return dataReadableBytes + padding; }
/** * Checks if the given HTTP message should be considered as a last SPDY frame. * * @param httpMessage check this HTTP message * @return whether the given HTTP message should generate a <em>last</em> SPDY frame. */ private static boolean isLast(HttpMessage httpMessage) { if (httpMessage instanceof FullHttpMessage) { FullHttpMessage fullMessage = (FullHttpMessage) httpMessage; if (fullMessage.trailingHeaders().isEmpty() && !fullMessage.content().isReadable()) { return true; } } return false; }
@Test public void clientRequestStreamDependencyInHttpMessageFlow() throws Exception { setServerLatch(2); final String text = "hello world big time data!"; final ByteBuf content = Unpooled.copiedBuffer(text.getBytes()); final String text2 = "hello world big time data...number 2!!"; final ByteBuf content2 = Unpooled.copiedBuffer(text2.getBytes()); final FullHttpRequest request = new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.PUT, "/some/path/resource", content, true); final FullHttpMessage request2 = new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.PUT, "/some/path/resource2", content2, true); try { HttpHeaders httpHeaders = request.headers(); httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length()); HttpHeaders httpHeaders2 = request2.headers(); httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3); httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), 123); httpHeaders2.set(HttpHeaders.Names.CONTENT_LENGTH, text2.length()); final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("PUT")).path(as("/some/path/resource")); final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(as("PUT")).path(as("/some/path/resource2")); runInChannel( clientChannel, new Http2Runnable() { @Override public void run() { frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient()); frameWriter.writeHeaders(ctxClient(), 5, http2Headers2, 0, false, newPromiseClient()); frameWriter.writePriority(ctxClient(), 5, 3, (short) 123, true, newPromiseClient()); frameWriter.writeData(ctxClient(), 3, content.retain(), 0, true, newPromiseClient()); frameWriter.writeData(ctxClient(), 5, content2.retain(), 0, true, newPromiseClient()); ctxClient().flush(); } }); awaitRequests(); ArgumentCaptor<FullHttpMessage> httpObjectCaptor = ArgumentCaptor.forClass(FullHttpMessage.class); verify(serverListener, times(2)).messageReceived(httpObjectCaptor.capture()); capturedRequests = httpObjectCaptor.getAllValues(); assertEquals(request, capturedRequests.get(0)); assertEquals(request2, capturedRequests.get(1)); } finally { request.release(); request2.release(); } }
@Override public boolean mustSendImmediately(FullHttpMessage msg) { if (msg instanceof FullHttpResponse) { return ((FullHttpResponse) msg).status().codeClass() == HttpStatusClass.INFORMATIONAL; } if (msg instanceof FullHttpRequest) { return msg.headers().contains(HttpHeaderNames.EXPECT); } return false; }
@Override public void onPushPromiseRead( ChannelHandlerContext ctx, int streamId, int promisedStreamId, Http2Headers headers, int padding) throws Http2Exception { // A push promise should not be allowed to add headers to an existing stream FullHttpMessage msg = processHeadersBegin(ctx, promisedStreamId, headers, false, false, false); if (msg == null) { throw connectionError( PROTOCOL_ERROR, "Push Promise Frame received for pre-existing stream id %d", promisedStreamId); } msg.headers() .setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), streamId); processHeadersEnd(ctx, promisedStreamId, msg, false); }
@Test public void serverResponseHeaderInformational() throws Exception { final FullHttpMessage request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, "/info/test", true); HttpHeaders httpHeaders = request.headers(); httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.EXPECT, HttpHeaders.Values.CONTINUE); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0); final Http2Headers http2Headers = new DefaultHttp2Headers() .method(as("PUT")) .path(as("/info/test")) .set( as(HttpHeaders.Names.EXPECT.toString()), as(HttpHeaders.Values.CONTINUE.toString())); final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE); final String text = "a big payload"; final ByteBuf payload = Unpooled.copiedBuffer(text.getBytes()); final FullHttpMessage request2 = request.copy(payload); final FullHttpMessage response2 = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); try { runInChannel( clientChannel, new Http2Runnable() { @Override public void run() { frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient()); ctxClient().flush(); } }); awaitRequests(); ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class); verify(serverListener).messageReceived(requestCaptor.capture()); capturedRequests = requestCaptor.getAllValues(); assertEquals(request, capturedRequests.get(0)); cleanupCapturedRequests(); reset(serverListener); httpHeaders = response.headers(); httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0); final Http2Headers http2HeadersResponse = new DefaultHttp2Headers().status(as("100")); runInChannel( serverConnectedChannel, new Http2Runnable() { @Override public void run() { frameWriter.writeHeaders( ctxServer(), 3, http2HeadersResponse, 0, false, newPromiseServer()); ctxServer().flush(); } }); awaitResponses(); ArgumentCaptor<FullHttpMessage> responseCaptor = ArgumentCaptor.forClass(FullHttpMessage.class); verify(clientListener).messageReceived(responseCaptor.capture()); capturedResponses = responseCaptor.getAllValues(); assertEquals(response, capturedResponses.get(0)); cleanupCapturedResponses(); reset(clientListener); setServerLatch(1); httpHeaders = request2.headers(); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length()); httpHeaders.remove(HttpHeaders.Names.EXPECT); runInChannel( clientChannel, new Http2Runnable() { @Override public void run() { frameWriter.writeData(ctxClient(), 3, payload.retain(), 0, true, newPromiseClient()); ctxClient().flush(); } }); awaitRequests(); requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class); verify(serverListener).messageReceived(requestCaptor.capture()); capturedRequests = requestCaptor.getAllValues(); assertEquals(request2, capturedRequests.get(0)); setClientLatch(1); httpHeaders = response2.headers(); httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0); final Http2Headers http2HeadersResponse2 = new DefaultHttp2Headers().status(as("200")); runInChannel( serverConnectedChannel, new Http2Runnable() { @Override public void run() { frameWriter.writeHeaders( ctxServer(), 3, http2HeadersResponse2, 0, true, newPromiseServer()); ctxServer().flush(); } }); awaitResponses(); responseCaptor = ArgumentCaptor.forClass(FullHttpMessage.class); verify(clientListener).messageReceived(responseCaptor.capture()); capturedResponses = responseCaptor.getAllValues(); assertEquals(response2, capturedResponses.get(0)); } finally { request.release(); request2.release(); response.release(); response2.release(); } }
@Test public void serverRequestPushPromise() throws Exception { setClientLatch(2); final String text = "hello world big time data!"; final ByteBuf content = Unpooled.copiedBuffer(text.getBytes()); final String text2 = "hello world smaller data?"; final ByteBuf content2 = Unpooled.copiedBuffer(text2.getBytes()); final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content, true); final FullHttpMessage response2 = new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.CREATED, content2, true); final FullHttpMessage request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/push/test", true); try { HttpHeaders httpHeaders = response.headers(); httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length()); HttpHeaders httpHeaders2 = response2.headers(); httpHeaders2.set(HttpUtil.ExtensionHeaderNames.SCHEME.text(), "https"); httpHeaders2.set(HttpUtil.ExtensionHeaderNames.AUTHORITY.text(), "example.org"); httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), 3); httpHeaders2.set(HttpHeaders.Names.CONTENT_LENGTH, text2.length()); httpHeaders = request.headers(); httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0); final Http2Headers http2Headers3 = new DefaultHttp2Headers().method(as("GET")).path(as("/push/test")); runInChannel( clientChannel, new Http2Runnable() { @Override public void run() { frameWriter.writeHeaders(ctxClient(), 3, http2Headers3, 0, true, newPromiseClient()); ctxClient().flush(); } }); awaitRequests(); ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class); verify(serverListener).messageReceived(requestCaptor.capture()); capturedRequests = requestCaptor.getAllValues(); assertEquals(request, capturedRequests.get(0)); final Http2Headers http2Headers = new DefaultHttp2Headers().status(as("200")); final Http2Headers http2Headers2 = new DefaultHttp2Headers() .status(as("201")) .scheme(as("https")) .authority(as("example.org")); runInChannel( serverConnectedChannel, new Http2Runnable() { @Override public void run() { frameWriter.writeHeaders(ctxServer(), 3, http2Headers, 0, false, newPromiseServer()); frameWriter.writePushPromise(ctxServer(), 3, 5, http2Headers2, 0, newPromiseServer()); frameWriter.writeData(ctxServer(), 3, content.retain(), 0, true, newPromiseServer()); frameWriter.writeData(ctxServer(), 5, content2.retain(), 0, true, newPromiseServer()); ctxServer().flush(); } }); awaitResponses(); ArgumentCaptor<FullHttpMessage> responseCaptor = ArgumentCaptor.forClass(FullHttpMessage.class); verify(clientListener, times(2)).messageReceived(responseCaptor.capture()); capturedResponses = responseCaptor.getAllValues(); assertEquals(response, capturedResponses.get(0)); assertEquals(response2, capturedResponses.get(1)); } finally { request.release(); response.release(); response2.release(); } }
@Override protected void decode( ChannelHandlerContext ctx, SpdyDataOrControlFrame msg, MessageBuf<Object> out) throws Exception { if (msg instanceof SpdySynStreamFrame) { // HTTP requests/responses are mapped one-to-one to SPDY streams. SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; int streamId = spdySynStreamFrame.getStreamId(); if (SpdyCodecUtil.isServerId(streamId)) { // SYN_STREAM frames initiated by the server are pushed resources int associatedToStreamId = spdySynStreamFrame.getAssociatedToStreamId(); // If a client receives a SYN_STREAM with an Associated-To-Stream-ID of 0 // it must reply with a RST_STREAM with error code INVALID_STREAM if (associatedToStreamId == 0) { SpdyRstStreamFrame spdyRstStreamFrame = new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.INVALID_STREAM); ctx.write(spdyRstStreamFrame); } String URL = SpdyHeaders.getUrl(spdyVersion, spdySynStreamFrame); // If a client receives a SYN_STREAM without a 'url' header // it must reply with a RST_STREAM with error code PROTOCOL_ERROR if (URL == null) { SpdyRstStreamFrame spdyRstStreamFrame = new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR); ctx.write(spdyRstStreamFrame); } try { FullHttpResponse httpResponseWithEntity = createHttpResponse(spdyVersion, spdySynStreamFrame); // Set the Stream-ID, Associated-To-Stream-ID, Priority, and URL as headers SpdyHttpHeaders.setStreamId(httpResponseWithEntity, streamId); SpdyHttpHeaders.setAssociatedToStreamId(httpResponseWithEntity, associatedToStreamId); SpdyHttpHeaders.setPriority(httpResponseWithEntity, spdySynStreamFrame.getPriority()); SpdyHttpHeaders.setUrl(httpResponseWithEntity, URL); if (spdySynStreamFrame.isLast()) { HttpHeaders.setContentLength(httpResponseWithEntity, 0); out.add(httpResponseWithEntity); } else { // Response body will follow in a series of Data Frames putMessage(streamId, httpResponseWithEntity); } } catch (Exception e) { SpdyRstStreamFrame spdyRstStreamFrame = new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR); ctx.write(spdyRstStreamFrame); } } else { // SYN_STREAM frames initiated by the client are HTTP requests try { FullHttpRequest httpRequestWithEntity = createHttpRequest(spdyVersion, spdySynStreamFrame); // Set the Stream-ID as a header SpdyHttpHeaders.setStreamId(httpRequestWithEntity, streamId); if (spdySynStreamFrame.isLast()) { out.add(httpRequestWithEntity); } else { // Request body will follow in a series of Data Frames putMessage(streamId, httpRequestWithEntity); } } catch (Exception e) { // If a client sends a SYN_STREAM without all of the getMethod, url (host and path), // scheme, and version headers the server must reply with a HTTP 400 BAD REQUEST reply. // Also sends HTTP 400 BAD REQUEST reply if header name/value pairs are invalid SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId); spdySynReplyFrame.setLast(true); SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, HttpResponseStatus.BAD_REQUEST); SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, HttpVersion.HTTP_1_0); ctx.write(spdySynReplyFrame); } } } else if (msg instanceof SpdySynReplyFrame) { SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; int streamId = spdySynReplyFrame.getStreamId(); try { FullHttpResponse httpResponseWithEntity = createHttpResponse(spdyVersion, spdySynReplyFrame); // Set the Stream-ID as a header SpdyHttpHeaders.setStreamId(httpResponseWithEntity, streamId); if (spdySynReplyFrame.isLast()) { HttpHeaders.setContentLength(httpResponseWithEntity, 0); out.add(httpResponseWithEntity); } else { // Response body will follow in a series of Data Frames putMessage(streamId, httpResponseWithEntity); } } catch (Exception e) { // If a client receives a SYN_REPLY without valid getStatus and version headers // the client must reply with a RST_STREAM frame indicating a PROTOCOL_ERROR SpdyRstStreamFrame spdyRstStreamFrame = new DefaultSpdyRstStreamFrame(streamId, SpdyStreamStatus.PROTOCOL_ERROR); ctx.write(spdyRstStreamFrame); } } else if (msg instanceof SpdyHeadersFrame) { SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; int streamId = spdyHeadersFrame.getStreamId(); FullHttpMessage fullHttpMessage = getMessage(streamId); // If message is not in map discard HEADERS frame. if (fullHttpMessage == null) { return; } for (Map.Entry<String, String> e : spdyHeadersFrame.headers().entries()) { fullHttpMessage.headers().add(e.getKey(), e.getValue()); } if (spdyHeadersFrame.isLast()) { HttpHeaders.setContentLength(fullHttpMessage, fullHttpMessage.content().readableBytes()); removeMessage(streamId); out.add(fullHttpMessage); } } else if (msg instanceof SpdyDataFrame) { SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; int streamId = spdyDataFrame.getStreamId(); FullHttpMessage fullHttpMessage = getMessage(streamId); // If message is not in map discard Data Frame. if (fullHttpMessage == null) { return; } ByteBuf content = fullHttpMessage.content(); if (content.readableBytes() > maxContentLength - spdyDataFrame.content().readableBytes()) { removeMessage(streamId); throw new TooLongFrameException( "HTTP content length exceeded " + maxContentLength + " bytes."); } ByteBuf spdyDataFrameData = spdyDataFrame.content(); int spdyDataFrameDataLen = spdyDataFrameData.readableBytes(); content.writeBytes(spdyDataFrameData, spdyDataFrameData.readerIndex(), spdyDataFrameDataLen); if (spdyDataFrame.isLast()) { HttpHeaders.setContentLength(fullHttpMessage, content.readableBytes()); removeMessage(streamId); out.add(fullHttpMessage); } } else if (msg instanceof SpdyRstStreamFrame) { SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; int streamId = spdyRstStreamFrame.getStreamId(); removeMessage(streamId); } }
/** * Set final headers and fire a channel read event * * @param ctx The context to fire the event on * @param msg The message to send * @param streamId the streamId of the message which is being fired */ protected void fireChannelRead(ChannelHandlerContext ctx, FullHttpMessage msg, int streamId) { removeMessage(streamId); HttpUtil.setContentLength(msg, msg.content().readableBytes()); ctx.fireChannelRead(msg); }