[nanohttpd]support chunked encoding&100-continue

This commit is contained in:
yushijinhun 2018-12-30 21:47:02 +08:00
parent f31e9f53dd
commit bdd5f4bfcb
No known key found for this signature in database
GPG key ID: 5BC167F73EA558E4
6 changed files with 424 additions and 10 deletions

View file

@ -0,0 +1,111 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
/**
* @author yushijinhun
*/
class ChunkedInputStream extends InputStream {
private final InputStream in;
// 0 = end of chunk, \r\n hasn't been read
// -1 = begin of chunk
// -2 = closed
// other values = bytes remaining in current chunk
private int currentRemaining = -1;
public ChunkedInputStream(InputStream in) {
this.in = in;
}
@Override
public synchronized int read() throws IOException {
if (currentRemaining == -2) {
return -1;
}
if (currentRemaining == 0) {
readCRLF();
currentRemaining = -1;
}
if (currentRemaining == -1) {
currentRemaining = readChunkLength();
if (currentRemaining == 0) {
readCRLF();
currentRemaining = -2;
return -1;
}
}
int result = in.read();
currentRemaining--;
if (result == -1) {
throw new EOFException();
}
return result;
}
private int readChunkLength() throws IOException {
int length = 0;
int b;
for (;;) {
b = in.read();
if (b == -1) {
throw new EOFException();
}
if (b == '\r') {
b = in.read();
if (b == -1) {
throw new EOFException();
} else if (b == '\n') {
return length;
} else {
throw new IOException("LF is expected, read: " + b);
}
}
int digit = hexDigit(b);
if (digit == -1) {
throw new IOException("Hex digit is expected, read: " + b);
}
if ((length & 0xf8000000) != 0) { // highest 5 bits must be zero
throw new IOException("Chunk is too long");
}
length <<= 4;
length += digit;
}
}
private void readCRLF() throws IOException {
int b1 = in.read();
int b2 = in.read();
if (b1 == '\r' && b2 == '\n') {
return;
}
if (b1 == -1 || b2 == -1) {
throw new EOFException();
}
throw new IOException("CRLF is expected, read: " + b1 + " " + b2);
}
private static int hexDigit(int ch) {
if (ch >= '0' && ch <= '9') {
return ch - '0';
} else if (ch >= 'a' && ch <= 'f') {
return ch - 'a' + 10;
} else if (ch >= 'A' && ch <= 'F') {
return ch - 'A' + 10;
} else {
return -1;
}
}
@Override
public synchronized int available() throws IOException {
if (currentRemaining > 0) {
return Math.min(currentRemaining, in.available());
} else {
return 0;
}
}
}

View file

@ -0,0 +1,38 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
/**
* @author yushijinhun
*/
class FixedLengthInputStream extends InputStream {
private final InputStream in;
private long remaining = 0;
public FixedLengthInputStream(InputStream in, long length) {
this.remaining = length;
this.in = in;
}
@Override
public synchronized int read() throws IOException {
if (remaining > 0) {
int result = in.read();
if (result == -1) {
throw new EOFException();
}
remaining--;
return result;
} else {
return -1;
}
}
@Override
public synchronized int available() throws IOException {
return Math.min(in.available(), (int) remaining);
}
}

View file

