/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ /* * To change this template, choose Tools | Templates * and open the template in the editor. */ package com.facebook.infrastructure.net.http; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; /** * * @author kranganathan */ public class HttpStartLineParser { private Callback callback_; public interface Callback { void onStartLine(String method, String path, String query, String version); }; public HttpStartLineParser(Callback cb) { callback_ = cb; } private enum StartLineParseState { EATING_WHITESPACE, READING_METHOD, READING_PATH, READING_QUERY, DECODING_FIRST_CHAR, DECODING_SECOND_CHAR, READING_VERSION, CHECKING_EOL, TO_RESET } private StartLineParseState parseState_ = StartLineParseState.TO_RESET; private StartLineParseState nextState_; private StringBuilder httpMethod_ = new StringBuilder(32); private StringBuilder httpPath_ = new StringBuilder(); private StringBuilder httpQuery_ = new StringBuilder(32); private StringBuilder httpVersion_ = new StringBuilder(); // we will encode things of the form %{2 digit hex number} and this is a // temporary holder for the leftmost digit's value as the second digit is // being read private int encodedValue_; // this is a pointer to one of httpMethod_, httpPath_, httpQuery_, // httpVersion_ so that the encoded value can be appended to the correct // buffer private StringBuilder encodeTo_; public void resetParserState() { httpMethod_.setLength(0); httpPath_.setLength(0); httpQuery_.setLength(0); httpVersion_.setLength(0); parseState_ = StartLineParseState.EATING_WHITESPACE; nextState_ = StartLineParseState.READING_METHOD; } private void finishLine_() { if (callback_ != null) { callback_.onStartLine( httpMethod_.toString(), httpPath_.toString(), httpQuery_.toString(), httpVersion_.toString() ); } } private static int decodeHex(int hex) { if (hex >= '0' && hex <= '9') { return hex-'0'; } else if (hex >= 'a' && hex <= 'f') { return hex-'a'+10; } else if (hex >= 'A' && hex <= 'F') { return hex-'A'+10; } else { return 0; } } public boolean onMoreBytes(InputStream in) throws HttpParsingException, IOException { int got; if (parseState_ == StartLineParseState.TO_RESET) { resetParserState(); } while (in.available() > 0) { in.mark(1); got = in.read(); switch (parseState_) { case EATING_WHITESPACE: switch (got) { case ' ': break; default: in.reset(); parseState_ = nextState_; break; } break; case READING_METHOD: switch (got) { case ' ': parseState_ = StartLineParseState.EATING_WHITESPACE; nextState_ = StartLineParseState.READING_PATH; break; default: httpMethod_.append((char) got); break; } break; case READING_PATH: switch (got) { case '\r': parseState_ = StartLineParseState.CHECKING_EOL; break; case '%': encodeTo_ = httpPath_; nextState_ = parseState_; parseState_ = StartLineParseState.DECODING_FIRST_CHAR; break; case ' ': parseState_ = StartLineParseState.EATING_WHITESPACE; nextState_ = StartLineParseState.READING_VERSION; break; case '?': parseState_ = StartLineParseState.READING_QUERY; break; default: httpPath_.append((char) got); break; } break; case READING_QUERY: switch (got) { case '\r': parseState_ = StartLineParseState.CHECKING_EOL; break; case '%': encodeTo_ = httpQuery_; nextState_ = parseState_; parseState_ = StartLineParseState.DECODING_FIRST_CHAR; break; case ' ': parseState_ = StartLineParseState.EATING_WHITESPACE; nextState_ = StartLineParseState.READING_VERSION; break; case '+': httpQuery_.append(' '); break; default: httpQuery_.append((char) got); break; } break; case DECODING_FIRST_CHAR: encodedValue_ = decodeHex(got) * 16; parseState_ = StartLineParseState.DECODING_SECOND_CHAR; break; case DECODING_SECOND_CHAR: encodeTo_.append((char) (decodeHex(got) + encodedValue_)); parseState_ = nextState_; break; case READING_VERSION: switch (got) { case '\r': parseState_ = StartLineParseState.CHECKING_EOL; break; default: httpVersion_.append((char) got); break; } break; case CHECKING_EOL: switch (got) { case '\n': finishLine_(); parseState_ = StartLineParseState.TO_RESET; return true; default: throw new HttpParsingException(); } default: throw new HttpParsingException(); } } return false; } public boolean onMoreBytesNew(ByteBuffer buffer) throws HttpParsingException, IOException { int got; int limit = buffer.limit(); int pos = buffer.position(); if (parseState_ == StartLineParseState.TO_RESET) { resetParserState(); } while(pos < limit) { switch(parseState_) { case EATING_WHITESPACE: while((char)buffer.get(pos) == ' ' && ++pos < limit); if(pos < limit) parseState_ = nextState_; break; case READING_METHOD: while(pos < limit && (got = buffer.get(pos)) != ' ') { httpMethod_.append((char)got); pos++; } if(pos < limit) { parseState_ = StartLineParseState.EATING_WHITESPACE; nextState_ = StartLineParseState.READING_PATH; } break; case READING_PATH: while(pos < limit && parseState_ == StartLineParseState.READING_PATH) { got = buffer.get(pos++); switch (got) { case '\r': parseState_ = StartLineParseState.CHECKING_EOL; break; case '%': encodeTo_ = httpPath_; nextState_ = parseState_; parseState_ = StartLineParseState.DECODING_FIRST_CHAR; break; case ' ': parseState_ = StartLineParseState.EATING_WHITESPACE; nextState_ = StartLineParseState.READING_VERSION; break; case '?': parseState_ = StartLineParseState.READING_QUERY; break; default: httpPath_.append((char) got); break; } } break; case READING_QUERY: while(pos < limit && parseState_ == StartLineParseState.READING_QUERY) { got = buffer.get(pos++); switch (got) { case '\r': parseState_ = StartLineParseState.CHECKING_EOL; break; case '%': encodeTo_ = httpQuery_; nextState_ = parseState_; parseState_ = StartLineParseState.DECODING_FIRST_CHAR; break; case ' ': parseState_ = StartLineParseState.EATING_WHITESPACE; nextState_ = StartLineParseState.READING_VERSION; break; case '+': httpQuery_.append(' '); break; default: httpQuery_.append((char) got); break; } } break; case DECODING_FIRST_CHAR: got = (int)buffer.get(pos++); encodedValue_ = decodeHex(got) * 16; parseState_ = StartLineParseState.DECODING_SECOND_CHAR; break; case DECODING_SECOND_CHAR: got = (int)buffer.get(pos++); encodeTo_.append((char) (decodeHex(got) + encodedValue_)); parseState_ = nextState_; break; case READING_VERSION: while(pos < limit && (got = buffer.get(pos)) != '\r' ) { httpVersion_.append((char)got); pos++; } if(pos < limit) { parseState_ = StartLineParseState.CHECKING_EOL; pos++; // skipping '\r' } break; case CHECKING_EOL: switch (buffer.get(pos++)) { case '\n': finishLine_(); parseState_ = StartLineParseState.TO_RESET; buffer.position(pos); return true; //could have reached limit here default: throw new HttpParsingException(); } default: throw new HttpParsingException(); } } buffer.position(pos); return false; } }