com.xqbase.tuna.http.HttpPacket Maven / Gradle / Ivy
package com.xqbase.tuna.http;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import com.xqbase.tuna.ConnectionHandler;
import com.xqbase.tuna.util.ByteArrayQueue;
public class HttpPacket {
public static final int
TYPE_REQUEST = 0,
TYPE_RESPONSE = 1,
TYPE_RESPONSE_HEAD = 2,
TYPE_RESPONSE_HTTP10 = 3;
private static final int
PHASE_START = 0,
PHASE_HEADER = 1,
PHASE_BODY = 2,
PHASE_CHUNK_SIZE = 3,
PHASE_CHUNK_DATA = 4,
PHASE_CHUNK_CRLF = 5,
PHASE_TRAILER = 6,
PHASE_END_CHUNK = 7,
PHASE_END = 8;
private static final byte[]
SPACE = {' '},
COLON = {':', ' '},
CRLF = {'\r', '\n'},
HTTP10 = "HTTP/1.0".getBytes(),
HTTP11 = "HTTP/1.1".getBytes(),
FINAL_CRLF = {'0', '\r', '\n'};
private int type = TYPE_REQUEST, phase = PHASE_START,
headerLimit = 32768, headerSize = 0, status = 0, bytesToRead = 0;
private boolean http10 = false;
private String method = null, uri = null, reason = null;
private LinkedHashMap> headers = new LinkedHashMap<>();
private ByteArrayQueue body = new ByteArrayQueue();
private StringBuilder line = new StringBuilder();
public HttpPacket() {/**/}
public HttpPacket(int status, String reason,
byte[] body, String... headerPairs) {
type = TYPE_RESPONSE;
this.status = status;
this.reason = reason;
getBody().add(body);
setHeader("Content-Length", "" + body.length);
for (int i = 0; i < headerPairs.length - 1; i += 2) {
String key = headerPairs[i];
String value = headerPairs[i + 1];
if (key != null && value != null) {
setHeader(key, value);
}
}
}
public void reset() {
phase = PHASE_START;
headerSize = status = bytesToRead = 0;
method = uri = reason = null;
headers.clear();
body.clear();
line.setLength(0);
}
/** @return true
for reading request */
public boolean isRequest() {
return type == TYPE_REQUEST;
}
/** @return true
for reading response */
public boolean isResponse() {
return type != TYPE_REQUEST;
}
/** @param headerLimit */
public void setHeaderLimit(int headerLimit) {
this.headerLimit = headerLimit;
}
/**
* @param type {@link #TYPE_REQUEST}, {@link #TYPE_RESPONSE},
* {@link #TYPE_RESPONSE_HEAD} or {@link #TYPE_RESPONSE_HTTP10}
*/
public void setType(int type) {
this.type = type;
}
/** @return true
for an HTTP/1.0 request or response */
public boolean isHttp10() {
return http10;
}
/**
* @param http10 true
to write body in HTTP/1.0 mode,
* regardless "Transfer-Encoding"
*/
public void setHttp10(boolean http10) {
this.http10 = http10;
}
/** @return true
for a complete header for request or response */
public boolean isCompleteHeader() {
return phase >= PHASE_BODY;
}
/** @return true
for a complete request or response */
public boolean isComplete() {
return phase >= PHASE_END_CHUNK;
}
/** @return true
for a chunked request or response */
public boolean isChunked() {
return phase >= PHASE_CHUNK_SIZE && phase <= PHASE_END_CHUNK;
}
/** @return Request Method, only available for reading request */
public String getMethod() {
return method;
}
/** @param method Request Method, only available for writing request */
public void setMethod(String method) {
this.method = method;
}
/** @return Request URI, only available for reading request */
public String getUri() {
return uri;
}
/** @param uri Request URI, only available for writing request */
public void setUri(String uri) {
this.uri = uri;
}
/** @return Response Status, only available for reading response */
public int getStatus() {
return status;
}
/** @param status Status Code, only available for writing response */
public void setStatus(int status) {
this.status = status;
}
/** @return Reason Phrase, only available for reading response */
public String getReason() {
return reason;
}
/** @param reason Reason Phrase, only available for writing response */
public void setReason(String reason) {
this.reason = reason;
}
/** @return HTTP Headers for request or response */
public LinkedHashMap> getHeaders() {
return headers;
}
/** @return HTTP Body for request or response */
public ByteArrayQueue getBody() {
return body;
}
/** @return true
for a complete line */
private boolean readLine(ByteArrayQueue queue) throws HttpPacketException {
byte[] b = queue.array();
int begin = queue.offset();
int end = begin + queue.length();
for (int i = begin; i < end; i ++) {
headerSize ++;
if (headerSize > headerLimit) {
throw new HttpPacketException(HttpPacketException.
HEADER_SIZE, "" + headerLimit);
}
char c = (char) (b[i] & 0xFF);
if (c == '\n') {
queue.remove(i - begin + 1);
return true;
}
if (c != '\r') {
line.append(c);
}
}
queue.remove(end - begin);
return false;
}
/** @return true
for a complete body or chunk */
private boolean readData(ByteArrayQueue queue) {
int n;
if (bytesToRead < 0) {
n = queue.length();
} else if (bytesToRead > queue.length()) {
n = queue.length();
bytesToRead -= n;
} else {
n = bytesToRead;
bytesToRead = 0;
}
body.add(queue.array(), queue.offset(), n);
queue.remove(n);
return bytesToRead == 0;
}
/** @return number of bytes read */
private void readHeader() {
int colon = line.indexOf(":");
if (colon < 0) {
line.setLength(0);
return;
}
String originalKey = line.substring(0, colon);
String key = originalKey.toUpperCase();
ArrayList values = headers.get(key);
if (values == null) {
values = new ArrayList<>();
values.add(originalKey);
headers.put(key, values);
}
values.add(line.substring(colon + 1).trim());
line.setLength(0);
}
/** @throws HttpPacketException a bad request or response */
public void read(ByteArrayQueue queue) throws HttpPacketException {
if (phase == PHASE_START) {
if (!readLine(queue) || line.length() == 0) {
return;
}
String[] ss = line.toString().split(" ", 3);
if (ss.length < 3) {
throw new HttpPacketException(HttpPacketException.START_LINE, line.toString());
}
String version = (type == TYPE_REQUEST ? ss[2] : ss[0]).toUpperCase();
if (version.equals("HTTP/1.0")) {
http10 = true;
} else if (!version.equals("HTTP/1.1")) {
throw new HttpPacketException(HttpPacketException.VERSION, version);
}
if (type == TYPE_REQUEST) {
method = ss[0];
uri = ss[1];
if (method.toUpperCase().equals("CONNECT")) {
bytesToRead = -1;
}
} else {
try {
status = Integer.parseInt(ss[1]);
} catch (NumberFormatException e) {
throw new HttpPacketException(HttpPacketException.STATUS, ss[1]);
}
reason = ss[2];
if (status == 101) {
bytesToRead = -1;
}
}
line.setLength(0);
phase = PHASE_HEADER;
}
if (phase == PHASE_HEADER) {
while (true) {
if (!readLine(queue)) {
return;
}
if (line.length() == 0) {
break;
}
readHeader();
}
phase = PHASE_BODY;
if (type != TYPE_RESPONSE_HEAD) {
if (testHeader("TRANSFER-ENCODING", "chunked")) {
phase = PHASE_CHUNK_SIZE;
} else {
String contentLength = getHeader("CONTENT-LENGTH");
if (contentLength != null) {
try {
bytesToRead = Integer.parseInt(contentLength);
} catch (NumberFormatException e) {
bytesToRead = -1;
}
if (bytesToRead < 0) {
throw new HttpPacketException(HttpPacketException.
CONTENT_LENGTH, contentLength);
}
} else if (type == TYPE_RESPONSE_HTTP10 ||
(type == TYPE_RESPONSE && http10)) {
bytesToRead = -1;
}
}
}
}
if (phase == PHASE_BODY) {
if (readData(queue)) {
phase = PHASE_END;
}
return;
}
// phase == PHASE_CHUNK_SIZE/DATA/CRLF
if (phase < PHASE_TRAILER) {
while (true) {
if (phase == PHASE_CHUNK_DATA) {
if (!readData(queue)) {
return;
}
phase = PHASE_CHUNK_CRLF;
}
// Reset "headerSize" before reading Chunk Size
headerSize = 0;
if (!readLine(queue)) {
return;
}
if (phase == PHASE_CHUNK_CRLF) {
line.setLength(0);
phase = PHASE_CHUNK_SIZE;
continue;
}
int space = line.indexOf(" ");
String value = space < 0 ? line.toString() : line.substring(0, space);
try {
bytesToRead = Integer.parseInt(value, 16);
} catch (NumberFormatException e) {
bytesToRead = -1;
}
if (bytesToRead < 0) {
throw new HttpPacketException(HttpPacketException.CHUNK_SIZE, value);
}
line.setLength(0);
if (bytesToRead == 0) {
phase = PHASE_TRAILER;
// Reset "headerSize" before reading Trailer
headerSize = 0;
break;
}
phase = PHASE_CHUNK_DATA;
}
}
if (phase == PHASE_TRAILER) {
while (true) {
if (!readLine(queue)) {
return;
}
if (line.length() == 0) {
break;
}
readHeader();
}
phase = PHASE_END_CHUNK;
}
}
public void endRead() {
phase = PHASE_END;
}
public void continueRead() {
phase = PHASE_BODY;
bytesToRead = -1;
}
/**
* @param key Field Name in Upper Case
* @param value Field Value in Lower Case
*/
public boolean testHeader(String key, String value) {
ArrayList values = headers.get(key);
if (values == null) {
return false;
}
int size = values.size();
for (int i = 1; i < size; i ++) {
for (String s : values.get(i).split(",")) {
if (s.trim().toLowerCase().equals(value)) {
return true;
}
}
}
return false;
}
/** @param key Field Name in Upper Case */
public String getHeader(String key) {
ArrayList values = headers.get(key);
return values == null || values.size() != 2 ? null : values.get(1);
}
/** @param key Field Name in Upper Case */
public void removeHeader(String key) {
headers.remove(key);
}
/**
* @param key Field Name
* @param value Field Value
*/
public void setHeader(String key, String value) {
String key_ = key.toUpperCase();
ArrayList values = headers.get(key_);
if (values == null) {
values = new ArrayList<>();
headers.put(key_, values);
values.add(key);
} else if (values.isEmpty()) {
// Should NOT Happen
values.add(key);
} else {
String value_ = values.get(0);
values.clear();
values.add(value_);
}
values.add(value);
}
public void writeHeaders(ByteArrayQueue data) {
Iterator>> it =
headers.entrySet().iterator();
while (it.hasNext()) {
Map.Entry> entry = it.next();
it.remove();
ArrayList values = entry.getValue();
int size = values.size();
if (size < 1) {
continue;
}
byte[] keyBytes = values.get(0).getBytes(StandardCharsets.ISO_8859_1);
for (int i = 1; i < size; i ++) {
data.add(keyBytes).add(COLON).add(values.get(i).
getBytes(StandardCharsets.ISO_8859_1)).add(CRLF);
}
}
data.add(CRLF);
}
/**
* @param data {@link ByteArrayQueue} to write into
* @param begin true
to write entire Request or Response,
* including Start Line and Headers,
* and false
to write Body and Trailers (if available) only
* @param forceChunked true
to force to write in Chunked mode
*/
public void write(ByteArrayQueue data, boolean begin, boolean forceChunked) {
if (begin) {
if (type == TYPE_REQUEST) {
data.add(method.getBytes(StandardCharsets.ISO_8859_1)).add(SPACE).
add(uri.getBytes(StandardCharsets.ISO_8859_1)).add(SPACE).
add(http10 ? HTTP10 : HTTP11).add(CRLF);
} else {
data.add(http10 ? HTTP10 : HTTP11).add(SPACE).
add(("" + status).getBytes()).add(SPACE).
add(reason.getBytes(StandardCharsets.ISO_8859_1)).add(CRLF);
}
writeHeaders(data);
}
int length = body.length();
if (forceChunked || (!http10 && phase >= PHASE_CHUNK_SIZE &&
phase <= PHASE_END_CHUNK)) {
if (length > 0) {
data.add(Integer.toHexString(length).getBytes());
data.add(CRLF);
data.add(body.array(), body.offset(), length);
body.remove(length);
data.add(CRLF);
}
if (phase >= PHASE_END_CHUNK) {
data.add(FINAL_CRLF);
writeHeaders(data);
}
} else if (length > 0) {
data.add(body.array(), body.offset(), length);
body.remove(length);
}
}
/**
* @param handler {@link ConnectionHandler} to write into
* @param begin true
to write entire Request or Response,
* including Start Line and Headers,
* and false
to write Body and Trailers (if available) only
* @param forceChunked true
to force to write in Chunked mode
*/
public void write(ConnectionHandler handler, boolean begin, boolean forceChunked) {
ByteArrayQueue data = new ByteArrayQueue();
write(data, begin, forceChunked);
handler.send(data.array(), data.offset(), data.length());
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy