io.nodyn.http.HTTPParser.java Source code

Java tutorial

Introduction

Here is the source code for io.nodyn.http.HTTPParser.java

Source

/*
 * Copyright 2014 Red Hat, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.nodyn.http;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import io.nodyn.CallbackResult;
import io.nodyn.EventSource;

import java.nio.charset.Charset;
import java.util.*;

/**
 * @author Bob McWhirter
 */
public class HTTPParser extends EventSource {

    public static final String[] METHODS = new String[] { "DELETE", "GET", "HEAD", "POST", "PUT", "CONNECT",
            "OPTIONS", "TRACE", "COPY", "LOCK", "MKCOL", "MOVE", "PROPFIND", "PROPPATCH", "SEARCH", "UNLOCK",
            "REPORT", "MKACTIVITY", "CHECKOUT", "MERGE", "MSEARCH", "NOTIFY", "SUBSCRIBE", "UNSUBSCRIBE", "PATCH",
            "PURGE", };

    public static enum Error {
        INVALID_EOF_STATE("stream ended at an unexpected time"), HEADER_OVERFLOW(
                "too many header bytes seen; overflow detected"), CLOSED_CONNECTION(
                        "data received after completed connection: close message"), INVALID_VERSION(
                                "invalid HTTP version"), INVALID_STATUS("invalid HTTP status code"), INVALID_METHOD(
                                        "invalid HTTP method"), INVALID_URL("invalid URL"), INVALID_HOST(
                                                "invalid host"), INVALID_PORT("invalid port"), INVALID_PATH(
                                                        "invalid path"), INVALID_QUERY_STRING(
                                                                "invalid query string"), INVALID_FRAGMENT(
                                                                        "invalid fragment"), LF_EXPECTED(
                                                                                "LF character expected"), INVALID_HEADER_TOKEN(
                                                                                        "invalid character in header"), INVALID_CONTENT_LENGTH(
                                                                                                "invalid character in content-length header"), INVALID_CHUNK_SIZE(
                                                                                                        "invalid character in chunk size header"), INVALID_CONSTANT(
                                                                                                                "invalid constant string"), INVALID_INTERNAL_STATE(
                                                                                                                        "encountered unexpected internal state"), STRICT(
                                                                                                                                "strict mode assertion failed"), PAUSED(
                                                                                                                                        "parser is paused"), UNKNOWN(
                                                                                                                                                "an unknown error occurred");

        private String text;

        Error(String text) {
            this.text = text;
        }

    }

    private static final Charset UTF8 = Charset.forName("utf8");
    private static final Charset ASCII = Charset.forName("us-ascii");

    private static enum State {
        REQUEST, RESPONSE, HEADERS, BODY, TRAILERS,

        CHUNK_START, CHUNK_BODY, CHUNK_END,
    }

    public static final int REQUEST = 1;
    public static final int RESPONSE = 2;

    private boolean shouldReinitialize;

    private int type;
    private State state;
    private Error error;

    private CompositeByteBuf buf;

    // common
    private String url;
    private int versionMajor;
    private int versionMinor;
    private Boolean shouldKeepAlive;

    // server
    private Integer method;

    // client
    private int statusCode;
    private String statusMessage;
    private boolean upgrade;

    private boolean chunked;
    private boolean skipBody;
    private int length;

    private List<String> headers = new ArrayList<>();
    private List<String> trailers = new ArrayList<>();

    private Set<String> expectedTrailers = new HashSet<>();

    public HTTPParser() {
        this.buf = Unpooled.compositeBuffer();
    }

    public String type() {
        if (this.type == REQUEST) {
            return "*** REQUEST";
        } else if (this.type == RESPONSE) {
            return "*** RESPONSE";
        }

        return "UNKNOWN";
    }

    public void reinitialize(int type) {
        this.type = type;
        if (this.type == REQUEST) {
            this.state = State.REQUEST;
        } else {
            this.state = State.RESPONSE;
        }
        this.buf.clear();
        this.method = null;
        this.url = null;
        this.versionMajor = 0;
        this.versionMinor = 0;
        this.headers.clear();
        this.trailers.clear();
        this.expectedTrailers.clear();
        this.shouldKeepAlive = null;

        this.chunked = false;
        this.skipBody = false;
        this.length = Integer.MAX_VALUE;

        this.statusCode = 0;
        this.statusMessage = "";

        this.upgrade = false;
        this.shouldReinitialize = false;
    }

