package org.httpkit.server;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.httpkit.HttpMethod;
import org.httpkit.HttpUtils;
import org.httpkit.HttpVersion;
import org.httpkit.LineReader;
import org.httpkit.LineTooLargeException;
import org.httpkit.ProtocolException;
import org.httpkit.RequestTooLargeException;

import static org.httpkit.HttpUtils.*;
import static org.httpkit.HttpVersion.HTTP_1_0;
import static org.httpkit.HttpVersion.HTTP_1_1;

public class HttpDecoder {

    public enum State {
        ALL_READ, CONNECTION_OPEN, READ_INITIAL, READ_HEADER, READ_FIXED_LENGTH_CONTENT, READ_CHUNK_SIZE, READ_CHUNKED_CONTENT, READ_CHUNK_FOOTER, READ_CHUNK_DELIMITER,
    }

    /**
     * Pattern for matching numbers 0 to 255.  We use this in the IPV4 address pattern to prevent invalid sequences
     * from be parsed by InetAddress.getByName and thus being treated as a name instead of an address.
     */
    private static final String IPV4SEG = "(?:0|1\\d{0,2}|2(?:[0-4]\\d*|5[0-5]?|[6-9])?|[3-9]\\d?)";
    private static final String IPV4ADDR = IPV4SEG + "(?:\\." + IPV4SEG + "){3}";
    /**
     * Pattern for a port number.  We are not as strict in our pattern matching as we are with ipv4 address
     * and instead rely upon Integer.parseInt and a range check.  Port 0 is invalid, so we disallow that here.
     */
    private static final String PORT = "[1-9]\\d{0,4}";
    /**
     * The PROXY protocol header is quite strict in what it allows, specifying for example that only
     * a single space character (\x20) is allowed between components.
     */
    private static final Pattern PROXY_PATTERN = Pattern.compile(
        "PROXY\\x20TCP4\\x20(" + IPV4ADDR + ")\\x20(" + IPV4ADDR +")\\x20(" + PORT + ")\\x20(" + PORT + ")"
    );

    private State state;
    private ProxyProtocolOption proxyProtocolOption;
    private int readRemaining = 0; // bytes need read
    private int readCount = 0; // already read bytes count

    private String xForwardedFor;
    private String xForwardedProto;
    private int xForwardedPort;
    HttpRequest request; // package visible
    private Map<String, Object> headers = new TreeMap<String, Object>();
    byte[] content;

    private final int maxBody;
    private final LineReader lineReader;

    public HttpDecoder(int maxBody, int maxLine, ProxyProtocolOption proxyProtocolOption) {
        this.maxBody = maxBody;
        this.lineReader = new LineReader(maxLine);
        this.proxyProtocolOption = (proxyProtocolOption == null)
            ? ProxyProtocolOption.DISABLED : proxyProtocolOption;

        this.state = (proxyProtocolOption == ProxyProtocolOption.DISABLED)
            ? State.READ_INITIAL : State.CONNECTION_OPEN;
    }

    private boolean parseProxyLine(String line) throws ProtocolException {
        // PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535\r\n
        if (!line.startsWith("PROXY ")) {
            return false;
        }

        final Matcher m = PROXY_PATTERN.matcher(line);
        if (!m.matches()) {
            throw new ProtocolException("Unsupported or malformed proxy header: "+line);
        }

        try {
            final InetAddress clientAddr = InetAddress.getByName(m.group(1));
            final InetAddress proxyAddr = InetAddress.getByName(m.group(2));

            final int clientPort = Integer.parseInt(m.group(3), 10);
            final int proxyPort = Integer.parseInt(m.group(4), 10);

            if (((clientPort | proxyPort) & ~0xffff) != 0) {
                throw new ProtocolException("Invalid port number: "+line);
            }

            xForwardedFor = clientAddr.getHostAddress();
            if (proxyPort == 80) {
                xForwardedProto = "http";
            } else if (proxyPort == 443) {
                xForwardedProto = "https";
            }
            xForwardedPort = proxyPort;

            return true;

        } catch (NumberFormatException ex) {
            throw new ProtocolException("Malformed port in: "+line);
        } catch (UnknownHostException ex) {
            throw new ProtocolException("Malformed address in: "+line);
        }
    }

