HttpUtils.java
package sprout.server;
import sprout.mvc.http.ResponseEntity;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public final class HttpUtils {
private HttpUtils() {}
public static boolean isRequestComplete(ByteBuffer buffer) {
// 버퍼가 비어있거나 읽을 데이터가 없으면 false
if (buffer == null || !buffer.hasRemaining()) {
return false;
}
// 현재 버퍼 위치 저장 및 작업 후 복원
int originalPosition = buffer.position();
try {
// 헤더 끝(\r\n\r\n) 찾기
int headerEnd = -1;
byte[] arr = new byte[buffer.remaining()];
buffer.get(arr);
String content = new String(arr, StandardCharsets.UTF_8);
headerEnd = content.indexOf("\r\n\r\n");
if (headerEnd < 0) {
return false; // 헤더 끝이 없으면 요청 미완성
}
// 헤더 부분 추출
String headers = content.substring(0, headerEnd);
// Content-Length 확인
int contentLength = parseContentLength(headers);
boolean isChunked = isChunked(headers);
// 헤더 이후 바디 데이터 시작 위치
int bodyStart = headerEnd + 4;
int totalLength = content.length();
if (isChunked) {
// 청크드 인코딩 처리
String body = content.substring(bodyStart);
return isChunkedBodyComplete(body);
} else if (contentLength >= 0) {
// Content-Length가 명시된 경우
int bodyReceived = totalLength - bodyStart;
return bodyReceived >= contentLength;
} else {
// 바디가 없는 경우 (예: GET 요청)
return true;
}
} finally {
// 버퍼 위치 복원
buffer.position(originalPosition);
}
}
// 청크드 바디가 완전한지 확인하는 헬퍼 메서드
private static boolean isChunkedBodyComplete(String body) {
int pos = 0;
while (pos < body.length()) {
int lineEnd = body.indexOf("\r\n", pos);
if (lineEnd < 0) {
return false; // 청크 크기 라인이 없음
}
String lenLine = body.substring(pos, lineEnd);
int len;
try {
len = Integer.parseInt(lenLine.trim(), 16);
} catch (NumberFormatException e) {
return false; // 잘못된 청크 크기
}
if (len == 0) {
// 마지막 청크 (0\r\n\r\n)
return body.substring(lineEnd).startsWith("\r\n\r\n");
}
// 청크 데이터 확인
pos = lineEnd + 2; // CRLF 건너뛰기
if (pos + len + 2 > body.length()) {
return false; // 청크 데이터가 충분히 수신되지 않음
}
pos += len + 2; // 청크 데이터 + CRLF
}
return false; // 청크 끝에 도달하지 못함
}
public static String readRawRequest(ByteBuffer initial, InputStream in) throws IOException {
StringBuilder sb = new StringBuilder();
// 1) initial buffer
if (initial != null && initial.hasRemaining()) {
byte[] arr = new byte[initial.remaining()];
initial.get(arr);
sb.append(new String(arr, StandardCharsets.UTF_8));
}
// 2) 헤더 끝까지 읽기
BufferedInputStream bin = new BufferedInputStream(in);
while (!sb.toString().contains("\r\n\r\n")) {
int ch = bin.read();
if (ch == -1) break; // 연결 끊김
sb.append((char) ch);
}
// 파싱해서 Content-Length or chunked 확인
String headerPart = sb.toString();
int headerEnd = headerPart.indexOf("\r\n\r\n");
if (headerEnd < 0) return headerPart; // 잘못된 요청
String headers = headerPart.substring(0, headerEnd);
String bodyStart = headerPart.substring(headerEnd + 4);
int contentLength = parseContentLength(headers); // 없으면 -1
boolean chunked = isChunked(headers);
if (chunked) {
// TODO: chunked 디코딩
bodyStart += readChunkedBody(bin);
} else if (contentLength > -1) {
int alreadyRead = bodyStart.getBytes(StandardCharsets.UTF_8).length;
int remaining = contentLength - alreadyRead;
if (remaining > 0) {
byte[] bodyBytes = bin.readNBytes(remaining);
bodyStart += new String(bodyBytes, StandardCharsets.UTF_8);
}
}
return headers + "\r\n\r\n" + bodyStart;
}
private static int parseContentLength(String headers) {
for (String line : headers.split("\r\n")) {
if (line.toLowerCase().startsWith("content-length:")) {
return Integer.parseInt(line.split(":")[1].trim());
}
}
return -1;
}
private static boolean isChunked(String headers) {
for (String line : headers.split("\r\n")) {
if (line.toLowerCase().startsWith("transfer-encoding:")
&& line.toLowerCase().contains("chunked")) {
return true;
}
}
return false;
}
private static String readChunkedBody(InputStream in) throws IOException {
BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
StringBuilder body = new StringBuilder();
while (true) {
String lenLine = r.readLine();
if (lenLine == null) break;
int len = Integer.parseInt(lenLine.trim(), 16);
if (len == 0) {
// trailing headers consume up to empty line
while (!"".equals(r.readLine())) {}
break;
}
char[] buf = new char[len];
int read = r.read(buf);
body.append(buf, 0, read);
r.readLine(); // consume CRLF
}
return body.toString();
}
public static ByteBuffer createResponseBuffer(ResponseEntity<?> res) {
if (res == null) return null;
// Body를 바이트로 변환 (UTF-8)
byte[] bodyBytes = res.getBody() != null
? res.getBody().toString().getBytes(StandardCharsets.UTF_8)
: new byte[0];
// HTTP 헤더 작성
StringBuilder header = new StringBuilder();
header.append("HTTP/1.1 ")
.append(res.getStatusCode().getCode())
.append(" ")
.append(res.getStatusCode().getMessage())
.append("\r\n");
// Content-Type
header.append("Content-Type: ")
.append(res.getContentType())
.append("\r\n");
// Content-Length (바이트 단위로 정확히)
header.append("Content-Length: ")
.append(bodyBytes.length)
.append("\r\n");
// Connection 헤더: keep-alive 활성화 (HTTP/1.1 기본)
header.append("Connection: keep-alive\r\n");
header.append("Keep-Alive: timeout=5, max=1000\r\n");
// Custom headers
if (res.getHeaders() != null) {
for (Map.Entry<String, String> entry : res.getHeaders().entrySet()) {
header.append(entry.getKey())
.append(": ")
.append(entry.getValue())
.append("\r\n");
}
}
// 헤더 끝
header.append("\r\n");
// 헤더 바이트
byte[] headerBytes = header.toString().getBytes(StandardCharsets.UTF_8);
// 전체 응답 버퍼 생성 (헤더 + 바디)
ByteBuffer buffer = ByteBuffer.allocate(headerBytes.length + bodyBytes.length);
buffer.put(headerBytes);
buffer.put(bodyBytes);
buffer.flip();
return buffer;
}
}