Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
## FusionAuth HTTP client and server ![semver 2.0.0 compliant](http://img.shields.io/badge/semver-2.0.0-brightgreen.svg?style=flat-square) [![test](https://github.com/FusionAuth/java-http/actions/workflows/test.yml/badge.svg)](https://github.com/FusionAuth/java-http/actions/workflows/test.yml)
## Java HTTP client and server ![semver 2.0.0 compliant](http://img.shields.io/badge/semver-2.0.0-brightgreen.svg?style=flat-square) [![test](https://github.com/FusionAuth/java-http/actions/workflows/test.yml/badge.svg)](https://github.com/FusionAuth/java-http/actions/workflows/test.yml)

### Latest versions

* Latest stable version: `1.3.1`
* Latest stable version: `1.4.0`
* Now with 100% more virtual threads!
* Prior stable version `0.3.7`

Expand All @@ -27,20 +27,20 @@ To add this library to your project, you can include this dependency in your Mav
<dependency>
<groupId>io.fusionauth</groupId>
<artifactId>java-http</artifactId>
<version>1.3.1</version>
<version>1.4.0</version>
</dependency>
```

If you are using Gradle, you can add this to your build file:

```groovy
implementation 'io.fusionauth:java-http:1.3.1'
implementation 'io.fusionauth:java-http:1.4.0'
```

If you are using Savant, you can add this to your build file:

```groovy
dependency(id: "io.fusionauth:java-http:1.3.1")
dependency(id: "io.fusionauth:java-http:1.4.0")
```

## Examples Usages:
Expand Down Expand Up @@ -231,14 +231,13 @@ The general requirements and roadmap are as follows:
### Server tasks

* [x] Basic HTTP 1.1
* [x] Support Accept-Encoding (gzip, deflate), by default and per response options.
* [x] Support Content-Encoding (gzip, deflate)
* [x] Support Keep-Alive
* [x] Support Expect-Continue 100
* [x] Support chunked request
* [x] Support chunked response
* [x] Support streaming entity bodies (via chunking likely)
* [x] Support compression (default and per response options)
* [x] Support Transfer-Encoding: chunked on request for streaming.
* [x] Support Transfer-Encoding: chunked on response
* [x] Support cookies in request and response
* [x] Clean up HTTPRequest
* [x] Support form data
* [x] Support multipart form data
* [x] Support TLS
Expand Down
2 changes: 1 addition & 1 deletion build.savant
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ restifyVersion = "4.2.1"
slf4jVersion = "2.0.17"
testngVersion = "7.11.0"

project(group: "io.fusionauth", name: "java-http", version: "1.3.1", licenses: ["ApacheV2_0"]) {
project(group: "io.fusionauth", name: "java-http", version: "1.4.0", licenses: ["ApacheV2_0"]) {
workflow {
fetch {
// Dependency resolution order:
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>io.fusionauth</groupId>
<artifactId>java-http</artifactId>
<version>1.3.1</version>
<version>1.4.0</version>
<packaging>jar</packaging>

<name>Java HTTP library (client and server)</name>
Expand Down Expand Up @@ -200,4 +200,4 @@
</build>
</profile>
</profiles>
</project>
</project>
4 changes: 4 additions & 0 deletions src/main/java/io/fusionauth/http/HTTPValues.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public static final class ContentEncodings {

public static final String Gzip = "gzip";

public static final String XGzip = "x-gzip";

private ContentEncodings() {
}
}
Expand Down Expand Up @@ -216,6 +218,8 @@ public static final class Headers {

public static final String ContentEncoding = "Content-Encoding";

public static final String ContentEncodingLower = "content-encoding";

public static final String ContentLength = "Content-Length";

public static final String ContentLengthLower = "content-length";
Expand Down
68 changes: 68 additions & 0 deletions src/main/java/io/fusionauth/http/io/FixedLengthInputStream.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific
* language governing permissions and limitations under the License.
*/
package io.fusionauth.http.io;

import java.io.IOException;
import java.io.InputStream;

/**
* A filter InputStream that reads a fixed length body.
*
* @author Daniel DeGroff
*/
public class FixedLengthInputStream extends InputStream {
private final byte[] b1 = new byte[1];

private final PushbackInputStream delegate;

private long bytesRemaining;

public FixedLengthInputStream(PushbackInputStream delegate, long contentLength) {
this.delegate = delegate;
this.bytesRemaining = contentLength;
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
if (bytesRemaining <= 0) {
return -1;
}

int read = delegate.read(b, off, len);
int reportBytesRead = read;
if (read > 0) {
int extraBytes = (int) (read - bytesRemaining);
if (extraBytes > 0) {
reportBytesRead -= extraBytes;
delegate.push(b, (int) bytesRemaining, extraBytes);
}

bytesRemaining -= reportBytesRead;
}

return reportBytesRead;
}

@Override
public int read() throws IOException {
var read = read(b1);
if (read <= 0) {
return read;
}

return b1[0] & 0xFF;
}
}
9 changes: 9 additions & 0 deletions src/main/java/io/fusionauth/http/server/Configurable.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ default T withChunkedBufferSize(int chunkedBufferSize) {
/**
* Sets the default compression behavior for the HTTP response. This behavior can be optionally set per response. See
* {@link HTTPResponse#setCompress(boolean)}. Defaults to true.
* <p>
* Set this configuration to <code>true</code> if you want to compress the response when the Accept-Encoding header is present. Set this
* configuration to <code>false</code> if you want to require the request handler to use {@link HTTPResponse#setCompress(boolean)} in
* order to compress the response.
* <p>
* Regardless of this configuration, you always have the option to use {@link HTTPResponse#setCompress(boolean)} on a per-response basis
* as an override.
* <p>
* When the request does not contain an Accept-Encoding the response will not be compressed regardless of this configuration.
*
* @param compressByDefault true if you want to compress by default, or false to not compress by default.
* @return This.
Expand Down
47 changes: 45 additions & 2 deletions src/main/java/io/fusionauth/http/server/HTTPRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import io.fusionauth.http.FileInfo;
import io.fusionauth.http.HTTPMethod;
import io.fusionauth.http.HTTPValues.Connections;
import io.fusionauth.http.HTTPValues.ContentEncodings;
import io.fusionauth.http.HTTPValues.ContentTypes;
import io.fusionauth.http.HTTPValues.Headers;
import io.fusionauth.http.HTTPValues.Protocols;
Expand All @@ -68,6 +69,8 @@ public class HTTPRequest implements Buildable<HTTPRequest> {

private final Map<String, Object> attributes = new HashMap<>();

private final List<String> contentEncodings = new LinkedList<>();

private final Map<String, Cookie> cookies = new HashMap<>();

private final List<FileInfo> files = new LinkedList<>();
Expand Down Expand Up @@ -147,6 +150,14 @@ public void addAcceptEncodings(List<String> encodings) {
this.acceptEncodings.addAll(encodings);
}

public void addContentEncoding(String encoding) {
this.contentEncodings.add(encoding);
}

public void addContentEncodings(List<String> encodings) {
this.contentEncodings.addAll(encodings);
}

public void addCookies(Cookie... cookies) {
for (Cookie cookie : cookies) {
this.cookies.put(cookie.name, cookie);
Expand Down Expand Up @@ -296,6 +307,15 @@ public void setCharacterEncoding(Charset encoding) {
this.encoding = encoding;
}

public List<String> getContentEncodings() {
return contentEncodings;
}

public void setContentEncodings(List<String> encodings) {
this.contentEncodings.clear();
this.contentEncodings.addAll(encodings);
}

public Long getContentLength() {
return contentLength;
}
Expand Down Expand Up @@ -606,12 +626,16 @@ public void setURLParameters(Map<String, List<String>> parameters) {
* {@code Content-Length} header was provided.
*/
public boolean hasBody() {
if (isChunked()) {
return true;
}

Long contentLength = getContentLength();
return isChunked() || (contentLength != null && contentLength > 0);
return contentLength != null && contentLength > 0;
}

public boolean isChunked() {
return getTransferEncoding() != null && getTransferEncoding().equalsIgnoreCase(TransferEncodings.Chunked);
return TransferEncodings.Chunked.equalsIgnoreCase(getTransferEncoding());
}

/**
Expand Down Expand Up @@ -756,6 +780,25 @@ private void decodeHeader(String name, String value) {
// Ignore the exception and keep the value null
}
break;
case Headers.ContentEncodingLower:
String[] encodings = value.split(",");
List<String> contentEncodings = new ArrayList<>(1);
for (String encoding : encodings) {
encoding = encoding.trim();
if (encoding.isEmpty()) {
continue;
}

// The HTTP/1.1 standard recommends that the servers supporting gzip also recognize x-gzip as an alias for compatibility.
if (encoding.equalsIgnoreCase(ContentEncodings.XGzip)) {
encoding = ContentEncodings.Gzip;
}

contentEncodings.add(encoding);
}

setContentEncodings(contentEncodings);
break;
case Headers.ContentTypeLower:
this.encoding = null;
this.multipart = false;
Expand Down
18 changes: 17 additions & 1 deletion src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.fusionauth.http.HTTPProcessingException;
import io.fusionauth.http.HTTPValues;
import io.fusionauth.http.HTTPValues.Connections;
import io.fusionauth.http.HTTPValues.ContentEncodings;
import io.fusionauth.http.HTTPValues.Headers;
import io.fusionauth.http.HTTPValues.Protocols;
import io.fusionauth.http.ParseException;
Expand Down Expand Up @@ -417,7 +418,6 @@ private Integer validatePreamble(HTTPRequest request) {
// However, as long as we ignore Content-Length we should be ok. Earlier specs indicate Transfer-Encoding should take precedence,
// later specs imply it is an error. Seems ok to allow it and just ignore it.
if (request.getHeader(Headers.TransferEncoding) == null) {
var contentLength = request.getContentLength();
var requestedContentLengthHeaders = request.getHeaders(Headers.ContentLength);
if (requestedContentLengthHeaders != null) {
if (requestedContentLengthHeaders.size() != 1) {
Expand All @@ -429,6 +429,7 @@ private Integer validatePreamble(HTTPRequest request) {
return Status.BadRequest;
}

var contentLength = request.getContentLength();
if (contentLength == null || contentLength < 0) {
if (debugEnabled) {
logger.debug("Invalid request. The Content-Length must be >= 0 and <= 9,223,372,036,854,775,807. [{}]", requestedContentLengthHeaders.getFirst());
Expand All @@ -444,6 +445,19 @@ private Integer validatePreamble(HTTPRequest request) {
request.removeHeader(Headers.ContentLength);
}

// Validate Content-Encoding, we currently support deflate and gzip.
// - If we see anything else we should fail, we will be unable to handle the request.
var contentEncodings = request.getContentEncodings();
for (var encoding : contentEncodings) {
if (!encoding.equalsIgnoreCase(ContentEncodings.Gzip) && !encoding.equalsIgnoreCase(ContentEncodings.Deflate)) {
// Note that while we do not expect multiple Content-Encoding headers, the last one will be used. For good measure,
// use the last one in the debug message as well.
var contentEncodingHeader = request.getHeaders(Headers.ContentEncoding).getLast();
logger.debug("Invalid request. The Content-Type header contains an un-supported value. [{}]", contentEncodingHeader);
return Status.UnsupportedMediaType;
}
}

return null;
}

Expand All @@ -465,5 +479,7 @@ private static class Status {
public static final int HTTPVersionNotSupported = 505;

public static final int InternalServerError = 500;

public static final int UnsupportedMediaType = 415;
}
}
Loading