package com.subgraph.orchid.directory.downloader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.Stream;
public class HttpConnection {
private final static Charset CHARSET = Charset.forName("ISO-8859-1");
private final static String HTTP_RESPONSE_REGEX = "HTTP/1\\.(\\d) (\\d+) (.*)";
private final static String CONTENT_LENGTH_HEADER = "Content-Length";
private final static String CONTENT_ENCODING_HEADER = "Content-Encoding";
private final static String COMPRESSION_SUFFIX = ".z";
private final String hostname;
private final Stream stream;
private final InputStream input;
private final OutputStream output;
private final Map<String, String> headers;
private final boolean useCompression;
private int responseCode;
private boolean bodyCompressed;
private String responseMessage;
private ByteBuffer messageBody;
public HttpConnection(Stream stream) {
this(stream, true);
}
public HttpConnection(Stream stream, boolean useCompression) {
this.hostname = getHostnameFromStream(stream);
this.stream = stream;
this.headers = new HashMap<String, String>();
this.input = stream.getInputStream();
this.output = stream.getOutputStream();
this.useCompression = useCompression;
}
private static String getHostnameFromStream(Stream stream) {
final StringBuilder sb = new StringBuilder();
final Router r = stream.getCircuit().getFinalCircuitNode().getRouter();
if(r == null) {
return null;
}
sb.append(r.getAddress().toString());
if(r.getOnionPort() != 80) {
sb.append(":");
sb.append(r.getOnionPort());
}
return sb.toString();
}
public void sendGetRequest(String request) throws IOException {
final StringBuilder sb = new StringBuilder();
sb.append("GET ");
sb.append(request);
if(useCompression && !request.endsWith(COMPRESSION_SUFFIX)) {
sb.append(COMPRESSION_SUFFIX);
}
sb.append(" HTTP/1.0\r\n");
if(hostname != null) {
sb.append("Host: "+ hostname +"\r\n");
}
sb.append("\r\n");
final String requestLine = sb.toString();
output.write(requestLine.getBytes(CHARSET));
output.flush();
}
public String getHost() {
if(hostname == null) {
return hostname;
} else {
return "(none)";
}
}
public void readResponse() throws IOException, DirectoryRequestFailedException {
readStatusLine();
readHeaders();
readBody();
}
public int getStatusCode() {
return responseCode;
}
public String getStatusMessage() {
return responseMessage;
}
public ByteBuffer getMessageBody() {
return messageBody;
}
public void close() {
if(stream == null) {
return;
}
stream.close();
}
private void readStatusLine() throws IOException, DirectoryRequestFailedException {
final String line = nextResponseLine();
final Pattern p = Pattern.compile(HTTP_RESPONSE_REGEX);
final Matcher m = p.matcher(line);
if(!m.find() || m.groupCount() != 3)
throw new DirectoryRequestFailedException("Error parsing HTTP response line: "+ line);
try {
int n1 = Integer.parseInt(m.group(1));
int n2 = Integer.parseInt(m.group(2));
if( (n1 != 0 && n1 != 1) ||
(n2 < 100 || n2 >= 600))
throw new DirectoryRequestFailedException("Failed to parse header: "+ line);
responseCode = n2;
responseMessage = m.group(3);
} catch(NumberFormatException e) {
throw new DirectoryRequestFailedException("Failed to parse header: "+ line);
}
}
private void readHeaders() throws IOException, DirectoryRequestFailedException {
headers.clear();
while(true) {
final String line = nextResponseLine();
if(line.length() == 0)
return;
final String[] args = line.split(": ", 2);
if(args.length != 2)
throw new DirectoryRequestFailedException("Failed to parse HTTP header: "+ line);
headers.put(args[0], args[1]);
}
}
private String nextResponseLine() throws IOException, DirectoryRequestFailedException {
final String line = readInputLine();
if(line == null) {
throw new DirectoryRequestFailedException("Unexpected EOF reading HTTP response");
}
return line;
}
private void readBody() throws IOException, DirectoryRequestFailedException {
processContentEncodingHeader();
if(headers.containsKey(CONTENT_LENGTH_HEADER)) {
readBodyFromContentLength();
} else {
readBodyUntilEOF();
}
}
private void processContentEncodingHeader() throws DirectoryRequestFailedException {
final String encoding = headers.get(CONTENT_ENCODING_HEADER);
if(encoding == null || encoding.equals("identity"))
bodyCompressed = false;
else if(encoding.equals("deflate") || encoding.equals("x-deflate"))
bodyCompressed = true;
else
throw new DirectoryRequestFailedException("Unrecognized content encoding: "+ encoding);
}
private void readBodyFromContentLength() throws IOException {
int bodyLength = Integer.parseInt(headers.get(CONTENT_LENGTH_HEADER));
byte[] bodyBuffer = new byte[bodyLength];
readAll(bodyBuffer);
messageBody = bytesToBody(bodyBuffer);
}
private void readBodyUntilEOF() throws IOException {
final byte[] bodyBuffer = readToEOF();
messageBody = bytesToBody(bodyBuffer);
}
private ByteBuffer bytesToBody(byte[] bs) throws IOException {
if(bodyCompressed) {
return ByteBuffer.wrap(decompressBuffer(bs));
} else {
return ByteBuffer.wrap(bs);
}
}
private byte[] decompressBuffer(byte[] buffer) throws IOException {
final ByteArrayOutputStream output = new ByteArrayOutputStream();
final Inflater decompressor = new Inflater();
final byte[] decompressBuffer = new byte[4096];
decompressor.setInput(buffer);
int n;
try {
while((n = decompressor.inflate(decompressBuffer)) != 0) {
output.write(decompressBuffer, 0, n);
}
return output.toByteArray();
} catch (DataFormatException e) {
throw new IOException("Error decompressing http body: "+ e);
}
}
private byte[] readToEOF() throws IOException {
final ByteArrayOutputStream output = new ByteArrayOutputStream();
final byte[] buffer = new byte[2048];
int n;
while( (n = input.read(buffer, 0, buffer.length)) != -1) {
output.write(buffer, 0, n);
}
return output.toByteArray();
}
private void readAll(byte[] buffer) throws IOException {
int offset = 0;
int remaining = buffer.length;
while(remaining > 0) {
int n = input.read(buffer, offset, remaining);
if(n == -1) {
throw new IOException("Unexpected early EOF reading HTTP body");
}
offset += n;
remaining -= n;
}
}
private String readInputLine() throws IOException {
final StringBuilder sb = new StringBuilder();
int c;
while((c = input.read()) != -1) {
if(c == '\n') {
return sb.toString();
} else if(c != '\r') {
sb.append((char) c);
}
}
return (sb.length() == 0) ? (null) : (sb.toString());
}
}