Skip to content

Commit c1c890f

Browse files
committed
Working
1 parent 1135e86 commit c1c890f

File tree

9 files changed

+230
-56
lines changed

9 files changed

+230
-56
lines changed

src/main/java/io/fusionauth/http/HTTPValues.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ public static final class ContentEncodings {
5151

5252
public static final String Gzip = "gzip";
5353

54+
public static final String XGzip = "x-gzip";
55+
5456
private ContentEncodings() {
5557
}
5658
}

src/main/java/io/fusionauth/http/io/PushbackInputStream.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ public class PushbackInputStream extends InputStream {
3838

3939
private int bufferPosition;
4040

41-
4241
public PushbackInputStream(InputStream delegate, Instrumenter instrumenter) {
4342
this.delegate = delegate;
4443
this.instrumenter = instrumenter;

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

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import io.fusionauth.http.FileInfo;
4545
import io.fusionauth.http.HTTPMethod;
4646
import io.fusionauth.http.HTTPValues.Connections;
47+
import io.fusionauth.http.HTTPValues.ContentEncodings;
4748
import io.fusionauth.http.HTTPValues.ContentTypes;
4849
import io.fusionauth.http.HTTPValues.Headers;
4950
import io.fusionauth.http.HTTPValues.Protocols;
@@ -231,7 +232,10 @@ public List<String> getAcceptEncodings() {
231232

232233
public void setAcceptEncodings(List<String> encodings) {
233234
this.acceptEncodings.clear();
234-
this.acceptEncodings.addAll(encodings);
235+
// TODO : ? Maybe not worth being defensive here since we likely have many methods on this object that are not null safe?
236+
if (encodings != null) {
237+
this.acceptEncodings.addAll(encodings);
238+
}
235239
}
236240

237241
/**
@@ -302,9 +306,13 @@ public List<String> getContentEncodings() {
302306
return contentEncodings;
303307
}
304308

309+
// TODO : Note : Should I add 'addContentEncodings' and 'addContentEncoding' to match 'accept*' ?
305310
public void setContentEncodings(List<String> encodings) {
306311
this.contentEncodings.clear();
307-
this.contentEncodings.addAll(encodings);
312+
// TODO : ? Maybe not worth being defensive here since we likely have many methods on this object that are not null safe?
313+
if (encodings != null) {
314+
this.contentEncodings.addAll(encodings);
315+
}
308316
}
309317

310318
public Long getContentLength() {
@@ -726,7 +734,7 @@ public void setURLParameters(String name, Collection<String> values) {
726734

727735
private void decodeHeader(String name, String value) {
728736
switch (name) {
729-
case Headers.AcceptEncodingLower, Headers.ContentEncodingLower:
737+
case Headers.AcceptEncodingLower:
730738
SortedSet<WeightedString> weightedStrings = new TreeSet<>();
731739
String[] parts = value.split(",");
732740
int index = 0;
@@ -743,21 +751,19 @@ private void decodeHeader(String name, String value) {
743751
weight = Double.parseDouble(weightText);
744752
}
745753

746-
WeightedString ws = new WeightedString(parsed.value(), weight, index);
754+
// Content-Encoding values are not case-sensitive
755+
// TODO : Ok to lc here? We are currently just always using equalsIgnoreCase
756+
WeightedString ws = new WeightedString(parsed.value().toLowerCase(), weight, index);
747757
weightedStrings.add(ws);
748758
index++;
749759
}
750760

751761
// Transfer the Strings in weighted-position order
752-
var result = weightedStrings.stream()
753-
.map(WeightedString::value)
754-
.toList();
755-
756-
if (name.equals(Headers.AcceptEncodingLower)) {
757-
setAcceptEncodings(result);
758-
} else {
759-
setContentEncodings(result);
760-
}
762+
setAcceptEncodings(
763+
weightedStrings.stream()
764+
.map(WeightedString::value)
765+
.toList()
766+
);
761767
break;
762768
case Headers.AcceptLanguageLower:
763769
try {
@@ -771,6 +777,31 @@ private void decodeHeader(String name, String value) {
771777
// Ignore the exception and keep the value null
772778
}
773779
break;
780+
case Headers.ContentEncodingLower:
781+
// TODO : Note that we don't expect more than one Content-Encoding header. We could take the last one we find,
782+
// or try and combine the values. MDN and other places indicate combining may be preferred even though
783+
// it isn't ideal to send multiple headers like this. I tend to think we should just accept the last one.
784+
String[] encodings = value.split(",");
785+
List<String> contentEncodings = new ArrayList<>(1);
786+
int encodingIndex = 0;
787+
for (String encoding : encodings) {
788+
// TODO : Ok to lc here? We are currently just always using equalsIgnoreCase
789+
encoding = encoding.trim().toLowerCase();
790+
if (encoding.isEmpty()) {
791+
continue;
792+
}
793+
794+
// The HTTP/1.1 standard recommends that the servers supporting gzip also recognize x-gzip as an alias, for compatibility purposes.
795+
if (encoding.equals(ContentEncodings.XGzip)) {
796+
encoding = ContentEncodings.Gzip;
797+
}
798+
799+
contentEncodings.add(encoding);
800+
encodingIndex++;
801+
}
802+
803+
setContentEncodings(contentEncodings);
804+
break;
774805
case Headers.ContentTypeLower:
775806
this.encoding = null;
776807
this.multipart = false;

src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.fusionauth.http.HTTPProcessingException;
2626
import io.fusionauth.http.HTTPValues;
2727
import io.fusionauth.http.HTTPValues.Connections;
28+
import io.fusionauth.http.HTTPValues.ContentEncodings;
2829
import io.fusionauth.http.HTTPValues.Headers;
2930
import io.fusionauth.http.HTTPValues.Protocols;
3031
import io.fusionauth.http.ParseException;
@@ -149,6 +150,8 @@ public void run() {
149150
// Also... with Pushback bytes... the bytes may be compressed for the next request. So we'll need to be able to
150151
// read handle this. So perhaps this should all be handled at a high level?
151152

153+
// HTTPInputStream > Pushback > Decompression > Throughput > Socket
154+
152155
httpInputStream = new HTTPInputStream(configuration, request, inputStream);
153156
request.setInputStream(httpInputStream);
154157

@@ -449,6 +452,25 @@ private Integer validatePreamble(HTTPRequest request) {
449452
request.removeHeader(Headers.ContentLength);
450453
}
451454

455+
// TODO : Note that some indicate you can return a 415 based upon the Accept-Encoding header as well if you do not support the request.
456+
// But I don't know that I agree- to me that is just telling the server here are encoding values I support, and you can tell me what you did.
457+
// The spec even says you can ignore the Accept-Encoding header if you want based upon CPU load, or other reasons.
458+
// Not planning to add any validation for Accept-Encoding.
459+
460+
// Content-Encoding
461+
var contentEncodings = request.getContentEncodings();
462+
// TODO : If provided, I think we can safely remove the Content-Length header if present?
463+
// Although, I suppose as long as we decompress early, we should still be able to use the Content-Length header as long as
464+
// it represents the un-compressed payload.
465+
for (var encoding : contentEncodings) {
466+
// We only support gzip and deflate.
467+
// TODO : Ensure we use the same equals check here as other places, I think we should lowercase during parse, and then expect these to be lc.
468+
if (!encoding.equals(ContentEncodings.Gzip) && !encoding.equals(ContentEncodings.Deflate)) {
469+
// TODO : Add log statement
470+
return Status.UnsupportedMediaType;
471+
}
472+
}
473+
452474
return null;
453475
}
454476

@@ -467,6 +489,8 @@ private enum CloseSocketReason {
467489
private static class Status {
468490
public static final int BadRequest = 400;
469491

492+
public static final int UnsupportedMediaType = 415;
493+
470494
public static final int HTTPVersionNotSupported = 505;
471495

472496
public static final int InternalServerError = 500;

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

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717

1818
import java.io.IOException;
1919
import java.io.InputStream;
20-
import java.util.zip.DeflaterInputStream;
2120
import java.util.zip.GZIPInputStream;
21+
import java.util.zip.InflaterInputStream;
2222

2323
import io.fusionauth.http.HTTPValues.ContentEncodings;
2424
import io.fusionauth.http.io.ChunkedInputStream;
@@ -170,18 +170,20 @@ private void commit() throws IOException {
170170
Long contentLength = request.getContentLength();
171171
boolean hasBody = (contentLength != null && contentLength > 0) || request.isChunked();
172172

173-
// if (hasBody) {
174-
// // The request may contain more than one value, apply in reverse order.
175-
// for (String contentEncoding : request.getContentEncodings().reversed()) {
176-
// if (contentEncoding.equalsIgnoreCase(ContentEncodings.Deflate)) {
177-
// // Use the default buffer size of 512
178-
// delegate = new DeflaterInputStream(delegate);
179-
// } else if (contentEncoding.equalsIgnoreCase(ContentEncodings.Gzip)) {
180-
// // Use the default buffer size of 512
181-
// delegate = new GZIPInputStream(delegate);
182-
// }
183-
// }
184-
// }
173+
// Request order of operations:
174+
// - "hello world" -> compress -> chunked.
175+
176+
// Current:
177+
// Chunked:
178+
// HTTPInputStream > Chunked > Pushback > Throughput > Socket
179+
// Fixed:
180+
// HTTPInputStream > Pushback > Throughput > Socket
181+
182+
// New:
183+
// Chunked
184+
// HTTPInputStream > Chunked > Pushback > Decompress > Throughput > Socket
185+
// Fixed
186+
// HTTPInputStream > Pushback > Decompress > Throughput > Socket
185187

186188
// Note that isChunked() should take precedence over the fact that we have a Content-Length.
187189
// - The client should not send both, but in the case they are both present we ignore Content-Length
@@ -198,5 +200,19 @@ private void commit() throws IOException {
198200
} else {
199201
logger.trace("Client indicated it was NOT sending an entity-body in the request");
200202
}
203+
204+
// TODO : Note I could leave this alone, but when we parse the header we can lower case these values and then remove the equalsIgnoreCase here?
205+
// Seems like ideally we would normalize them to lowercase earlier.
206+
if (hasBody) {
207+
// The request may contain more than one value, apply in reverse order.
208+
// - These are both using the default 512 buffer size.
209+
for (String contentEncoding : request.getContentEncodings().reversed()) {
210+
if (contentEncoding.equalsIgnoreCase(ContentEncodings.Deflate)) {
211+
delegate = new InflaterInputStream(delegate);
212+
} else if (contentEncoding.equalsIgnoreCase(ContentEncodings.Gzip)) {
213+
delegate = new GZIPInputStream(delegate);
214+
}
215+
}
216+
}
201217
}
202218
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ public void reset() {
128128
*/
129129
public boolean willCompress() {
130130
if (compress) {
131+
// TODO : Note I could leave this alone, but when we parse the header we can lower case these values and then remove the equalsIgnoreCase here?
132+
// Seems like ideally we would normalize them to lowercase earlier.
133+
// Hmm.. sems like we have to in theory since someone could call setAcceptEncodings later, or addAcceptEncodings?
131134
for (String encoding : acceptEncodings) {
132135
if (encoding.equalsIgnoreCase(ContentEncodings.Gzip)) {
133136
return true;
@@ -191,7 +194,11 @@ private void commit(boolean closing) throws IOException {
191194
response.setContentLength(0L);
192195
} else {
193196
// 204 status is specifically "No Content" so we shouldn't write the content-encoding and vary headers if the status is 204
197+
// TODO : Compress by default is on by default. But it looks like we don't actually compress unless you also send in an Accept-Encoding header?
194198
if (compress && !twoOhFour) {
199+
// TODO : Note I could leave this alone, but when we parse the header we can lower case these values and then remove the equalsIgnoreCase here?
200+
// Seems like ideally we would normalize them to lowercase earlier.
201+
// Hmm.. sems like we have to in theory since someone could call setAcceptEncodings later, or addAcceptEncodings?
195202
for (String encoding : acceptEncodings) {
196203
if (encoding.equalsIgnoreCase(ContentEncodings.Gzip)) {
197204
response.setHeader(Headers.ContentEncoding, ContentEncodings.Gzip);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ public static boolean isHexadecimalCharacter(byte ch) {
9292
*/
9393
public static boolean isTokenCharacter(byte ch) {
9494
return ch == '!' || ch == '#' || ch == '$' || ch == '%' || ch == '&' || ch == '\'' || ch == '*' || ch == '+' || ch == '-' || ch == '.' ||
95-
ch == '^' || ch == '_' || ch == '`' || ch == '|' || ch == '~' || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ||
96-
(ch >= '0' && ch <= '9');
95+
ch == '^' || ch == '_' || ch == '`' || ch == '|' || ch == '~' || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ||
96+
(ch >= '0' && ch <= '9');
9797
}
9898

9999
/**

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package io.fusionauth.http;
1717

18+
import java.io.ByteArrayInputStream;
19+
import java.io.ByteArrayOutputStream;
1820
import java.io.IOException;
1921
import java.io.InputStream;
2022
import java.io.OutputStream;
@@ -51,6 +53,10 @@
5153
import java.util.Map;
5254
import java.util.UUID;
5355
import java.util.stream.Collectors;
56+
import java.util.zip.DeflaterOutputStream;
57+
import java.util.zip.GZIPInputStream;
58+
import java.util.zip.GZIPOutputStream;
59+
import java.util.zip.InflaterInputStream;
5460

5561
import io.fusionauth.http.HTTPValues.Connections;
5662
import io.fusionauth.http.log.FileLogger;
@@ -407,6 +413,33 @@ protected void assertHTTPResponseEquals(Socket socket, String expectedResponse)
407413
}
408414
}
409415

416+
protected byte[] deflate(byte[] bytes) throws Exception {
417+
ByteArrayOutputStream baseOutputStream = new ByteArrayOutputStream();
418+
try (DeflaterOutputStream out = new DeflaterOutputStream(baseOutputStream, true)) {
419+
out.write(bytes);
420+
out.flush();
421+
out.finish();
422+
return baseOutputStream.toByteArray();
423+
}
424+
}
425+
426+
protected byte[] gzip(byte[] bytes) throws Exception {
427+
ByteArrayOutputStream baseOutputStream = new ByteArrayOutputStream();
428+
try (DeflaterOutputStream out = new GZIPOutputStream(baseOutputStream, true)) {
429+
out.write(bytes);
430+
out.flush();
431+
out.finish();
432+
return baseOutputStream.toByteArray();
433+
}
434+
}
435+
436+
protected byte[] inflate(byte[] bytes) throws Exception {
437+
ByteArrayInputStream baseInputStream = new ByteArrayInputStream(bytes);
438+
try (InflaterInputStream in = new InflaterInputStream(baseInputStream)) {
439+
return in.readAllBytes();
440+
}
441+
}
442+
410443
protected void printf(String format, Object... args) {
411444
if (verbose) {
412445
System.out.printf(SystemOutPrefix + format, args);
@@ -426,6 +459,13 @@ protected void sleep(long millis) {
426459
}
427460
}
428461

462+
protected byte[] ungzip(byte[] bytes) throws Exception {
463+
ByteArrayInputStream baseInputStream = new ByteArrayInputStream(bytes);
464+
try (InflaterInputStream in = new GZIPInputStream(baseInputStream)) {
465+
return in.readAllBytes();
466+
}
467+
}
468+
429469
/**
430470
* Verifies that the chain certificates can be validated up to the supplied root certificate. See
431471
* {@link CertPathValidator#validate(CertPath, CertPathParameters)} for details.

0 commit comments

Comments
 (0)