Skip to content

Commit 08f9b71

Browse files
authored
Merge pull request #26 from FusionAuth/mmanes/header-fix
Add support for non-ASCII header values
2 parents c45f806 + 4ea1105 commit 08f9b71

File tree

7 files changed

+92
-30
lines changed

7 files changed

+92
-30
lines changed

.github/workflows/test.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
name: test
22

3-
# Run the tests when code is pushed to `master`
3+
# Run the tests when code is pushed to `main`
44
on:
55
push:
6-
branches: [ master ]
6+
branches: [ main ]
77

88
# Allows you to run this workflow manually from the Actions tab
99
workflow_dispatch:
@@ -13,8 +13,8 @@ jobs:
1313
runs-on: ubuntu-latest
1414

1515
steps:
16-
- uses: actions/checkout@v3
17-
- uses: actions/setup-java@v3
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-java@v4
1818
with:
1919
distribution: 'temurin'
2020
java-version: 17
@@ -23,9 +23,9 @@ jobs:
2323
mkdir -p ~/dev/savant
2424
mkdir -p ~/.savant/plugins
2525
cd ~/dev/savant
26-
curl -fSL https://github.com/savant-build/savant-core/releases/download/2.0.0-RC.6/savant-2.0.0-RC.6.tar.gz > savant.tar.gz
26+
curl -fSL https://github.com/savant-build/savant-core/releases/download/2.0.0-RC.7/savant-2.0.0-RC.7.tar.gz > savant.tar.gz
2727
tar -xzf savant.tar.gz
28-
ln -s savant-2.0.0-RC.6 current
28+
ln -s savant-2.0.0-RC.7 current
2929
rm savant.tar.gz
3030
cat <<EOF > ~/.savant/plugins/org.savantbuild.plugin.java.properties
3131
17=${JAVA_HOME_17_X64}
@@ -38,7 +38,7 @@ jobs:
3838
shell: bash
3939
- name: Archive TestNG reports
4040
if: failure()
41-
uses: actions/upload-artifact@v3
41+
uses: actions/upload-artifact@v4
4242
with:
4343
name: testng-reports
4444
path: build/test-reports

build.savant

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
* language governing permissions and limitations under the License.
1515
*/
1616
jackson5Version = "3.0.1"
17-
restifyVersion = "4.2.1"
18-
testngVersion = "7.8.0"
17+
restifyVersion = "4.2.1"
18+
testngVersion = "7.10.2"
1919