    public Integer getMethod() {
        return this.method;
    }

    public String getUrl() {
        return this.url;
    }

    public int getVersionMajor() {
        return this.versionMajor;
    }

    public int getVersionMinor() {
        return this.versionMinor;
    }

    public int getStatusCode() {
        return this.statusCode;
    }

    public String getStatusMessage() {
        return this.statusMessage;
    }

    public boolean getUpgrade() {
        return this.upgrade;
    }

    public String[] getHeaders() {
        return (String[]) this.headers.toArray(new String[this.headers.size()]);
    }

    public String[] getTrailers() {
        return (String[]) this.trailers.toArray(new String[this.headers.size()]);
    }

    public boolean getShouldKeepAlive() {
        if (this.versionMajor == 1 && this.versionMinor == 1) {
            if (this.shouldKeepAlive == null) {
                return true;
            }
            return this.shouldKeepAlive;
        } else {
            return false;
        }
    }

    public void setError(Error error) {
        this.error = error;
    }

    public Error getError() {
        return this.error;
    }

    protected boolean needsEof() {
        if (this.type == REQUEST) {
            return false;
        }

        if (((int) (this.statusCode / 100) == 1) || this.statusCode == 204 || this.statusCode == 304
                || this.skipBody) {
            return false;
        }

        if (this.chunked || this.length != Integer.MAX_VALUE) {
            return false;
        }

        return true;

    }

    public int execute(ByteBuf buf) {
        if (buf.readableBytes() == 0 && needsEof()) {
            finish();
        }

        addBuffer(buf);
        int startingLength = this.buf.readableBytes();

        LOOP: while (this.buf.readableBytes() > 0) {
            switch (this.state) {
            case REQUEST:
                if (!readRequestLine()) {
                    break LOOP;
                }
                this.state = State.HEADERS;
                continue LOOP;
            case RESPONSE:
                if (!readStatusLine()) {
                    break LOOP;
                }
                this.state = State.HEADERS;
                continue LOOP;
            case HEADERS:
                int headerResult = readHeaders();
                if (headerResult == 0) {
                    Object result = emit("headersComplete", CallbackResult.EMPTY_SUCCESS);
                    this.state = State.BODY;
                    if (result instanceof Boolean && ((Boolean) result).booleanValue()) {
                        this.skipBody = true;
                    }
                    if (this.skipBody) {
                        finish();
                        break LOOP;
                    } else {
                        if (this.chunked) {
                            this.state = State.BODY;
                            continue LOOP;
                        } else if (this.length == 0) {
                            finish();
                            break LOOP;
                        } else if (this.length != Integer.MAX_VALUE) {
                            this.state = State.BODY;
                        } else {
                            if (this.type == REQUEST || !needsEof()) {
                                finish();
                                break LOOP;
                            } else {
                                this.state = State.BODY;
                            }
                        }
                    }
                    continue LOOP;
                }
                break LOOP;
            case BODY:
                if (this.chunked) {
                    this.state = State.CHUNK_START;
                    continue LOOP;
                }
                ByteBuf body = readBody();
                emit("body", CallbackResult.createSuccess(body));
                if (this.length == 0) {
                    finish();
                    break LOOP;
                }
                continue LOOP;
            case CHUNK_START:
                if (!readChunkStart()) {
                    break LOOP;
                }
                if (this.length == 0) {
                    this.state = State.TRAILERS;
                } else {
                    this.state = State.CHUNK_BODY;
                }
                continue LOOP;
            case CHUNK_BODY:
                ByteBuf chunkBody = readBody();
                emit("body", CallbackResult.createSuccess(chunkBody));
                if (this.length == 0) {
                    this.state = State.CHUNK_END;
                }
                continue LOOP;
            case CHUNK_END:
                if (!readChunkEnd()) {
                    break LOOP;
                }
                this.state = State.CHUNK_START;
                continue LOOP;
            case TRAILERS:
                int trailerResult = readTrailers();
                if (trailerResult == 0) {
                    finish();
                }
                break LOOP;
            }
        }

        if (this.error != null) {
            return -1 * this.error.ordinal();
        }

        int endingLength = this.buf.readableBytes();
        int numRead = startingLength - endingLength;

        if (this.shouldReinitialize) {
            reinitialize(this.type);
        }

        return numRead;
    }