    private void createRequest(String sb) throws ProtocolException {
        int aStart;
        int aEnd;
        int bStart;
        int bEnd;
        int cStart;
        int cEnd;

        aStart = findNonWhitespace(sb, 0);
        aEnd = findWhitespace(sb, aStart);

        bStart = findNonWhitespace(sb, aEnd);
        bEnd = findWhitespace(sb, bStart);

        cStart = findNonWhitespace(sb, bEnd);
        cEnd = findEndOfString(sb, cStart);

        if (cStart < cEnd) {
            try {
                HttpMethod method = HttpMethod.valueOf(sb.substring(aStart, aEnd).toUpperCase());
                HttpVersion version = HTTP_1_1;
                if ("HTTP/1.0".equals(sb.substring(cStart, cEnd))) {
                    version = HTTP_1_0;
                }
                request = new HttpRequest(method, sb.substring(bStart, bEnd), version);
            } catch (Exception e) {
                throw new ProtocolException("method not understand");
            }
        } else {
            throw new ProtocolException("not http?");
        }
    }

    public boolean requiresContinue() {
	if (request == null || request.version != HTTP_1_1 || request.sentContinue) {
	    return false;
	} else {
	    String expect = (String) headers.get(EXPECT);
	    return(expect != null && CONTINUE.equalsIgnoreCase(expect));
	}
    }

    public void setSentContinue() {
        request.sentContinue = true;
    }

    public HttpRequest decode(ByteBuffer buffer) throws LineTooLargeException,
            ProtocolException, RequestTooLargeException {
        String line;
        while (buffer.hasRemaining()) {
            switch (state) {
                case ALL_READ:
                    return request;
                case CONNECTION_OPEN:
                    line = lineReader.readLine(buffer);
                    if (line != null) {
                        // parseProxyLines returns true if the line parsed
                        // false if it was not a PROXY line
                        // or throws ProtocolException, if the PROXY line is malformed or unsupported.
                        if (parseProxyLine(line)) {
                            // valid proxy header
                            state = State.READ_INITIAL;
                        } else if (proxyProtocolOption == ProxyProtocolOption.OPTIONAL) {
                            // did not parse as a proxy header, try to create a request from it
                            // as the READ_INITIAL state would.
                            createRequest(line);
                            state = State.READ_HEADER;
                        } else {
                            throw new ProtocolException("Expected PROXY header, got: "+line);
                        }
                    }
                    break;
                case READ_INITIAL:
                    line = lineReader.readLine(buffer);
                    if (line != null) {
                        createRequest(line);
                        state = State.READ_HEADER;
                    }
                    break;
                case READ_HEADER:
                    readHeaders(buffer);
                    break;
                case READ_CHUNK_SIZE:
                    line = lineReader.readLine(buffer);
                    if (line != null) {
                        readRemaining = getChunkSize(line);
                        if (readRemaining == 0) {
                            state = State.READ_CHUNK_FOOTER;
                        } else {
                            throwIfBodyIsTooLarge();
                            if (content == null) {
                                content = new byte[readRemaining];
                            } else if (content.length < readCount + readRemaining) {
                                // *1.3 to protect slow client
                                int newLength = (int) ((readRemaining + readCount) * 1.3);
                                content = Arrays.copyOf(content, newLength);
                            }
                            state = State.READ_CHUNKED_CONTENT;
                        }
                    }
                    break;
                case READ_FIXED_LENGTH_CONTENT:
                    readFixedLength(buffer);
                    if (readRemaining == 0) {
                        finish();
                    }
                    break;
                case READ_CHUNKED_CONTENT:
                    readFixedLength(buffer);
                    if (readRemaining == 0) {
                        state = State.READ_CHUNK_DELIMITER;
                    }
                    break;
                case READ_CHUNK_FOOTER:
                    readEmptyLine(buffer);
                    finish();
                    break;
                case READ_CHUNK_DELIMITER:
                    readEmptyLine(buffer);
                    state = State.READ_CHUNK_SIZE;
                    break;
            }
        }
        return state == State.ALL_READ ? request : null;
    }

    private void finish() {
        state = State.ALL_READ;
        request.setBody(content, readCount);
    }

    void readEmptyLine(ByteBuffer buffer) {
        byte b = buffer.get();
        if (b == CR && buffer.hasRemaining()) {
            buffer.get(); // should be LF
        }
    }

