mirror of
https://github.com/yushijinhun/authlib-injector.git
synced 2024-11-15 06:11:09 +01:00
[nanohttpd]support chunked encoding&100-continue
This commit is contained in:
parent
f31e9f53dd
commit
bdd5f4bfcb
6 changed files with 424 additions and 10 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ public interface IHTTPSession {
|
|||
|
||||
Map<String, String> getHeaders();
|
||||
|
||||
InputStream getInputStream();
|
||||
InputStream getInputStream() throws IOException;
|
||||
|
||||
String getMethod();
|
||||
|
||||
|
|
|
@ -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())
|
||||
r = serve(this);
|
||||
// TODO: this.inputStream.skip(body_size -
|
||||
// (this.inputStream.totalRead() - pos_before_serve))
|
||||
} 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);
|
||||
} 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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue