Skip to content

Commit

Permalink
New option to allow HTTP message body to end abruptly
Browse files Browse the repository at this point in the history
before its expected length.
Fix #65.
  • Loading branch information
renatoathaydes committed Aug 22, 2023
1 parent b3488d1 commit 97d94ef
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 28 deletions.
2 changes: 1 addition & 1 deletion rawhttp-core/src/main/java/rawhttp/core/RawHttp.java
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ public FramedBody getFramedBody(StartLine startLine, RawHttpHeaders headers) {
} catch (NumberFormatException e) {
throw new InvalidMessageFrame("Content-Length header value is not a valid number");
}
return new FramedBody.ContentLength(bodyDecoder, bodyLength);
return new FramedBody.ContentLength(bodyDecoder, bodyLength, options.allowContentLengthMismatch());
}

/**
Expand Down
33 changes: 31 additions & 2 deletions rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class RawHttpOptions {
private final boolean ignoreLeadingEmptyLine;
private final boolean allowIllegalStartLineCharacters;
private final boolean allowComments;
private final boolean allowContentLengthMismatch;
private final HttpHeadersOptions httpHeadersOptions;
private final HttpBodyEncodingRegistry encodingRegistry;

Expand All @@ -34,6 +35,7 @@ private RawHttpOptions(boolean insertHostHeaderIfMissing,
boolean ignoreLeadingEmptyLine,
boolean allowIllegalStartLineCharacters,
boolean allowComments,
boolean allowContentLengthMismatch,
HttpHeadersOptions httpHeadersOptions,
HttpBodyEncodingRegistry encodingRegistry) {
this.insertHostHeaderIfMissing = insertHostHeaderIfMissing;
Expand All @@ -42,6 +44,7 @@ private RawHttpOptions(boolean insertHostHeaderIfMissing,
this.ignoreLeadingEmptyLine = ignoreLeadingEmptyLine;
this.allowIllegalStartLineCharacters = allowIllegalStartLineCharacters;
this.allowComments = allowComments;
this.allowContentLengthMismatch = allowContentLengthMismatch;
this.httpHeadersOptions = httpHeadersOptions;
this.encodingRegistry = encodingRegistry;
}
Expand Down Expand Up @@ -100,6 +103,15 @@ public boolean ignoreLeadingEmptyLine() {
return ignoreLeadingEmptyLine;
}

/**
* @return whether to allow the content-length header to not match exactly a HTTP
* message's body length.
* @see Builder#allowContentLengthMismatch()
*/
public boolean allowContentLengthMismatch() {
return allowContentLengthMismatch;
}