@ -15,7 +15,7 @@ public interface IHTTPSession {
Map<String, String> getHeaders();
InputStream getInputStream();
InputStream getInputStream() throws IOException;
String getMethod();

View file

@ -1,5 +1,7 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import static java.nio.charset.StandardCharsets.US_ASCII;
/*
* #%L
* NanoHttpd-Core
@ -200,6 +202,8 @@ public abstract class NanoHTTPD {
private final BufferedInputStream inputStream;
private InputStream parsedInputStream;
private int splitbyte;
private int rlen;
@ -220,6 +224,11 @@ public abstract class NanoHTTPD {
private String protocolVersion;
private boolean expect100Continue;
private boolean continueSent;
private boolean isServing;
private final Object servingLock = new Object();
public HTTPSession(InputStream inputStream, OutputStream outputStream) {
this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE);
this.outputStream = outputStream;
@ -392,14 +401,52 @@ public abstract class NanoHTTPD {
String connection = this.headers.get("connection");
boolean keepAlive = "HTTP/1.1".equals(protocolVersion) && (connection == null || !connection.matches("(?i).*close.*"));
// Ok, now do the serve()
String transferEncoding = this.headers.get("transfer-encoding");
String contentLengthStr = this.headers.get("content-length");
if (transferEncoding != null && contentLengthStr == null) {
if ("chunked".equals(transferEncoding)) {
parsedInputStream = new ChunkedInputStream(inputStream);
} else {
throw new ResponseException(Status.NOT_IMPLEMENTED, "Unsupported Transfer-Encoding");
}
// TODO: long body_size = getBodySize();
// TODO: long pos_before_serve = this.inputStream.totalRead()
// (requires implementation for totalRead())
} else if (transferEncoding == null && contentLengthStr != null) {
int contentLength = -1;
try {
contentLength = Integer.parseInt(contentLengthStr);
} catch (NumberFormatException e) {
}
if (contentLength < 0) {
throw new ResponseException(Status.BAD_REQUEST, "The request has an invalid Content-Length header.");
}
parsedInputStream = new FixedLengthInputStream(inputStream, contentLength);
} else if (transferEncoding != null && contentLengthStr != null) {
throw new ResponseException(Status.BAD_REQUEST, "Content-Length and Transfer-Encoding cannot exist at the same time.");
} else /* if both are null */ {
// no request payload
}
expect100Continue = "HTTP/1.1".equals(protocolVersion)
&& "100-continue".equals(this.headers.get("expect"))
&& parsedInputStream != null;
// Ok, now do the serve()
this.isServing = true;
try {
r = serve(this);
// TODO: this.inputStream.skip(body_size -
// (this.inputStream.totalRead() - pos_before_serve))
} finally {
synchronized (servingLock) {
this.isServing = false;
}
}
if (!(parsedInputStream == null || (expect100Continue && !continueSent))) {
// consume the input
while (parsedInputStream.read() != -1)
;
}
if (r == null) {
throw new ResponseException(Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
@ -460,8 +507,17 @@ public abstract class NanoHTTPD {
}
@Override
public final InputStream getInputStream() {
return this.inputStream;
public final InputStream getInputStream() throws IOException {
synchronized (servingLock) {
if (!isServing) {
throw new IllegalStateException();
}
if (expect100Continue && !continueSent) {
continueSent = true;
this.outputStream.write("HTTP/1.1 100 Continue\r\n\r\n".getBytes(US_ASCII));
}
}
return this.parsedInputStream;
}
@Override

View file

@ -0,0 +1,158 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static moe.yushi.authlibinjector.util.IOUtils.asBytes;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import org.junit.Test;
@SuppressWarnings("resource")
public class ChunkedInputStreamTest {
@Test
public void testRead1() throws IOException {
byte[] data = ("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
assertArrayEquals(("Wikipedia in\r\n\r\nchunks.").getBytes(US_ASCII), asBytes(in));
assertEquals(underlying.read(), -1);
}
@Test
public void testRead2() throws IOException {
byte[] data = ("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n.").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
assertArrayEquals(("Wikipedia in\r\n\r\nchunks.").getBytes(US_ASCII), asBytes(in));
assertEquals(underlying.read(), '.');
}
@Test
public void testRead3() throws IOException {
byte[] data = ("25\r\nThis is the data in the first chunk\r\n\r\n1c\r\nand this is the second one\r\n\r\n3\r\ncon\r\n8\r\nsequence\r\n0\r\n\r\n").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
assertArrayEquals(("This is the data in the first chunk\r\nand this is the second one\r\nconsequence").getBytes(US_ASCII), asBytes(in));
assertEquals(underlying.read(), -1);
}
@Test
public void testRead4() throws IOException {
byte[] data = ("25\r\nThis is the data in the first chunk\r\n\r\n1C\r\nand this is the second one\r\n\r\n3\r\ncon\r\n8\r\nsequence\r\n0\r\n\r\n.").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
assertArrayEquals(("This is the data in the first chunk\r\nand this is the second one\r\nconsequence").getBytes(US_ASCII), asBytes(in));
assertEquals(underlying.read(), '.');
}
@Test
public void testRead5() throws IOException {
byte[] data = ("0\r\n\r\n").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
assertArrayEquals(new byte[0], asBytes(in));
assertEquals(underlying.read(), -1);
}
@Test(expected = EOFException.class)
public void testReadEOF1() throws IOException {
byte[] data = ("a").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = EOFException.class)
public void testReadEOF2() throws IOException {
byte[] data = ("a\r").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = EOFException.class)
public void testReadEOF3() throws IOException {
byte[] data = ("a\r\n").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = EOFException.class)
public void testReadEOF4() throws IOException {
byte[] data = ("a\r\nabc").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = EOFException.class)
public void testReadEOF5() throws IOException {
byte[] data = ("a\r\n123456789a\r").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = EOFException.class)
public void testReadEOF6() throws IOException {
byte[] data = ("a\r\n123456789a\r\n").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = EOFException.class)
public void testReadEOF7() throws IOException {
byte[] data = ("a\r\n123456789a\r\n0\r\n\r").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = IOException.class)
public void testBadIn1() throws IOException {
byte[] data = ("-1").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = IOException.class)
public void testBadIn2() throws IOException {
byte[] data = ("a\ra").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = IOException.class)
public void testBadIn3() throws IOException {
byte[] data = ("a\r\n123456789aa").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = IOException.class)
public void testBadIn4() throws IOException {
byte[] data = ("a\r\n123456789a\ra").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = IOException.class)
public void testBadIn5() throws IOException {
byte[] data = ("a\r\n123456789a\r\n0\r\n\r-").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
}

View file

@ -0,0 +1,51 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import static moe.yushi.authlibinjector.util.IOUtils.asBytes;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import org.junit.Test;
@SuppressWarnings("resource")
public class FixedLengthInputStreamTest {
@Test
public void testRead1() throws IOException {
byte[] data = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55 };
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new FixedLengthInputStream(underlying, 5);
assertArrayEquals(data, asBytes(in));
assertEquals(underlying.read(), -1);
}
@Test
public void testRead2() throws IOException {
byte[] data = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55 };
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new FixedLengthInputStream(underlying, 4);
assertArrayEquals(Arrays.copyOf(data, 4), asBytes(in));
assertEquals(underlying.read(), 0x55);
}
@Test
public void testRead3() throws IOException {
byte[] data = new byte[] { 0x11 };
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new FixedLengthInputStream(underlying, 0);
assertArrayEquals(new byte[0], asBytes(in));
assertEquals(underlying.read(), 0x11);
}
@Test(expected = EOFException.class)
public void testReadEOF() throws IOException {
byte[] data = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55 };
InputStream in = new FixedLengthInputStream(new ByteArrayInputStream(data), 6);
asBytes(in);
}
}