| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665 |
- /*
- * HappyHTTP - a simple HTTP library
- * Version 0.1
- *
- * Copyright (c) 2006 Ben Campbell
- *
- * This software is provided 'as-is', without any express or implied
- * warranty. In no event will the authors be held liable for any damages
- * arising from the use of this software.
- *
- * Permission is granted to anyone to use this software for any purpose,
- * including commercial applications, and to alter it and redistribute it
- * freely, subject to the following restrictions:
- *
- * 1. The origin of this software must not be misrepresented; you must not
- * claim that you wrote the original software. If you use this software in a
- * product, an acknowledgment in the product documentation would be
- * appreciated but is not required.
- *
- * 2. Altered source versions must be plainly marked as such, and must not
- * be misrepresented as being the original software.
- *
- * 3. This notice may not be removed or altered from any source distribution.
- *
- *
- *Ported to SquiLu http://code.google.com/p/squilu/ by Domingo Alvarez Duarte
- */
-
- // HTTP status codes
- enum HTTP_status_code {
- // 1xx informational
- CONTINUE = 100,
- SWITCHING_PROTOCOLS = 101,
- PROCESSING = 102,
- // 2xx successful
- OK = 200,
- CREATED = 201,
- ACCEPTED = 202,
- NON_AUTHORITATIVE_INFORMATION = 203,
- NO_CONTENT = 204,
- RESET_CONTENT = 205,
- PARTIAL_CONTENT = 206,
- MULTI_STATUS = 207,
- IM_USED = 226,
- // 3xx redirection
- MULTIPLE_CHOICES = 300,
- MOVED_PERMANENTLY = 301,
- FOUND = 302,
- SEE_OTHER = 303,
- NOT_MODIFIED = 304,
- USE_PROXY = 305,
- TEMPORARY_REDIRECT = 307,
- // 4xx client error
- BAD_REQUEST = 400,
- UNAUTHORIZED = 401,
- PAYMENT_REQUIRED = 402,
- FORBIDDEN = 403,
- NOT_FOUND = 404,
- METHOD_NOT_ALLOWED = 405,
- NOT_ACCEPTABLE = 406,
- PROXY_AUTHENTICATION_REQUIRED = 407,
- REQUEST_TIMEOUT = 408,
- CONFLICT = 409,
- GONE = 410,
- LENGTH_REQUIRED = 411,
- PRECONDITION_FAILED = 412,
- REQUEST_ENTITY_TOO_LARGE = 413,
- REQUEST_URI_TOO_LONG = 414,
- UNSUPPORTED_MEDIA_TYPE = 415,
- REQUESTED_RANGE_NOT_SATISFIABLE = 416,
- EXPECTATION_FAILED = 417,
- UNPROCESSABLE_ENTITY = 422,
- LOCKED = 423,
- FAILED_DEPENDENCY = 424,
- UPGRADE_REQUIRED = 426,
- // 5xx server error
- INTERNAL_SERVER_ERROR = 500,
- NOT_IMPLEMENTED = 501,
- BAD_GATEWAY = 502,
- SERVICE_UNAVAILABLE = 503,
- GATEWAY_TIMEOUT = 504,
- HTTP_VERSION_NOT_SUPPORTED = 505,
- INSUFFICIENT_STORAGE = 507,
- NOT_EXTENDED = 510,
- };
- //-------------------------------------------------
- // Connection
- //
- // Handles the socket connection, issuing of requests and managing
- // responses.
- // ------------------------------------------------
- enum Connection_State { IDLE, REQ_STARTED, REQ_SENT };
- class HappyHttpConnection {
- m_Port = null;
- m_Host = null;
- m_State = null;
- m_Sock = null;
- m_Buffer = null; // lines of request
- m_Outstanding = null; // responses for outstanding requests
-
-
- // doesn't connect immediately
- constructor(host, port){
- m_Host = host;
- m_Port = port;
- m_Buffer = [];
- m_Outstanding = [];
- m_State =Connection_State.IDLE;
- }
- // Don't need to call connect() explicitly as issuing a request will
- // call it automatically if needed.
- // But it could block (for name lookup etc), so you might prefer to
- // call it in advance.
- function connect(){
- m_Sock = socket.tcp();
- m_Sock.connect(m_Host, m_Port);
- m_Sock.settimeout(0.01);
- }
- // close connection, discarding any pending requests.
- function close(){
- m_Sock.close();
- m_Sock = null;
- m_Outstanding.clear();
- }
- // Update the connection (non-blocking)
- // Just keep calling this regularly to service outstanding requests.
- function pump(milisec=10) { //10 miliseconds to prevent high cpu load
- if( m_Outstanding.isempty() ) return; // no requests outstanding
- assert( m_Sock != null ); // outstanding requests but no connection!
- if( !datawaiting( m_Sock, milisec) ) return; // recv will block
- local rc = m_Sock.receive(8192); //2048
- local rc_status = rc[1];
- if( (rc_status == socket.IO_DONE) || rc_status == socket.IO_TIMEOUT) {
- //case socket.IO_CLOSED:
- } else {
- if(rc[0].len() == 0) throw(format("socket io error %d", rc_status));
- }
- local buf = rc[0];
- local a = buf.len();
- if( a== 0 )
- {
- // connection has closed
- local r = m_Outstanding[0];
- r->notifyconnectionclosed();
- assert( r->completed() );
- m_Outstanding.remove(0);
- // any outstanding requests will be discarded
- close();
- }
- else
- {
- local used = 0;
- while( used < a && !m_Outstanding.isempty() )
- {
- local r = m_Outstanding[0];
- local u = r->pump( buf, used, a-used );
- if(u == 0) throw("Bad data received !");
- // delete response once completed
- if( r->completed() ) m_Outstanding.remove(0);
- used += u;
- }
- // NOTE: will lose bytes if response queue goes empty
- // (but server shouldn't be sending anything if we don't have
- // anything outstanding anyway)
- assert( used == a ); // all bytes should be used up by here.
- }
- }
- // any requests still outstanding?
- function outstanding() { return m_Outstanding && !m_Outstanding.isempty(); }
- // ---------------------------
- // high-level request interface
- // ---------------------------
- // method is "GET", "POST" etc...
- // url is only path part: eg "/index.html"
- // headers is array of name/value pairs, terminated by a null-ptr
- // body & bodysize specify body data of request (eg values for a form)
- function request( method, url, headers=null, body=null, bodysize=0){
- local gotcontentlength = false; // already in headers?
- // check headers for content-length
- // TODO: check for "Host" and "Accept-Encoding" too
- // and avoid adding them ourselves in putrequest()
- if( headers ) gotcontentlength = table_rawget(headers, "content-length", false);
- putrequest( method, url );
- if( body && !gotcontentlength ) putheader( "Content-Length", bodysize );
- if( headers ) foreach(name, value in headers) putheader( name, value );
- endheaders();
- if( body ) send( body, bodysize );
- }
- // ---------------------------
- // low-level request interface
- // ---------------------------
- // begin request
- // method is "GET", "POST" etc...
- // url is only path part: eg "/index.html"
- function putrequest( method, url ){
- if( m_State != Connection_State.IDLE ) throw ("Request already issued" );
- if( !method || !url ) throw ( "Method and url can't be NULL" );
- m_State = Connection_State.REQ_STARTED;
- local req = format("%s %s HTTP/1.1", method, url);
- m_Buffer.push( req );
- putheader( "Host", m_Host ); // required for HTTP1.1
- // don't want any fancy encodings please
- putheader("Accept-Encoding", "identity");
- // Push a new response onto the queue
- local r = new HappyHttpResponse( method, url, this );
- m_Outstanding.push( r );
- }
- // Add a header to the request (call after putrequest() )
- function putheader( header, value ){
- if( m_State != Connection_State.REQ_STARTED ) throw ( "putheader() failed" );
- m_Buffer.push( format("%s: %s", header, value.tostring()) );
- }
- // Finished adding headers, issue the request.
- function endheaders(){
- if( m_State != Connection_State.REQ_STARTED ) throw "Cannot send header";
- m_State = Connection_State.IDLE;
- m_Buffer.push( "\r\n" ); //for double "\r\n\r\n"
- local msg = m_Buffer.concat("\r\n");
- m_Buffer.clear();
- send( msg , msg.len() );
- }
- // send body data if any.
- // To be called after endheaders()
- function send( buf, numbytes ){
- if( !m_Sock ) connect();
- local n = m_Sock.send( buf , 0, numbytes );
- //print(m_Sock.getfd(), n, buf);
- if(n != numbytes) throw("Could not send ");
- }
- // return true if socket has data waiting to be read
- static function datawaiting( sock , milisec)
- {
- try {
- local rc = socket.select( [sock], [], milisec ? milisec / 1000.0 : 0);
- local rar = rc[0];
- if(rar.len() == 1 && rar[0] == sock) return true;
- return false;
- }
- catch(e){
- if(e == "timeout") return false;
- else throw(e);
- }
- }
- function response_begin( r ) {}
- function response_data( r, data, data_idx, numbytes ) {}
- function response_complete( r ) {}
- };
- enum Response_state {
- STATUSLINE, // start here. status line is first line of response.
- HEADERS, // reading in header lines
- BODY, // waiting for some body data (all or a chunk)
- CHUNKLEN, // expecting a chunk length indicator (in hex)
- CHUNKEND, // got the chunk, now expecting a trailing blank line
- TRAILERS, // reading trailers after body.
- COMPLETE, // response is complete!
- };
-
- class HappyHttpResponse {
- m_Connection = null; // to access callback ptrs
- m_Method = null; // req method: "GET", "POST" etc...
- m_Url = null; // req url: /image.php?d=2...
- m_VersionString = null; // HTTP-Version
- m_Version = null; // 10: HTTP/1.0 11: HTTP/1.x (where x>=1)
- m_Status = null; // Status-Code
- m_Reason = null; // Reason-Phrase
- m_Headers = null; // header/value pairs
- m_BytesRead = null; // body bytes read so far
- m_Chunked = null; // response is chunked?
- m_ChunkLeft = null; // bytes left in current chunk
- m_Length = null; // -1 if unknown
- m_WillClose = null; // connection will close at response end?
- m_LineBuf = null; // line accumulation for states that want it
- m_HeaderAccum = null; // accumulation buffer for headers
- m_State = null;
-
- // only Connection creates Responses.
- constructor(method, url, conn){
- m_Method = method;
- m_Url = url;
- m_Connection = conn;
- m_Version = 0;
- m_Status = 0;
- m_BytesRead = 0;
- m_Chunked = false;
- m_ChunkLeft = 0;
- m_Length = -1;
- m_WillClose = false;
- m_Headers = {};
- m_HeaderAccum = "";
- m_LineBuf = "";
- m_State = Response_state.STATUSLINE;
- }
-
- // retrieve a header (returns null if not present)
- function getheader( name ){
- local lname = name.tolower();
- return table_get(m_Headers, lname, null);
- }
-
- function completed() { return m_State == Response_state.COMPLETE; }
-
- // get the HTTP status code
- function getstatus(){
- // only valid once we've got the statusline
- assert( m_State != Response_state.STATUSLINE );
- return m_Status;
- }
-
- // get the HTTP response reason string
- function getreason(){
- // only valid once we've got the statusline
- assert( m_State != Response_state.STATUSLINE );
- return m_Reason;
- }
-
- // true if connection is expected to close after this response.
- function willclose() { return m_WillClose; }
-
- function getconnection(){return m_Connection;}
- function geturl() { return m_Url; }
- function getLength() { return m_Length;}
-
- // pump some data in for processing.
- // Returns the number of bytes used.
- // Will always return 0 when response is complete.
- function pump( data, start, datasize ){
- assert( datasize != 0 );
- local count = datasize;
- local data_idx = 0;
- local last_count = 0;
-
- while( count > 0 && m_State != Response_state.COMPLETE && last_count != count)
- {
- last_count = count;
- if( m_State == Response_state.STATUSLINE ||
- m_State == Response_state.HEADERS ||
- m_State == Response_state.TRAILERS ||
- m_State == Response_state.CHUNKLEN ||
- m_State == Response_state.CHUNKEND )
- {
- // we want to accumulate a line
- local pos = data.find("\n", data_idx);
- if( pos >= 0 )
- {
- count -= pos-data_idx+1;
- local new_pos = pos;
- if(pos > 0 && data[pos-1] == '\r') --new_pos;
- m_LineBuf = data.slice(data_idx, new_pos);
- data_idx = pos+1;
- // now got a whole line!
- switch( m_State )
- {
- case Response_state.STATUSLINE:
- ProcessStatusLine( m_LineBuf );
- break;
- case Response_state.HEADERS:
- ProcessHeaderLine( m_LineBuf );
- break;
- case Response_state.TRAILERS:
- ProcessTrailerLine( m_LineBuf );
- break;
- case Response_state.CHUNKLEN:
- ProcessChunkLenLine( m_LineBuf );
- break;
- case Response_state.CHUNKEND:
- // just soak up the crlf after body and go to next state
- assert( m_Chunked == true );
- m_State = Response_state.CHUNKLEN;
- break;
- default:
- break;
- }
- m_LineBuf = "";
- }
- }
- else if( m_State == Response_state.BODY )
- {
- local bytesused = 0;
- if( m_Chunked )
- bytesused = ProcessDataChunked( data, data_idx, count );
- else
- bytesused = ProcessDataNonChunked( data, data_idx, count );
- data_idx += bytesused;
- count -= bytesused;
- }
- //os.sleep(1);
- }
- // return number of bytes used
- return datasize - count;
- }
-
- // tell response that connection has closed
- function notifyconnectionclosed(){
- if( m_State == Response_state.COMPLETE ) return;
- // eof can be valid...
- if( m_State == Response_state.BODY &&
- !m_Chunked && m_Length <= 0 ) Finish(); // we're all done!
- else throw ( "Connection closed unexpectedly" );
- }
-
- function FlushHeader(){
- if( m_HeaderAccum.isempty() ) return; // no flushing required
- local rc = m_HeaderAccum.match("([^:]+):%s*(.+)");
- if(!rc) throw(format("Invalid header (%s)", m_HeaderAccum));
- m_Headers[ rc[0].tolower() ] <- rc[1];
- m_HeaderAccum = "";
- }
- function ProcessStatusLine( line ){
- //HTTP/1.1 200 OK
- local rc = line.match("%s*(%S+)%s+(%d+)%s+(.+)");
- if(!rc) throw(format("BadStatusLine (%s)", line));
- m_VersionString = rc[0];
- m_Status = rc[1].tointeger();
- m_Reason = rc[2];
- if( m_Status < 100 || m_Status > 999 ) throw ( format("BadStatusLine (%s)", line) );
- if( m_VersionString == "HTTP:/1.0" ) m_Version = 10;
- else if( m_VersionString.startswith( "HTTP/1." ) ) m_Version = 11;
- else throw ( format("UnknownProtocol (%s)", m_VersionString) );
- // TODO: support for HTTP/0.9
- // OK, now we expect headers!
- m_State = Response_state.HEADERS;
- m_HeaderAccum = "";
- }
-
- function ProcessHeaderLine( line ){
- if( line.isempty() )
- {
- FlushHeader();
- // end of headers
- // HTTP code 100 handling (we ignore 'em)
- if( m_Status == HTTP_status_code.CONTINUE )
- m_State = Response_state.STATUSLINE; // reset parsing, expect new status line
- else
- BeginBody(); // start on body now!
- return;
- }
- if( line[0] == ' ' )
- {
- // it's a continuation line - just add it to previous data
- m_HeaderAccum += line;
- }
- else
- {
- // begin a new header
- FlushHeader();
- m_HeaderAccum = line;
- }
- }
-
- function ProcessTrailerLine(line){
- // TODO: handle trailers?
- // (python httplib doesn't seem to!)
- if( line.isempty() ) Finish();
- // just ignore all the trailers...
- }
-
- function ProcessChunkLenLine( line ){
- // chunklen in hex at beginning of line
- m_ChunkLeft = line.tointeger(16);
- if( m_ChunkLeft == 0 )
- {
- // got the whole body, now check for trailing headers
- m_State = Response_state.TRAILERS;
- m_HeaderAccum = "";
- }
- else
- {
- m_State = Response_state.BODY;
- }
- }
-
- function ProcessDataChunked( data, data_idx, count ){
- assert( m_Chunked );
- local n = count;
- if( n>m_ChunkLeft ) n = m_ChunkLeft;
- // invoke callback to pass out the data
- m_Connection.response_data( this, data, data_idx, n );
- m_BytesRead += n;
- m_ChunkLeft -= n;
- assert( m_ChunkLeft >= 0);
- if( m_ChunkLeft == 0 )
- {
- // chunk completed! now soak up the trailing CRLF before next chunk
- m_State = Response_state.CHUNKEND;
- }
- return n;
- }
-
- function ProcessDataNonChunked( data, data_idx, count ){
- local n = count;
- if( m_Length != -1 )
- {
- // we know how many bytes to expect
- local remaining = m_Length - m_BytesRead;
- if( n > remaining ) n = remaining;
- }
- // invoke callback to pass out the data
- m_Connection.response_data( this, data, data_idx, n );
- m_BytesRead += n;
- // Finish if we know we're done. Else we're waiting for connection close.
- if( m_Length != -1 && m_BytesRead == m_Length ) Finish();
- return n;
- }
-
- // OK, we've now got all the headers read in, so we're ready to start
- // on the body. But we need to see what info we can glean from the headers
- // first...
- function BeginBody(){
- m_Chunked = false;
- m_Length = -1; // unknown
- m_WillClose = false;
- // using chunked encoding?
- local trenc = getheader( "transfer-encoding" );
- if( trenc && trenc == "chunked" )
- {
- m_Chunked = true;
- m_ChunkLeft = -1; // unknown
- }
- m_WillClose = CheckClose();
- // length supplied?
- local contentlen = getheader( "content-length" );
- if( contentlen && !m_Chunked )
- {
- m_Length = contentlen.tointeger();
- }
- // check for various cases where we expect zero-length body
- if( m_Status == HTTP_status_code.NO_CONTENT ||
- m_Status == HTTP_status_code.NOT_MODIFIED ||
- ( m_Status >= 100 && m_Status < 200 ) || // 1xx codes have no body
- m_Method == "HEAD" )
- {
- m_Length = 0;
- }
- // if we're not using chunked mode, and no length has been specified,
- // assume connection will close at end.
- if( !m_WillClose && !m_Chunked && m_Length == -1 ) m_WillClose = true;
- // Invoke the user callback, if any
- m_Connection.response_begin( this );
- /*
- printf("---------BeginBody()--------\n");
- printf("Length: %d\n", m_Length );
- printf("WillClose: %d\n", (int)m_WillClose );
- printf("Chunked: %d\n", (int)m_Chunked );
- printf("ChunkLeft: %d\n", (int)m_ChunkLeft );
- printf("----------------------------\n");
- */
- // now start reading body data!
- if( m_Chunked ) m_State = Response_state.CHUNKLEN;
- else m_State = Response_state.BODY;
- }
-
- function CheckClose(){
- if( m_Version == 11 )
- {
- // HTTP1.1
- // the connection stays open unless "connection: close" is specified.
- local conn = getheader( "connection" );
- if( conn && conn == "close" ) return true;
- else return false;
- }
- // Older HTTP
- // keep-alive header indicates persistant connection
- if( getheader( "keep-alive" ) ) return false;
- // TODO: some special case handling for Akamai and netscape maybe?
- // (see _check_close() in python httplib.py for details)
- return true;
- }
-
- function Finish(){
- m_State = Response_state.COMPLETE;
-
- // invoke the callback
- m_Connection.response_complete( this );
- }
- }
- /*
- class MyHappyHttpConnection extends HappyHttpConnection {
- count = null;
-
- constructor(host, port){
- base.constructor(host, port);
- count = 0;
- }
- function response_begin( r ) {
- printf( "BEGIN (%d %s)\n", r->getstatus(), r->getreason() );
- count = 0;
- }
- function response_data( r, data, data_idx, numbytes ) {
- print( data );
- count += numbytes;
- }
- function response_complete( r ) {
- printf( "COMPLETE (%d bytes)\n", count );
- }
- }
- function test_happyhttp(){
- local start = os.getmillicount();
- local conn = MyHappyHttpConnection( "www.scumways.com", 80);
- conn.request( "GET", "/happyhttp/test.php" );
- while( conn.outstanding() ) conn.pump();
- print(format("Took : %d ms", os.getmillispan(start)));
- }
- test_happyhttp();
- */
|