    void readFixedLength(ByteBuffer buffer) {
        int toRead = Math.min(buffer.remaining(), readRemaining);
        buffer.get(content, readCount, toRead);
        readRemaining -= toRead;
        readCount += toRead;
    }

    private void readHeaders(ByteBuffer buffer) throws LineTooLargeException,
            RequestTooLargeException, ProtocolException {
        if (proxyProtocolOption == ProxyProtocolOption.OPTIONAL
            || proxyProtocolOption == ProxyProtocolOption.ENABLED) {
            headers.put("x-forwarded-for", xForwardedFor);
            headers.put("x-forwarded-proto", xForwardedProto);
            headers.put("x-forwarded-port", xForwardedPort);
        }
        String line = lineReader.readLine(buffer);
        while (line != null && !line.isEmpty()) {
            HttpUtils.splitAndAddHeader(line, headers);
            line = lineReader.readLine(buffer);
        }

        if (line == null) {
            return;
        }

        request.setHeaders(headers);

        String te = HttpUtils.getStringValue(headers, TRANSFER_ENCODING);
        if (CHUNKED.equals(te)) {
            state = State.READ_CHUNK_SIZE;
        } else {
            String cl = HttpUtils.getStringValue(headers, CONTENT_LENGTH);
            if (cl != null) {
                try {
                    readRemaining = Integer.parseInt(cl);
                    if (readRemaining > 0) {
                        throwIfBodyIsTooLarge();
                        content = new byte[readRemaining];
                        state = State.READ_FIXED_LENGTH_CONTENT;
                    } else {
                        state = State.ALL_READ;
                    }
                } catch (NumberFormatException e) {
                    throw new ProtocolException(e.getMessage());
                }
            } else {
                state = State.ALL_READ;
            }
        }
    }

    public void reset() {
        state = State.READ_INITIAL;
        headers = new TreeMap<String, Object>();
        readCount = 0;
        content = null;
        lineReader.reset();
        request = null;
    }

    private void throwIfBodyIsTooLarge() throws RequestTooLargeException {
        if (readCount + readRemaining > maxBody) {
            throw new RequestTooLargeException("request body " + (readCount + readRemaining)
                    + "; max request body " + maxBody);
        }
    }
}

// valid proxy header
package org.httpkit.server;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.httpkit.HttpMethod;
import org.httpkit.HttpUtils;
import org.httpkit.HttpVersion;
import org.httpkit.LineReader;
import org.httpkit.LineTooLargeException;
import org.httpkit.ProtocolException;
import org.httpkit.RequestTooLargeException;

import static org.httpkit.HttpUtils.*;
import static org.httpkit.HttpVersion.HTTP_1_0;
import static org.httpkit.HttpVersion.HTTP_1_1;

public class HttpDecoder {

    public enum State {
        ALL_READ, CONNECTION_OPEN, READ_INITIAL, READ_HEADER, READ_FIXED_LENGTH_CONTENT, READ_CHUNK_SIZE, READ_CHUNKED_CONTENT, READ_CHUNK_FOOTER, READ_CHUNK_DELIMITER,
    }

    /**
     * Pattern for matching numbers 0 to 255.  We use this in the IPV4 address pattern to prevent invalid sequences
     * from be parsed by InetAddress.getByName and thus being treated as a name instead of an address.
     */
    private static final String IPV4SEG = "(?:0|1\\d{0,2}|2(?:[0-4]\\d*|5[0-5]?|[6-9])?|[3-9]\\d?)";
    private static final String IPV4ADDR = IPV4SEG + "(?:\\." + IPV4SEG + "){3}";
    /**
     * Pattern for a port number.  We are not as strict in our pattern matching as we are with ipv4 address
     * and instead rely upon Integer.parseInt and a range check.  Port 0 is invalid, so we disallow that here.
     */
    private static final String PORT = "[1-9]\\d{0,4}";
    /**
     * The PROXY protocol header is quite strict in what it allows, specifying for example that only
     * a single space character (\x20) is allowed between components.
     */
    private static final Pattern PROXY_PATTERN = Pattern.compile(
        "PROXY\\x20TCP4\\x20(" + IPV4ADDR + ")\\x20(" + IPV4ADDR +")\\x20(" + PORT + ")\\x20(" + PORT + ")"
    );

    private State state;
    private ProxyProtocolOption proxyProtocolOption;
    private int readRemaining = 0; // bytes need read
    private int readCount = 0; // already read bytes count