    void addBuffer(ByteBuf buf) {
        this.buf.writeBytes(buf);
        //this.buf.addComponent( buf );
    }

    int readableBytes() {
        return this.buf.readableBytes();
    }

    int readerIndex() {
        return this.buf.readerIndex();
    }

    protected ByteBuf readLine() {
        int cr = buf.indexOf(readerIndex(), readerIndex() + readableBytes(), (byte) '\r');
        if (cr < 0) {
            return null;
        }

        if (buf.getByte(cr + 1) != '\n') {
            return null;
        }

        int len = (cr + 2) - readerIndex();

        ByteBuf line = buf.readSlice(len);
        return line;
    }

    protected boolean readRequestLine() {
        ByteBuf line = readLine();
        if (line == null) {
            return false;
        }

        int space = line.indexOf(line.readerIndex(), line.readerIndex() + line.readableBytes(), (byte) ' ');
        if (space < 0) {
            setError(Error.INVALID_METHOD);
            return false;
        }

        int len = space - line.readerIndex();

        ByteBuf methodBuf = line.readSlice(len);

        String methodName = methodBuf.toString(UTF8);
        for (int i = 0; i < METHODS.length; ++i) {
            if (METHODS[i].equals(methodName)) {
                this.method = i;
                break;
            }
        }

        if (this.method == null) {
            setError(Error.INVALID_METHOD);
            return false;
        }

        if ("CONNECT".equals(methodName)) {
            this.upgrade = true;
        }

        // skip the space
        line.readByte();

        space = line.indexOf(line.readerIndex(), line.readerIndex() + line.readableBytes(), (byte) ' ');

        ByteBuf urlBuf = null;
        ByteBuf versionBuf = null;
        if (space < 0) {
            // HTTP/1.0
            urlBuf = line.readSlice(line.readableBytes());
        } else {
            len = space - line.readerIndex();
            urlBuf = line.readSlice(len);
            versionBuf = line.readSlice(line.readableBytes());
        }

        this.url = urlBuf.toString(UTF8).trim();

        if (versionBuf != null) {
            if (!readVersion(versionBuf)) {
                setError(Error.INVALID_VERSION);
                return false;
            }
        } else {
            this.versionMajor = 1;
            this.versionMinor = 0;
        }
        return true;
    }

    protected boolean readStatusLine() {
        ByteBuf line = readLine();

        if (line == null) {
            return false;
        }

        int space = line.indexOf(line.readerIndex(), line.readerIndex() + line.readableBytes(), (byte) ' ');

        if (space < 0) {
            setError(Error.INVALID_VERSION);
            return false;
        }

        int len = space - line.readerIndex();

        ByteBuf versionBuf = line.readSlice(len);

        if (!readVersion(versionBuf)) {
            setError(Error.INVALID_VERSION);
            return false;
        }

        // skip space
        line.readByte();

        space = line.indexOf(line.readerIndex(), line.readerIndex() + line.readableBytes(), (byte) ' ');

        if (space < 0) {
            setError(Error.INVALID_STATUS);
            return false;
        }

        len = space - line.readerIndex();

        ByteBuf statusBuf = line.readSlice(len);

        int status = -1;

        try {
            status = Integer.parseInt(statusBuf.toString(UTF8));
        } catch (NumberFormatException e) {
            setError(Error.INVALID_STATUS);
            return false;
        }

        if (status > 999 || status < 100) {
            setError(Error.INVALID_STATUS);
            return false;
        }

        this.statusCode = status;

        // skip space
        line.readByte();

        ByteBuf messageBuf = line.readSlice(line.readableBytes());

        this.statusMessage = messageBuf.toString(UTF8).trim();

        return true;
    }

