Files
Flash5/flash/src/main/java/dev/relism/RequestParser.java
T

181 lines
7.8 KiB
Java

package dev.relism;
import dev.relism.http.HttpMethod;
import dev.relism.models.HeaderMap;
import dev.relism.models.Request;
import dev.relism.models.RequestLine;
import dev.relism.routing.routers.fastpathrouter.FastPathViews;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.util.Arrays;
/**
* One instance per connection, the buffer is allocated once and reused across keep-alive
* requests. Grows on demand (doubling, up to {@code maxHeaderBufferSize}). Zero String
* allocations during parsing; paths, headers and protocol are exposed as {@link dev.relism.fpr.core.ByteView} slices.
*/
@Slf4j
public class RequestParser {
private static final int INITIAL_BUFFER_SIZE = 8192;
private final int maxHeaderBufferSize;
private final InetSocketAddress remoteAddress; // set once per connection, never changes
private final HeaderMap headerMap = new HeaderMap();
private byte[] buffer;
private int bufBase = 0; // absolute start of valid data in buffer
private int bufLen = 0; // number of valid bytes from bufBase
public RequestParser() { this(64 * 1024, null); }
public RequestParser(int maxHeaderBufferSize) { this(maxHeaderBufferSize, null); }
public RequestParser(int maxHeaderBufferSize, InetSocketAddress remoteAddress) {
this.maxHeaderBufferSize = maxHeaderBufferSize;
this.remoteAddress = remoteAddress;
this.buffer = new byte[Math.min(INITIAL_BUFFER_SIZE, maxHeaderBufferSize)];
}
public Request parse(InputStream in) throws IOException {
// Take ownership of any leftover bytes from the previous request, then reset so
// early-returns leave the fields in a clean state.
int base = bufBase;
int totalRead = bufLen;
bufBase = 0;
bufLen = 0;
int headerEndIdx = totalRead > 0 ? findEndOfHeader(buffer, base, base + totalRead) : -1;
while (headerEndIdx == -1) {
if (base + totalRead == buffer.length) {
if (base > 0) {
// Compact: slide valid data to position 0 — rare path (~every N requests
// where N = bufferSize / avgRequestSize rather than every request).
System.arraycopy(buffer, base, buffer, 0, totalRead);
base = 0;
} else if (buffer.length >= maxHeaderBufferSize) {
throw new IOException("Request headers exceed " + maxHeaderBufferSize + " bytes");
} else {
buffer = Arrays.copyOf(buffer, Math.min(buffer.length * 2, maxHeaderBufferSize));
}
}
int n = in.read(buffer, base + totalRead, buffer.length - base - totalRead);
if (n <= 0) break;
int prevTotal = totalRead;
totalRead += n;
headerEndIdx = findEndOfHeader(buffer, base + Math.max(0, prevTotal - 3), base + totalRead);
}
if (totalRead <= 0) return null;
if (headerEndIdx == -1) {
throw new IOException("Request headers exceed " + maxHeaderBufferSize + " bytes");
}
int methodEnd = find(buffer, base, headerEndIdx, (byte) ' ');
if (methodEnd == -1) throw new IOException("Invalid request line (method)");
HttpMethod method = HttpMethod.fromBytes(buffer, base, methodEnd - base);
if (method == null) throw new IOException("Unsupported HTTP method");
int pathStart = methodEnd + 1;
int pathEnd = find(buffer, pathStart, headerEndIdx, (byte) ' ');
if (pathEnd == -1) throw new IOException("Invalid request line (path)");
int queryMark = find(buffer, pathStart, pathEnd, (byte) '?');
FastPathViews.RequestByteView pathView = new FastPathViews.RequestByteView(buffer, pathStart,
queryMark != -1 ? queryMark - pathStart : pathEnd - pathStart);
FastPathViews.RequestByteView queryView = queryMark != -1
? new FastPathViews.RequestByteView(buffer, queryMark + 1, pathEnd - queryMark - 1)
: null;
int protocolStart = pathEnd + 1;
int protocolEnd = find(buffer, protocolStart, headerEndIdx, (byte) '\r');
if (protocolEnd == -1) throw new IOException("Invalid request line (protocol)");
FastPathViews.RequestByteView protocolView = new FastPathViews.RequestByteView(buffer, protocolStart, protocolEnd - protocolStart);
int sectionStart = find(buffer, protocolEnd, headerEndIdx, (byte) '\n') + 1;
int current = sectionStart;
long contentLength = 0;
boolean isChunked = false;
while (current < headerEndIdx) {
int lineEnd = find(buffer, current, headerEndIdx + 1, (byte) '\r');
if (lineEnd == -1 || lineEnd == current) break;
int colon = find(buffer, current, lineEnd, (byte) ':');
if (colon != -1) {
int valueStart = colon + 1;
while (valueStart < lineEnd && buffer[valueStart] == ' ') valueStart++;
if (equalsIgnoreCase(buffer, current, colon, "content-length")) {
contentLength = parseLong(buffer, valueStart, lineEnd);
} else if (equalsIgnoreCase(buffer, current, colon, "transfer-encoding")) {
isChunked = equalsIgnoreCase(buffer, valueStart, lineEnd, "chunked");
}
}
current = lineEnd + 2;
}
headerMap.reset(buffer, sectionStart, headerEndIdx);
int bodyStart = headerEndIdx + 4;
int preBufLen = (base + totalRead) - bodyStart;
// Any bytes read beyond this request's body belong to the next request.
// Store their absolute position in the buffer — no copy needed; the next parse()
// call will read directly from bufBase without touching the data.
if (!isChunked && contentLength == 0 && preBufLen > 0) {
bufBase = bodyStart;
bufLen = preBufLen;
preBufLen = 0;
} else if (!isChunked && contentLength > 0 && preBufLen > contentLength) {
bufBase = bodyStart + (int) contentLength;
bufLen = preBufLen - (int) contentLength;
preBufLen = (int) contentLength;
}
RequestLine requestLine = new RequestLine(method, pathView, queryView, protocolView, headerMap);
if (isChunked) {
return Request.forParsed(requestLine, new ChunkedInputStream(in, buffer, bodyStart, preBufLen), -1L, null, 0, 0, remoteAddress);
}
return Request.forParsed(requestLine, in, contentLength, buffer, bodyStart, preBufLen, remoteAddress);
}
private static int findEndOfHeader(byte[] buf, int from, int len) {
for (int i = from; i <= len - 4; i++) {
if (buf[i] == '\r' && buf[i+1] == '\n' && buf[i+2] == '\r' && buf[i+3] == '\n') {
return i;
}
}
return -1;
}
private static int find(byte[] buf, int start, int end, byte target) {
for (int i = start; i < end; i++) {
if (buf[i] == target) return i;
}
return -1;
}
private static boolean equalsIgnoreCase(byte[] buf, int start, int end, String target) {
int len = end - start;
if (len != target.length()) return false;
for (int i = 0; i < len; i++) {
byte b = buf[start + i];
if (b >= 'A' && b <= 'Z') b += 32;
if (b != (byte) target.charAt(i)) return false;
}
return true;
}
private static long parseLong(byte[] buf, int start, int end) {
long value = 0;
for (int i = start; i < end; i++) {
byte c = buf[i];
if (c >= '0' && c <= '9') value = value * 10 + (c - '0');
}
return value;
}
}