    private String xForwardedFor;
    private String xForwardedProto;
    private int xForwardedPort;
    HttpRequest request; // package visible
    private Map<String, Object> headers = new TreeMap<String, Object>();
    byte[] content;

    private final int maxBody;
    private final LineReader lineReader;

    public HttpDecoder(int maxBody, int maxLine, ProxyProtocolOption proxyProtocolOption) {
        this.maxBody = maxBody;
        this.lineReader = new LineReader(maxLine);
        this.proxyProtocolOption = (proxyProtocolOption == null)
            ? ProxyProtocolOption.DISABLED : proxyProtocolOption;

        this.state = (proxyProtocolOption == ProxyProtocolOption.DISABLED)
            ? State.READ_INITIAL : State.CONNECTION_OPEN;
    }

    private boolean parseProxyLine(String line) throws ProtocolException {
        // PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535\r\n
        if (!line.startsWith("PROXY ")) {
            return false;
        }

        final Matcher m = PROXY_PATTERN.matcher(line);
        if (!m.matches()) {
            throw new ProtocolException("Unsupported or malformed proxy header: "+line);
        }

        try {
            final InetAddress clientAddr = InetAddress.getByName(m.group(1));
            final InetAddress proxyAddr = InetAddress.getByName(m.group(2));

            final int clientPort = Integer.parseInt(m.group(3), 10);
            final int proxyPort = Integer.parseInt(m.group(4), 10);

            if (((clientPort | proxyPort) & ~0xffff) != 0) {
                throw new ProtocolException("Invalid port number: "+line);
            }

            xForwardedFor = clientAddr.getHostAddress();
            if (proxyPort == 80) {
                xForwardedProto = "http";
            } else if (proxyPort == 443) {
                xForwardedProto = "https";
            }
            xForwardedPort = proxyPort;

            return true;

        } catch (NumberFormatException ex) {
            throw new ProtocolException("Malformed port in: "+line);
        } catch (UnknownHostException ex) {
            throw new ProtocolException("Malformed address in: "+line);
        }
    }

    private void createRequest(String sb) throws ProtocolException {
        int aStart;
        int aEnd;
        int bStart;
        int bEnd;
        int cStart;
        int cEnd;

        aStart = findNonWhitespace(sb, 0);
        aEnd = findWhitespace(sb, aStart);

        bStart = findNonWhitespace(sb, aEnd);
        bEnd = findWhitespace(sb, bStart);

        cStart = findNonWhitespace(sb, bEnd);
        cEnd = findEndOfString(sb, cStart);

        if (cStart < cEnd) {
            try {
                HttpMethod method = HttpMethod.valueOf(sb.substring(aStart, aEnd).toUpperCase());
                HttpVersion version = HTTP_1_1;
                if ("HTTP/1.0".equals(sb.substring(cStart, cEnd))) {
                    version = HTTP_1_0;
                }
                request = new HttpRequest(method, sb.substring(bStart, bEnd), version);
            } catch (Exception e) {
                throw new ProtocolException("method not understand");
            }
        } else {
            throw new ProtocolException("not http?");
        }
    }

    public boolean requiresContinue() {
	if (request == null || request.version != HTTP_1_1 || request.sentContinue) {
	    return false;
	} else {
	    String expect = (String) headers.get(EXPECT);
	    return(expect != null && CONTINUE.equalsIgnoreCase(expect));
	}
    }

    public void setSentContinue() {
        request.sentContinue = true;
    }

