From 8e2da6b64e7a9947673f2ad635370ab30e447e40 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 9 Apr 2019 21:16:02 +0200 Subject: [PATCH] Issue #250 - Implement HTTP CONNECT for HTTP/2. Implemented semantic defined by RFC 8441. Signed-off-by: Simone Bordet --- .../org/eclipse/jetty/http/HttpHeader.java | 1 + .../java/org/eclipse/jetty/http/MetaData.java | 43 ++++++++------- .../jetty/http2/client/ConnectTunnelTest.java | 54 +++++++++++++++++++ .../jetty/http2/hpack/HpackEncoder.java | 6 ++- .../jetty/http2/hpack/MetaDataBuilder.java | 37 ++++++++----- 5 files changed, 109 insertions(+), 32 deletions(-) diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java index f39c0f7df98f..e03e267db285 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java @@ -132,6 +132,7 @@ public enum HttpHeader C_AUTHORITY(":authority"), C_PATH(":path"), C_STATUS(":status"), + C_PROTOCOL(":protocol"), UNKNOWN("::UNKNOWN::"); diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java b/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java index 11ad1149340b..8e76ab334127 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java @@ -133,6 +133,7 @@ public static class Request extends MetaData { private String _method; private HttpURI _uri; + private String _protocol; public Request(HttpFields fields) { @@ -153,31 +154,26 @@ public Request(String method, HttpURI uri, HttpVersion version, HttpFields field public Request(String method, HttpScheme scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields) { - this(method, new HttpURI(scheme == null ? null : scheme.asString(), - hostPort==null?null:hostPort.getHost(), - hostPort==null?-1:hostPort.getPort(), - uri), version, fields); + this(method, scheme, hostPort, uri, version, fields, Long.MIN_VALUE); } public Request(String method, HttpScheme scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields, long contentLength) { - this(method, new HttpURI(scheme==null?null:scheme.asString(), - hostPort==null?null:hostPort.getHost(), - hostPort==null?-1:hostPort.getPort(), - uri), version, fields, contentLength); + this(method, scheme == null ? null : scheme.asString(), hostPort, uri, version, fields, contentLength); } public Request(String method, String scheme, HostPortHttpField hostPort, String uri, HttpVersion version, HttpFields fields, long contentLength) { this(method, new HttpURI(scheme, - hostPort==null?null:hostPort.getHost(), - hostPort==null?-1:hostPort.getPort(), - uri), version, fields, contentLength); + hostPort == null ? null : hostPort.getHost(), + hostPort == null ? -1 : hostPort.getPort(), + uri), version, fields, contentLength); } public Request(Request request) { this(request.getMethod(),new HttpURI(request.getURI()), request.getHttpVersion(), new HttpFields(request.getFields()), request.getContentLength()); + setProtocol(request.getProtocol()); } @Override @@ -187,6 +183,7 @@ public void recycle() _method = null; if (_uri != null) _uri.clear(); + _protocol = null; } @Override @@ -219,6 +216,14 @@ public HttpURI getURI() return _uri; } + /** + * @param uri the HTTP URI to set + */ + public void setURI(HttpURI uri) + { + _uri = uri; + } + /** * @return the HTTP URI in string form */ @@ -227,20 +232,22 @@ public String getURIString() return _uri == null ? null : _uri.toString(); } - /** - * @param uri the HTTP URI to set - */ - public void setURI(HttpURI uri) + public String getProtocol() { - _uri = uri; + return _protocol; + } + + public void setProtocol(String protocol) + { + _protocol = protocol; } @Override public String toString() { HttpFields fields = getFields(); - return String.format("%s{u=%s,%s,h=%d,cl=%d}", - getMethod(), getURI(), getHttpVersion(), fields == null ? -1 : fields.size(), getContentLength()); + return String.format("%s{u=%s,%s,h=%d,cl=%d,p=%s}", + getMethod(), getURI(), getHttpVersion(), fields == null ? -1 : fields.size(), getContentLength(), getProtocol()); } } diff --git a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ConnectTunnelTest.java b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ConnectTunnelTest.java index 42184ad2b9f6..b9efedca8492 100644 --- a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ConnectTunnelTest.java +++ b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ConnectTunnelTest.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.http.HostPortHttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MetaData; @@ -95,4 +96,57 @@ public void onData(Stream stream, DataFrame frame, Callback callback) assertTrue(latch.await(5, TimeUnit.SECONDS)); } + + @Test + public void testCONNECTWithProtocol() throws Exception + { + start(new ServerSessionListener.Adapter() + { + @Override + public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) + { + // Verifies that the CONNECT request is well formed. + MetaData.Request request = (MetaData.Request)frame.getMetaData(); + assertEquals(HttpMethod.CONNECT.asString(), request.getMethod()); + HttpURI uri = request.getURI(); + assertNotNull(uri.getScheme()); + assertNotNull(uri.getPath()); + assertNotNull(uri.getAuthority()); + assertNotNull(request.getProtocol()); + return new Stream.Listener.Adapter() + { + @Override + public void onData(Stream stream, DataFrame frame, Callback callback) + { + stream.data(frame, callback); + } + }; + } + }); + + Session client = newClient(new Session.Listener.Adapter()); + + CountDownLatch latch = new CountDownLatch(1); + byte[] bytes = "HELLO".getBytes(StandardCharsets.UTF_8); + String host = "localhost"; + int port = connector.getLocalPort(); + String authority = host + ":" + port; + MetaData.Request request = new MetaData.Request(HttpMethod.CONNECT.asString(), HttpScheme.HTTP, new HostPortHttpField(authority), "/", HttpVersion.HTTP_2, new HttpFields()); + request.setProtocol("websocket"); + FuturePromise streamPromise = new FuturePromise<>(); + client.newStream(new HeadersFrame(request, null, false), streamPromise, new Stream.Listener.Adapter() + { + @Override + public void onData(Stream stream, DataFrame frame, Callback callback) + { + if (frame.isEndStream()) + latch.countDown(); + } + }); + Stream stream = streamPromise.get(5, TimeUnit.SECONDS); + ByteBuffer data = ByteBuffer.wrap(bytes); + stream.data(new DataFrame(stream.getId(), data, true), Callback.NOOP); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } } diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java index 4e0af53a5de8..bf5db5f4b85e 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java @@ -167,10 +167,14 @@ public void encode(ByteBuffer buffer, MetaData metadata) String scheme=request.getURI().getScheme(); encode(buffer,new HttpField(HttpHeader.C_METHOD,request.getMethod())); encode(buffer,new HttpField(HttpHeader.C_AUTHORITY,request.getURI().getAuthority())); - if (!HttpMethod.CONNECT.is(request.getMethod())) + boolean isConnect = HttpMethod.CONNECT.is(request.getMethod()); + String protocol = request.getProtocol(); + if (!isConnect || protocol != null) { encode(buffer,new HttpField(HttpHeader.C_SCHEME,scheme==null?HttpScheme.HTTP.asString():scheme)); encode(buffer,new HttpField(HttpHeader.C_PATH,request.getURI().getPathQuery())); + if (protocol != null) + encode(buffer,new HttpField(HttpHeader.C_PROTOCOL,protocol)); } } else if (metadata.isResponse()) diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java index 5f0e680e606a..212891027e3b 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java @@ -39,6 +39,7 @@ public class MetaDataBuilder private HttpScheme _scheme; private HostPortHttpField _authority; private String _path; + private String _protocol; private long _contentLength=Long.MIN_VALUE; private HttpFields _fields = new HttpFields(10); private HpackException.StreamException _streamException; @@ -139,6 +140,23 @@ else if (value != null) _request = true; break; + case C_PATH: + if(checkPseudoHeader(header, _path)) + { + if (value!=null && value.length()>0) + _path = value; + else + streamException("No Path"); + } + _request = true; + break; + + case C_PROTOCOL: + if (checkPseudoHeader(header, _protocol)) + _protocol = value; + _request = true; + break; + case HOST: // :authority fields must come first. If we have one, ignore the host header as far as authority goes. if (_authority==null) @@ -151,17 +169,6 @@ else if (value != null) _fields.add(field); break; - case C_PATH: - if(checkPseudoHeader(header, _path)) - { - if (value!=null && value.length()>0) - _path = value; - else - streamException("No Path"); - } - _request = true; - break; - case CONTENT_LENGTH: _contentLength = field.getLongValue(); _fields.add(field); @@ -239,14 +246,17 @@ public MetaData build() throws HpackException.StreamException { if (_method==null) throw new HpackException.StreamException("No Method"); - if (!HttpMethod.CONNECT.is(_method)) + boolean isConnect = HttpMethod.CONNECT.is(_method); + if (!isConnect || _protocol != null) { if (_scheme==null) throw new HpackException.StreamException("No Scheme"); if (_path==null) throw new HpackException.StreamException("No Path"); } - return new MetaData.Request(_method,_scheme,_authority,_path,HttpVersion.HTTP_2,fields,_contentLength); + MetaData.Request request = new MetaData.Request(_method, _scheme, _authority, _path, HttpVersion.HTTP_2, fields, _contentLength); + request.setProtocol(_protocol); + return request; } if (_response) { @@ -267,6 +277,7 @@ public MetaData build() throws HpackException.StreamException _scheme = null; _authority = null; _path = null; + _protocol = null; _size = 0; _contentLength = Long.MIN_VALUE; }