    protected boolean readVersion(ByteBuf versionBuf) {
        int dotLoc = versionBuf.indexOf(versionBuf.readerIndex(),
                versionBuf.readerIndex() + versionBuf.readableBytes(), (byte) '.');
        if (dotLoc < 0) {
            return false;
        }

        char majorChar = (char) versionBuf.getByte(dotLoc - 1);
        char minorChar = (char) versionBuf.getByte(dotLoc + 1);
        try {
            this.versionMajor = Integer.parseInt("" + majorChar);
            this.versionMinor = Integer.parseInt("" + minorChar);
        } catch (NumberFormatException e) {
            return false;
        }
        return true;
    }

    protected int readHeaders() {
        return readHeaders(this.headers, true);
    }

    protected int readTrailers() {
        return readHeaders(this.trailers, false);
    }

    protected int readHeaders(List<String> target, boolean analyze) {
        while (true) {
            ByteBuf line = readLine();
            if (line == null) {
                // try again next time
                return 1;
            }

            if (line.readableBytes() == 2) {
                // end-of-headers
                return 0;
            }

            if (!readHeader(line, target, analyze)) {
                setError(Error.INVALID_HEADER_TOKEN);
                return -1;
            }
        }
    }

    protected boolean readHeader(ByteBuf line, List<String> target, boolean analyze) {
        int colonLoc = line.indexOf(line.readerIndex(), line.readerIndex() + line.readableBytes(), (byte) ':');

        if (colonLoc < 0) {
            // maybe it's a continued header
            if (line.readableBytes() > 1) {
                char c = (char) line.getByte(0);
                if (c == ' ' || c == '\t') {
                    // it IS a continued header value
                    int lastIndex = this.headers.size() - 1;
                    String val = this.headers.get(lastIndex);
                    val = val + " " + line.toString(ASCII).trim();
                    this.headers.set(lastIndex, val);
                    return true;
                }
            }
            return false;
        }

        int len = colonLoc - line.readerIndex();
        ByteBuf keyBuf = line.readSlice(len);

        // skip colon
        line.readByte();

        ByteBuf valueBuf = line.readSlice(line.readableBytes());

        String key = keyBuf.toString(UTF8).trim();
        String value = valueBuf.toString(UTF8).trim();

        target.add(key);
        target.add(value);

        if (analyze) {
            return analyzeHeader(key.toLowerCase(), value);
        }

        return true;
    }

    protected boolean analyzeHeader(String name, String value) {
        if ("content-length".equals(name)) {
            try {
                this.length = Integer.parseInt(value);
            } catch (NumberFormatException e) {
                setError(Error.INVALID_CONTENT_LENGTH);
                return false;
            }
        } else if ("transfer-encoding".equals(name)) {
            if (value.toLowerCase().contains("chunked")) {
                this.chunked = true;
            }
        } else if ("connection".equals(name)) {
            if (value.toLowerCase().contains("close")) {
                this.shouldKeepAlive = false;
            }
        } else if ("upgrade".equals(name)) {
            this.upgrade = true;
        }

        return true;
    }

    protected boolean readChunkStart() {
        ByteBuf line = readLine();
        if (line == null) {
            return false;
        }

        try {
            int len = Integer.parseInt(line.toString(UTF8).trim(), 16);
            this.length = len;
        } catch (NumberFormatException e) {
            setError(Error.INVALID_CHUNK_SIZE);
            return false;
        }

        return true;
    }

    protected boolean readChunkEnd() {
        ByteBuf line = readLine();

        if (line == null) {
            return false;
        }

        if (line.readableBytes() != 2) {
            setError(Error.INVALID_FRAGMENT);
            return false;
        }

        return true;

    }

    protected ByteBuf readBody() {
        ByteBuf data = null;
        if (this.buf.readableBytes() <= this.length) {
            data = this.buf.readSlice(this.buf.readableBytes());
            this.length -= data.readableBytes();
        } else {
            data = this.buf.readSlice(this.length);
            this.length = 0;
        }

        return data;
    }

    public void finish() {
        if (this.type == RESPONSE && this.statusCode == 100) {
            reinitialize(RESPONSE);
            return;
        }

        if (this.skipBody) {
            return;
        }

        emit("messageComplete", CallbackResult.EMPTY_SUCCESS);
        this.shouldReinitialize = true;

    }

}