    public HttpRequest decode(ByteBuffer buffer) throws LineTooLargeException,
            ProtocolException, RequestTooLargeException {
        String line;
        while (buffer.hasRemaining()) {
            switch (state) {
                case ALL_READ:
                    return request;
                case CONNECTION_OPEN:
                    line = lineReader.readLine(buffer);
                    if (line != null) {
                        // parseProxyLines returns true if the line parsed
                        // false if it was not a PROXY line
                        // or throws ProtocolException, if the PROXY line is malformed or unsupported.
                        if (parseProxyLine(line)) {
                            // valid proxy<SUF>
                            state = State.READ_INITIAL;
                        } else if (proxyProtocolOption == ProxyProtocolOption.OPTIONAL) {
                            // did not parse as a proxy header, try to create a request from it
                            // as the READ_INITIAL state would.
                            createRequest(line);
                            state = State.READ_HEADER;
                        } else {
                            throw new ProtocolException("Expected PROXY header, got: "+line);
                        }
                    }
                    break;
                case READ_INITIAL:
                    line = lineReader.readLine(buffer);
                    if (line != null) {
                        createRequest(line);
                        state = State.READ_HEADER;
                    }
                    break;
                case READ_HEADER:
                    readHeaders(buffer);
                    break;
                case READ_CHUNK_SIZE:
                    line = lineReader.readLine(buffer);
                    if (line != null) {
                        readRemaining = getChunkSize(line);
                        if (readRemaining == 0) {
                            state = State.READ_CHUNK_FOOTER;
                        } else {
                            throwIfBodyIsTooLarge();
                            if (content == null) {
                                content = new byte[readRemaining];
                            } else if (content.length < readCount + readRemaining) {
                                // *1.3 to protect slow client
                                int newLength = (int) ((readRemaining + readCount) * 1.3);
                                content = Arrays.copyOf(content, newLength);
                            }
                            state = State.READ_CHUNKED_CONTENT;
                        }
                    }
                    break;
                case READ_FIXED_LENGTH_CONTENT:
                    readFixedLength(buffer);
                    if (readRemaining == 0) {
                        finish();
                    }
                    break;
                case READ_CHUNKED_CONTENT:
                    readFixedLength(buffer);
                    if (readRemaining == 0) {
                        state = State.READ_CHUNK_DELIMITER;
                    }
                    break;
                case READ_CHUNK_FOOTER:
                    readEmptyLine(buffer);
                    finish();
                    break;
                case READ_CHUNK_DELIMITER:
                    readEmptyLine(buffer);
                    state = State.READ_CHUNK_SIZE;
                    break;
            }
        }
        return state == State.ALL_READ ? request : null;
    }

    private void finish() {
        state = State.ALL_READ;
        request.setBody(content, readCount);
    }

    void readEmptyLine(ByteBuffer buffer) {
        byte b = buffer.get();
        if (b == CR && buffer.hasRemaining()) {
            buffer.get(); // should be LF
        }
    }

    void readFixedLength(ByteBuffer buffer) {
        int toRead = Math.min(buffer.remaining(), readRemaining);
        buffer.get(content, readCount, toRead);
        readRemaining -= toRead;
        readCount += toRead;
    }

    private void readHeaders(ByteBuffer buffer) throws LineTooLargeException,
            RequestTooLargeException, ProtocolException {
        if (proxyProtocolOption == ProxyProtocolOption.OPTIONAL
            || proxyProtocolOption == ProxyProtocolOption.ENABLED) {
            headers.put("x-forwarded-for", xForwardedFor);
            headers.put("x-forwarded-proto", xForwardedProto);
            headers.put("x-forwarded-port", xForwardedPort);
        }
        String line = lineReader.readLine(buffer);
        while (line != null && !line.isEmpty()) {
            HttpUtils.splitAndAddHeader(line, headers);
            line = lineReader.readLine(buffer);
        }

        if (line == null) {
            return;
        }

        request.setHeaders(headers);

        String te = HttpUtils.getStringValue(headers, TRANSFER_ENCODING);
        if (CHUNKED.equals(te)) {
            state = State.READ_CHUNK_SIZE;
        } else {
            String cl = HttpUtils.getStringValue(headers, CONTENT_LENGTH);
            if (cl != null) {
                try {
                    readRemaining = Integer.parseInt(cl);
                    if (readRemaining > 0) {
                        throwIfBodyIsTooLarge();
                        content = new byte[readRemaining];
                        state = State.READ_FIXED_LENGTH_CONTENT;
                    } else {
                        state = State.ALL_READ;
                    }
                } catch (NumberFormatException e) {
                    throw new ProtocolException(e.getMessage());
                }
            } else {
                state = State.ALL_READ;
            }
        }
    }

    public void reset() {
        state = State.READ_INITIAL;
        headers = new TreeMap<String, Object>();
        readCount = 0;
        content = null;
        lineReader.reset();
        request = null;
    }

    private void throwIfBodyIsTooLarge() throws RequestTooLargeException {
        if (readCount + readRemaining > maxBody) {
            throw new RequestTooLargeException("request body " + (readCount + readRemaining)
                    + "; max request body " + maxBody);
        }
    }
}