/**
* @return whether or not to allow illegal characters in the start line.
*/
Expand Down Expand Up @@ -178,7 +190,7 @@ public Consumer<RawHttpHeaders> getHeadersValidator() {

/**
* @return the encoding that should be used to interpret HTTP headers's values.
*
* <p>
* If not set, defaults to ISO-8859-1 according to note at https://tools.ietf.org/html/rfc7230#section-3.2.4.
*/
public Charset getHeaderValuesCharset() {
Expand All @@ -197,6 +209,7 @@ public static final class Builder {
private boolean insertHttpVersionIfMissing = true;
private boolean allowIllegalStartLineCharacters = false;
private boolean allowComments = false;
private boolean allowContentLengthMismatch = false;
private HttpHeadersOptionsBuilder httpHeadersOptionsBuilder = new HttpHeadersOptionsBuilder();
private HttpBodyEncodingRegistry encodingRegistry;

Expand Down Expand Up @@ -299,6 +312,22 @@ public Builder allowComments() {
return this;
}

/**
* Allow a HTTP message's content-length header to not match exactly the
* message body (both requests and responses).
* <p>
* This may happen, for example, when a server miscalculates the response length
* and sends less data than expected. In such case, by setting this option,
* instead of an Exception, RawHTTP will return the bytes that have been read
* as if it were a full message body.
*
* @return this
*/
public Builder allowContentLengthMismatch() {
this.allowContentLengthMismatch = true;
return this;
}

/**
* Get a builder of {@link HttpHeadersOptions} to use with this object.
*
Expand Down Expand Up @@ -333,7 +362,7 @@ public RawHttpOptions build() {

return new RawHttpOptions(insertHostHeaderIfMissing, insertHttpVersionIfMissing,
allowNewLineWithoutReturn, ignoreLeadingEmptyLine, allowIllegalStartLineCharacters, allowComments,
httpHeadersOptionsBuilder.getOptions(), registry);
allowContentLengthMismatch, httpHeadersOptionsBuilder.getOptions(), registry);
}

public class HttpHeadersOptionsBuilder {
Expand Down
14 changes: 12 additions & 2 deletions rawhttp-core/src/main/java/rawhttp/core/body/BodyConsumer.java
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,15 @@ public Iterator<ChunkedBodyContents.Chunk> consumeLazily(InputStream inputStream
public static final class ContentLengthBodyConsumer extends BodyConsumer {

private final long bodyLength;
private final boolean allowContentLengthMismatch;

ContentLengthBodyConsumer(long bodyLength) {
this(bodyLength, false);
}

ContentLengthBodyConsumer(long bodyLength, boolean allowContentLengthMismatch) {
this.bodyLength = bodyLength;
this.allowContentLengthMismatch = allowContentLengthMismatch;
}

@Override
Expand All @@ -143,7 +149,7 @@ public void consumeDataInto(InputStream inputStream, OutputStream outputStream,
consumeInto(inputStream, outputStream, bufferSize);
}

private static void readAndWriteBytesUpToLength(InputStream inputStream,
private void readAndWriteBytesUpToLength(InputStream inputStream,
long bodyLength,
OutputStream outputStream,
int bufferSize) throws IOException {
Expand All @@ -156,7 +162,11 @@ private static void readAndWriteBytesUpToLength(InputStream inputStream,
int bytesToRead = (int) Math.min(bytes.length, bodyLength - offset);
int actuallyRead = inputStream.read(bytes, 0, bytesToRead);
if (actuallyRead < 0) {
throw new IOException("InputStream provided " + offset + ", but " + bodyLength + " were expected");
if (!allowContentLengthMismatch) {
throw new IOException("InputStream provided " + offset + " byte(s), but " + bodyLength + " were expected");
}
// pretend that the body has been fully read
break;
} else {
outputStream.write(bytes, 0, actuallyRead);
}
Expand Down
32 changes: 31 additions & 1 deletion rawhttp-core/src/main/java/rawhttp/core/body/FramedBody.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public final <T> T use(IOFunction<ContentLength, T> useContentLength,
public static final class ContentLength extends FramedBody {

private final long bodyLength;
private final boolean allowContentLengthMismatch;

/**
* Create a new instance of the {@link ContentLength} framed body.
Expand All @@ -98,24 +99,52 @@ public ContentLength(long bodyLength) {
this(new BodyDecoder(), bodyLength);
}

/**
* Create a new instance of the {@link ContentLength} framed body.
*
* @param bodyLength the length of the HTTP message body
* @param allowContentLengthMismatch allow the content-length header to not match exactly a HTTP
* message's body length.
*/
public ContentLength(long bodyLength, boolean allowContentLengthMismatch) {
this(new BodyDecoder(), bodyLength, allowContentLengthMismatch);
}

/**
* Create a new instance of the {@link ContentLength} framed body.
*
* @param bodyDecoder the body encoding
* @param bodyLength the length of the HTTP message body
*/
public ContentLength(BodyDecoder bodyDecoder, long bodyLength) {
this(bodyDecoder, bodyLength, false);
}

/**
* Create a new instance of the {@link ContentLength} framed body.
*
* @param bodyDecoder the body encoding
* @param bodyLength the length of the HTTP message body
* @param allowContentLengthMismatch allow the content-length header to not match exactly a HTTP
* message's body length.
*/
public ContentLength(BodyDecoder bodyDecoder, long bodyLength, boolean allowContentLengthMismatch) {
super(bodyDecoder);
this.bodyLength = bodyLength;
this.allowContentLengthMismatch = allowContentLengthMismatch;
}

public long getBodyLength() {
return bodyLength;
}

public boolean isAllowContentLengthMismatch() {
return allowContentLengthMismatch;
}

@Override
protected BodyConsumer getBodyConsumer() {
return new BodyConsumer.ContentLengthBodyConsumer(bodyLength);
return new BodyConsumer.ContentLengthBodyConsumer(bodyLength, allowContentLengthMismatch);
}

@Override
Expand All @@ -136,6 +165,7 @@ public String toString() {
return "ContentLength{" +
"value=" + bodyLength +
", encodings=" + getEncodings() +
", allowContentLengthMismatch=" + allowContentLengthMismatch +
'}';
}
}
Expand Down
30 changes: 30 additions & 0 deletions rawhttp-core/src/test/kotlin/rawhttp/core/RawHttpTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package rawhttp.core

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.optional.bePresent
Expand All @@ -9,6 +10,7 @@ import io.kotest.matchers.shouldNot
import io.kotest.matchers.string.shouldContain
import org.junit.jupiter.api.Test
import java.io.File
import java.io.IOException
import java.net.URI
import java.nio.charset.StandardCharsets.UTF_8

Expand Down Expand Up @@ -204,6 +206,34 @@ class SimpleHttpRequestTests {
}
}

@Test
fun `May allow request to have shorter body than expected`() {
val defaultHttp = RawHttp()
val lenientHttp = RawHttp(
RawHttpOptions.newBuilder()
.allowContentLengthMismatch()
.build()
)

val requestWithTooShortBody = "POST /foo HTTP/1.1\n" +
"Host: example.org\n" +
"Content-Length: 10\n\n" +
"short"

val defaultRequest = defaultHttp.parseRequest(requestWithTooShortBody)
val lenientRequest = lenientHttp.parseRequest(requestWithTooShortBody)

// by default, an Exception is thrown
defaultRequest.body shouldBePresent {
shouldThrow<IOException> { decodeBodyToString(UTF_8) }
}

// but the lenient RawHTTP allows mismatches
lenientRequest.body.shouldBePresent {
decodeBodyToString(UTF_8) shouldBe "short"
}
}

}

class SimpleHttpResponseTests {
Expand Down
Loading

0 comments on commit 97d94ef

Please sign in to comment.