20-
project(group: "io.fusionauth", name: "java-http", version: "0.3.4", licenses: ["ApacheV2_0"]) {
20+
project(group: "io.fusionauth", name: "java-http", version: "0.3.5", licenses: ["ApacheV2_0"]) {
2121
workflow {
2222
fetch {
2323
// Dependency resolution order:
@@ -54,7 +54,7 @@ project(group: "io.fusionauth", name: "java-http", version: "0.3.4", licenses: [
5454
}
5555

5656
// Plugins
57-
dependency = loadPlugin(id: "org.savantbuild.plugin:dependency:2.0.0-RC.6")
57+
dependency = loadPlugin(id: "org.savantbuild.plugin:dependency:2.0.0-RC.7")
5858
java = loadPlugin(id: "org.savantbuild.plugin:java:2.0.0-RC.6")
5959
javaTestNG = loadPlugin(id: "org.savantbuild.plugin:java-testng:2.0.0-RC.6")
6060
idea = loadPlugin(id: "org.savantbuild.plugin:idea:2.0.0-RC.7")

java-http.iml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@
7272
<orderEntry type="module-library" scope="TEST">
7373
<library>
7474
<CLASSES>
75-
<root url="jar://$MODULE_DIR$/.savant/cache/org/testng/testng/7.8.0/testng-7.8.0.jar!/" />
75+
<root url="jar://$MODULE_DIR$/.savant/cache/org/testng/testng/7.10.2/testng-7.10.2.jar!/" />
7676
</CLASSES>
7777
<JAVADOC />
7878
<SOURCES>
79-
<root url="jar://$MODULE_DIR$/.savant/cache/org/testng/testng/7.8.0/testng-7.8.0-src.jar!/" />
79+
<root url="jar://$MODULE_DIR$/.savant/cache/org/testng/testng/7.10.2/testng-7.10.2-src.jar!/" />
8080
</SOURCES>
8181
</library>
8282
</orderEntry>
@@ -105,11 +105,11 @@
105105
<orderEntry type="module-library" scope="TEST">
106106
<library>
107107
<CLASSES>
108-
<root url="jar://$MODULE_DIR$/.savant/cache/org/webjars/jquery/3.6.1/jquery-3.6.1.jar!/" />
108+
<root url="jar://$MODULE_DIR$/.savant/cache/org/webjars/jquery/3.7.1/jquery-3.7.1.jar!/" />
109109
</CLASSES>
110110
<JAVADOC />
111111
<SOURCES>
112-
<root url="jar://$MODULE_DIR$/.savant/cache/org/webjars/jquery/3.6.1/jquery-3.6.1-src.jar!/" />
112+
<root url="jar://$MODULE_DIR$/.savant/cache/org/webjars/jquery/3.7.1/jquery-3.7.1-src.jar!/" />
113113
</SOURCES>
114114
</library>
115115
</orderEntry>

src/main/java/io/fusionauth/http/server/HTTPRequestProcessor.java

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
*/
1616
package io.fusionauth.http.server;
1717

18+
import java.io.ByteArrayOutputStream;
1819
import java.nio.ByteBuffer;
20+
import java.nio.charset.StandardCharsets;
1921

2022
import io.fusionauth.http.HTTPMethod;
2123
import io.fusionauth.http.HTTPValues.Headers;
@@ -34,8 +36,8 @@
3436
public class HTTPRequestProcessor {
3537
private final int bufferSize;
3638

37-
// TODO : Should this be sized with a configuration parameter?
38-
private final StringBuilder builder = new StringBuilder();
39+
// Allocate a 4k buffer for starters, it will grow as needed.
40+
private final ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream(4096);
3941

4042
private final HTTPServerConfiguration configuration;
4143

@@ -78,28 +80,27 @@ public RequestState processBodyBytes() {
7880

7981
public RequestState processPreambleBytes(ByteBuffer buffer) {
8082
while (buffer.hasRemaining()) {
81-
// TODO : Can we get some performance using ByteBuffer rather than StringBuilder here?
8283

8384
// If there is a state transition, store the value properly and reset the builder (if needed)
8485
byte ch = buffer.get();
8586
RequestPreambleState nextState = preambleState.next(ch);
8687
if (nextState != preambleState) {
8788
switch (preambleState) {
88-
case RequestMethod -> request.setMethod(HTTPMethod.of(builder.toString()));
89-
case RequestPath -> request.setPath(builder.toString());
90-
case RequestProtocol -> request.setProtocol(builder.toString());
91-
case HeaderName -> headerName = builder.toString();
92-
case HeaderValue -> request.addHeader(headerName, builder.toString());
89+
case RequestMethod -> request.setMethod(HTTPMethod.of(byteBuffer.toString(StandardCharsets.UTF_8)));
90+
case RequestPath -> request.setPath(byteBuffer.toString(StandardCharsets.UTF_8));
91+
case RequestProtocol -> request.setProtocol(byteBuffer.toString(StandardCharsets.UTF_8));
92+
case HeaderName -> headerName = byteBuffer.toString(StandardCharsets.UTF_8);
93+
case HeaderValue -> request.addHeader(headerName, byteBuffer.toString(StandardCharsets.UTF_8));
9394
}
9495

9596
// If the next state is storing, reset the builder
9697
if (nextState.store()) {
97-
builder.delete(0, builder.length());
98-
builder.appendCodePoint(ch);
98+
byteBuffer.reset();
99+
byteBuffer.write(ch);
99100
}
100101
} else if (preambleState.store()) {
101102
// If the current state is storing, store the character
102-
builder.appendCodePoint(ch);
103+
byteBuffer.write(ch);
103104
}
104105

105106
preambleState = nextState;

src/main/java/io/fusionauth/http/util/HTTPTools.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,10 @@ public static boolean isURICharacter(byte ch) {
119119
return ch >= '!' && ch <= '~';
120120
}
121121

122+
// RFC9110 section-5.5 allows for "obs-text", which includes 0x80-0xFF, but really shouldn't be used.
122123
public static boolean isValueCharacter(byte ch) {
123-
return isURICharacter(ch) || ch == ' ' || ch == '\t' || ch == '\n';
124+
int intVal = ch & 0xFF; // Convert the value into an integer without extending the sign bit.
125+
return isURICharacter(ch) || intVal == ' ' || intVal == '\t' || intVal == '\n' || intVal >= 0x80;
124126
}
125127

126128
/**

src/test/java/io/fusionauth/http/BaseTest.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022-2023, FusionAuth, All Rights Reserved
2+
* Copyright (c) 2022-2024, FusionAuth, All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,7 +15,6 @@
1515
*/
1616
package io.fusionauth.http;
1717

18-
import java.io.File;
1918
import java.io.IOException;
2019
import java.io.InputStream;
2120
import java.io.OutputStream;
@@ -144,6 +143,18 @@ public HttpClient makeClient(String scheme, CookieHandler cookieHandler) throws
144143
return builder.connectTimeout(ClientTimeout).build();
145144
}
146145

146+
public Socket makeClientSocket(String scheme) throws GeneralSecurityException, IOException {
147+
Socket socket;
148+
if (scheme.equals("https")) {
149+
var ctx = SecurityTools.clientContext(rootCertificate);
150+
socket = ctx.getSocketFactory().createSocket("127.0.0.1", 4242);
151+
} else {
152+
socket = new Socket("127.0.0.1", 4242);
153+
}
154+
155+
return socket;
156+
}
157+
147158
public HTTPServer makeServer(String scheme, HTTPHandler handler, Instrumenter instrumenter) {
148159
return makeServer(scheme, handler, instrumenter, null);
149160
}

src/test/java/io/fusionauth/http/CoreTest.java

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022-2023, FusionAuth, All Rights Reserved
2+
* Copyright (c) 2022-2024, FusionAuth, All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -787,6 +787,54 @@ public void unicode(String scheme) throws Exception {
787787
}
788788
}
789789

790+
@Test(dataProvider = "schemes")
791+
public void utf8HeaderValues(String scheme) throws Exception {
792+
793+
var city = "São Paulo";
794+
795+
HTTPHandler handler = (req, res) -> {
796+
res.setHeader(Headers.ContentType, "text/plain");
797+
res.setHeader(Headers.ContentLength, "" + ExpectedResponse.length());
798+
res.setHeader("X-Response-Header", city);
799+
res.setStatus(200);
800+
801+
try {
802+
OutputStream outputStream = res.getOutputStream();
803+
outputStream.write(ExpectedResponse.getBytes());
804+
outputStream.close();
805+
} catch (IOException e) {
806+
throw new RuntimeException(e);
807+
}
808+
};
809+
810+
// Java HttpClient only supports ASCII header values, so send request directly
811+
try (HTTPServer ignore = makeServer(scheme, handler).start();
812+
Socket sock = makeClientSocket(scheme)) {
813+
814+
var os = sock.getOutputStream();
815+
var is = sock.getInputStream();
816+
os.write(String.format("""
817+
GET /api/status HTTP/1.1\r
818+
Host: localhost:42\r
819+
X-Request-Header: %s\r
820+
\r
821+
""", city)
822+
.getBytes(StandardCharsets.UTF_8));
823+
os.flush();
824+
825+
var resp = new String(is.readAllBytes(), StandardCharsets.UTF_8);
826+
827+
assertEquals(resp, String.format("""
828+
HTTP/1.1 200 \r
829+
content-length: 16\r
830+
content-type: text/plain\r
831+
connection: keep-alive\r
832+
x-response-header: %s\r
833+
\r
834+
{"version":"42"}""", city));
835+
}
836+
}
837+
790838
@Test(dataProvider = "schemes")
791839
public void writer(String scheme) throws Exception {
792840
HTTPHandler handler = (req, res) -> {

0 commit comments

Comments
 (0)