Skip to content

Commit 910d0fb

Browse files
committed
Add WebSocket support to httpcore5-websocket
Improve server handling and add extensive core tests/examples Align client usage with core WebSocket APIs Extend HTTP/2 request conversion and tests
1 parent dbe6198 commit 910d0fb

83 files changed

Lines changed: 7311 additions & 8 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

httpcore5-h2/src/main/java/org/apache/hc/core5/http2/H2PseudoRequestHeaders.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,9 @@ public final class H2PseudoRequestHeaders {
3838
public static final String SCHEME = ":scheme";
3939
public static final String AUTHORITY = ":authority";
4040
public static final String PATH = ":path";
41+
/**
42+
* RFC 8441 extended CONNECT pseudo-header.
43+
*/
44+
public static final String PROTOCOL = ":protocol";
4145

4246
}

httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/DefaultH2RequestConverter.java

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public HttpRequest convert(final List<Header> headers) throws HttpException {
6161
String scheme = null;
6262
String authority = null;
6363
String path = null;
64+
String protocol = null;
6465
final List<Header> messageHeaders = new ArrayList<>();
6566

6667
for (int i = 0; i < headers.size(); i++) {
@@ -98,6 +99,12 @@ public HttpRequest convert(final List<Header> headers) throws HttpException {
9899
case H2PseudoRequestHeaders.AUTHORITY:
99100
authority = value;
100101
break;
102+
case H2PseudoRequestHeaders.PROTOCOL:
103+
if (protocol != null) {
104+
throw new ProtocolException("Multiple '%s' request headers are illegal", name);
105+
}
106+
protocol = value;
107+
break;
101108
default:
102109
throw new ProtocolException("Unsupported request header '%s'", name);
103110
}
@@ -118,11 +125,21 @@ public HttpRequest convert(final List<Header> headers) throws HttpException {
118125
if (authority == null) {
119126
throw new ProtocolException("Header '%s' is mandatory for CONNECT request", H2PseudoRequestHeaders.AUTHORITY);
120127
}
121-
if (scheme != null) {
122-
throw new ProtocolException("Header '%s' must not be set for CONNECT request", H2PseudoRequestHeaders.SCHEME);
123-
}
124-
if (path != null) {
125-
throw new ProtocolException("Header '%s' must not be set for CONNECT request", H2PseudoRequestHeaders.PATH);
128+
if (protocol != null) {
129+
if (scheme == null) {
130+
throw new ProtocolException("Header '%s' is mandatory for extended CONNECT", H2PseudoRequestHeaders.SCHEME);
131+
}
132+
if (path == null) {
133+
throw new ProtocolException("Header '%s' is mandatory for extended CONNECT", H2PseudoRequestHeaders.PATH);
134+
}
135+
validatePathPseudoHeader(method, scheme, path);
136+
} else {
137+
if (scheme != null) {
138+
throw new ProtocolException("Header '%s' must not be set for CONNECT request", H2PseudoRequestHeaders.SCHEME);
139+
}
140+
if (path != null) {
141+
throw new ProtocolException("Header '%s' must not be set for CONNECT request", H2PseudoRequestHeaders.PATH);
142+
}
126143
}
127144
} else {
128145
if (scheme == null) {
@@ -143,6 +160,9 @@ public HttpRequest convert(final List<Header> headers) throws HttpException {
143160
throw new ProtocolException(ex.getMessage(), ex);
144161
}
145162
httpRequest.setPath(path);
163+
if (protocol != null) {
164+
httpRequest.addHeader(new BasicHeader(H2PseudoRequestHeaders.PROTOCOL, protocol));
165+
}
146166
for (int i = 0; i < messageHeaders.size(); i++) {
147167
httpRequest.addHeader(messageHeaders.get(i));
148168
}
@@ -155,12 +175,26 @@ public List<Header> convert(final HttpRequest message) throws HttpException {
155175
throw new ProtocolException("Request method is empty");
156176
}
157177
final boolean optionMethod = Method.CONNECT.name().equalsIgnoreCase(message.getMethod());
178+
final Header protocolHeader = message.getFirstHeader(H2PseudoRequestHeaders.PROTOCOL);
179+
final String protocol = protocolHeader != null ? protocolHeader.getValue() : null;
180+
if (protocol != null && !optionMethod) {
181+
throw new ProtocolException("Header name '%s' is invalid", H2PseudoRequestHeaders.PROTOCOL);
182+
}
158183
if (optionMethod) {
159184
if (message.getAuthority() == null) {
160185
throw new ProtocolException("CONNECT request authority is not set");
161186
}
162-
if (message.getPath() != null) {
163-
throw new ProtocolException("CONNECT request path must be null");
187+
if (protocol != null) {
188+
if (TextUtils.isBlank(message.getScheme())) {
189+
throw new ProtocolException("CONNECT request scheme is not set");
190+
}
191+
if (TextUtils.isBlank(message.getPath())) {
192+
throw new ProtocolException("CONNECT request path is not set");
193+
}
194+
} else {
195+
if (message.getPath() != null) {
196+
throw new ProtocolException("CONNECT request path must be null");
197+
}
164198
}
165199
} else {
166200
if (TextUtils.isBlank(message.getScheme())) {
@@ -173,7 +207,14 @@ public List<Header> convert(final HttpRequest message) throws HttpException {
173207
final List<Header> headers = new ArrayList<>();
174208
headers.add(new BasicHeader(H2PseudoRequestHeaders.METHOD, message.getMethod(), false));
175209
if (optionMethod) {
176-
headers.add(new BasicHeader(H2PseudoRequestHeaders.AUTHORITY, message.getAuthority(), false));
210+
if (protocol != null) {
211+
headers.add(new BasicHeader(H2PseudoRequestHeaders.PROTOCOL, protocol, false));
212+
headers.add(new BasicHeader(H2PseudoRequestHeaders.SCHEME, message.getScheme(), false));
213+
headers.add(new BasicHeader(H2PseudoRequestHeaders.AUTHORITY, message.getAuthority(), false));
214+
headers.add(new BasicHeader(H2PseudoRequestHeaders.PATH, message.getPath(), false));
215+
} else {
216+
headers.add(new BasicHeader(H2PseudoRequestHeaders.AUTHORITY, message.getAuthority(), false));
217+
}
177218
} else {
178219
headers.add(new BasicHeader(H2PseudoRequestHeaders.SCHEME, message.getScheme(), false));
179220
if (message.getAuthority() != null) {
@@ -186,6 +227,12 @@ public List<Header> convert(final HttpRequest message) throws HttpException {
186227
final Header header = it.next();
187228
final String name = header.getName();
188229
final String value = header.getValue();
230+
if (name.startsWith(":")) {
231+
if (optionMethod && H2PseudoRequestHeaders.PROTOCOL.equals(name)) {
232+
continue;
233+
}
234+
throw new ProtocolException("Header name '%s' is invalid", name);
235+
}
189236
if (!FieldValidationSupport.isNameValid(name)) {
190237
throw new ProtocolException("Header name '%s' is invalid", name);
191238
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.core5.http2.impl;
28+
29+
import static org.junit.jupiter.api.Assertions.assertEquals;
30+
import static org.junit.jupiter.api.Assertions.assertNotNull;
31+
32+
import java.util.ArrayList;
33+
import java.util.List;
34+
35+
import org.apache.hc.core5.http.Header;
36+
import org.apache.hc.core5.http.HttpRequest;
37+
import org.apache.hc.core5.http.Method;
38+
import org.apache.hc.core5.http.message.BasicHeader;
39+
import org.apache.hc.core5.http2.H2PseudoRequestHeaders;
40+
import org.apache.hc.core5.net.URIAuthority;
41+
import org.junit.jupiter.api.Test;
42+
43+
class TestExtendedConnectRequestConverter {
44+
45+
@Test
46+
void parsesExtendedConnect() throws Exception {
47+
final List<Header> headers = new ArrayList<>();
48+
headers.add(new BasicHeader(H2PseudoRequestHeaders.METHOD, Method.CONNECT.name(), false));
49+
headers.add(new BasicHeader(H2PseudoRequestHeaders.PROTOCOL, "websocket", false));
50+
headers.add(new BasicHeader(H2PseudoRequestHeaders.SCHEME, "https", false));
51+
headers.add(new BasicHeader(H2PseudoRequestHeaders.AUTHORITY, "example.com", false));
52+
headers.add(new BasicHeader(H2PseudoRequestHeaders.PATH, "/echo", false));
53+
54+
final DefaultH2RequestConverter converter = new DefaultH2RequestConverter();
55+
final HttpRequest request = converter.convert(headers);
56+
assertNotNull(request);
57+
assertEquals(Method.CONNECT.name(), request.getMethod());
58+
assertEquals("/echo", request.getPath());
59+
assertEquals("websocket", request.getFirstHeader(H2PseudoRequestHeaders.PROTOCOL).getValue());
60+
}
61+
62+
@Test
63+
void emitsProtocolPseudoHeader() throws Exception {
64+
final DefaultH2RequestConverter converter = new DefaultH2RequestConverter();
65+
final HttpRequest request = new org.apache.hc.core5.http.message.BasicHttpRequest(Method.CONNECT.name(), "/echo");
66+
request.setScheme("https");
67+
request.setAuthority(new URIAuthority("example.com"));
68+
request.setPath("/echo");
69+
request.addHeader(H2PseudoRequestHeaders.PROTOCOL, "websocket");
70+
final List<Header> headers = converter.convert(request);
71+
boolean found = false;
72+
for (final Header header : headers) {
73+
if (H2PseudoRequestHeaders.PROTOCOL.equals(header.getName())) {
74+
found = true;
75+
break;
76+
}
77+
}
78+
assertEquals(true, found);
79+
}
80+
}

httpcore5-websocket/pom.xml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
====================================================================
4+
Licensed to the Apache Software Foundation (ASF) under one
5+
or more contributor license agreements. See the NOTICE file
6+
distributed with this work for additional information
7+
regarding copyright ownership. The ASF licenses this file
8+
to you under the Apache License, Version 2.0 (the
9+
"License"); you may not use this file except in compliance
10+
with the License. You may obtain a copy of the License at
11+
12+
http://www.apache.org/licenses/LICENSE-2.0
13+
14+
Unless required by applicable law or agreed to in writing,
15+
software distributed under the License is distributed on an
16+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17+
KIND, either express or implied. See the License for the
18+
specific language governing permissions and limitations
19+
under the License.
20+
====================================================================
21+
22+
This software consists of voluntary contributions made by many
23+
individuals on behalf of the Apache Software Foundation. For more
24+
information on the Apache Software Foundation, please see
25+
<http://www.apache.org />.
26+
--><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">
27+
<modelVersion>4.0.0</modelVersion>
28+
<parent>
29+
<groupId>org.apache.httpcomponents.core5</groupId>
30+
<artifactId>httpcore5-parent</artifactId>
31+
<version>5.5-alpha1-SNAPSHOT</version>
32+
</parent>
33+
34+
<artifactId>httpcore5-websocket</artifactId>
35+
<name>Apache HttpComponents Core WebSocket</name>
36+
<inceptionYear>2005</inceptionYear>
37+
<description>Apache HttpComponents WebSocket server support built on HttpCore</description>
38+
39+
<properties>
40+
<Automatic-Module-Name>org.apache.httpcomponents.core5.websocket</Automatic-Module-Name>
41+
</properties>
42+
43+
<dependencies>
44+
<dependency>
45+
<groupId>org.apache.httpcomponents.core5</groupId>
46+
<artifactId>httpcore5</artifactId>
47+
</dependency>
48+
<dependency>
49+
<groupId>org.apache.httpcomponents.core5</groupId>
50+
<artifactId>httpcore5-h2</artifactId>
51+
</dependency>
52+
53+
<dependency>
54+
<groupId>org.junit.jupiter</groupId>
55+
<artifactId>junit-jupiter</artifactId>
56+
<scope>test</scope>
57+
</dependency>
58+
<dependency>
59+
<groupId>org.junit.platform</groupId>
60+
<artifactId>junit-platform-launcher</artifactId>
61+
<scope>test</scope>
62+
</dependency>
63+
<dependency>
64+
<groupId>org.hamcrest</groupId>
65+
<artifactId>hamcrest</artifactId>
66+
<scope>test</scope>
67+
</dependency>
68+
</dependencies>
69+
70+
</project>

0 commit comments

Comments
 (0)