Package io.nodyn.http

Source Code of io.nodyn.http.HTTPParser

/*
* 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;

    }

}
TOP

Related Classes of io.nodyn.http.HTTPParser

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.