diff --git a/third_party/happyhttp/src/Makefile b/third_party/happyhttp/src/Makefile new file mode 100644 index 0000000000..98d3d0f90b --- /dev/null +++ b/third_party/happyhttp/src/Makefile @@ -0,0 +1,40 @@ +# to build for windows, use make WIN32=1 + +EXE = test +SRCS = test.cpp happyhttp.cpp + +CXXFLAGS = -ggdb +LDFLAGS = + +ifdef WIN32 +# need to link with winsock2 +LDFLAGS += -lws2_32 +EXE = test.exe +endif + +OBJS = $(patsubst %.cpp,%.o,$(SRCS) ) + + +# ------------- + + +$(EXE): $(OBJS) + $(CXX) -o $@ $(OBJS) $(LDFLAGS) + + +clean: + rm -f $(OBJS) + rm -f .depend + + +# automatically generate dependencies from our .cpp files +# (-MM tells the compiler to just output makefile rules) +depend: + $(CXX) -MM $(CPPFLAGS) $(CXXFLAGS) $(SRCS) > .depend + +ifeq ($(wildcard .depend),.depend) +include .depend +endif + + + diff --git a/third_party/happyhttp/src/README.md b/third_party/happyhttp/src/README.md new file mode 100644 index 0000000000..ea4ecc0f60 --- /dev/null +++ b/third_party/happyhttp/src/README.md @@ -0,0 +1,224 @@ +HappyHTTP +========= + +*a simple HTTP library* + + Fork of [http://www.scumways.com/happyhttp/happyhttp.html](http://www.scumways.com/happyhttp/happyhttp.html) + +Contents +-------- + +- [Overview](#overview) +- [Download](#download) +- [Usage](#usage) +- [Example](#example) +- [TODO](#todo) +- [License](#license) + +* * * * * + +Overview +-------- + +HappyHTTP is a simple C++ library for issuing HTTP requests and +processing responses. + +- Simple to integrate - just drop in the [.h](happyhttp.h) and + [.cpp](happyhttp.cpp) files +- Easy-to-use interface ([example](#example)) +- Non-blocking operation, suitable for use in game update loops +- Supports pipelining. You can issue multiple requests without waiting + for responses. +- Licensed under the [zlib/libpng + license](http://www.opensource.org/licenses/zlib-license.php). +- Cross-platform (Linux, OSX, Windows) + +* * * * * + +Download +-------- + +Latest Version is 0.1: +[happyhttp-0.1.tar.gz](http://www.scumways.com/happyhttp/happyhttp-0.1.tar.gz) + +* * * * * + +Usage +----- + +The interface is based loosely on Python's +[httplib](http://docs.python.org/lib/module-httplib.html). + +All HappyHTTP code is kept within the `happyhttp::` namespace + +To issue and process a HTTP request, the basic steps are: + +1. Create a connection object +2. Set up callbacks for handling responses +3. Issue request(s) +4. 'pump' the connection at regular intervals. As responses are + received, the callbacks will be invoked. + +### Connection object methods + +**`Connection( const char* host, int port )`** + Constructor. Specifies host and port, but connection isn't made until +request is issued or connect() is called. + +**`~Connection()`** + Destructor. If connection is open, it'll be closed, and any outstanding +requests will be discarded. + +**`void setcallbacks( ResponseBegin_CB begincb, ResponseData_CB datacb, ResponseComplete_CB completecb, void* userdata )`** + Set up the response handling callbacks. These will be invoked during +calls to pump(). + `begincb` - called when the responses headers have been received\ + `datacb` - called repeatedly to handle body data + `completecb` - response is completed + `userdata` - passed as a param to all callbacks. + +**`void connect()`** + 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. + +**`void close()`** + // close connection, discarding any pending requests. + +**`void pump()`** + Update the connection (non-blocking) Just keep calling this regularly +to service outstanding requests. As responses are received, the +callbacks will be invoked. + +**`bool outstanding() const`** + Returns true if any requests are still outstanding. + +**`void request( const char* method, const char* url, const char* headers[], const unsigned char* body, int bodysize )`** + High-level request interface. Issues a request. + `method` - "GET", "POST" etc... + `url` - eg "/index.html" + `headers` - array of name/value pairs, terminated by a null-ptr + `body, bodysize` - specify body data of request (eg values for a form) + +**`void putrequest( const char* method, const char* url )`** + (part of low-level request interface) + Begin a request + method is "GET", "POST" etc... + url is only path part: eg "/index.html" + +**`void putheader( const char* header, const char* value )`** + **`void putheader( const char* header, int numericvalue )`** + (part of low-level request interface) + Add a header to the request (call after putrequest() ) + +**`void endheaders()`** + (part of low-level request interface) + Indicate that your are finished adding headers and the request can be +issued. + +`void send( const unsigned char* buf, int numbytes )` + (part of low-level request interface) + send body data if any. To be called after endheaders() + +### Callback types + +**`typedef void (*ResponseBegin_CB)( const Response* r, void* userdata )`** + Invoked when all the headers for a response have been received.\ + The Response object can be queried to determine status and header +values. + `userdata` is the same value that was passed in to +`Connection::setcallbacks()`. + +**`typedef void (*ResponseData_CB)( const Response* r, void* userdata, const unsigned char* data, int numbytes )`** + This callback is invoked to pass out data from the body of the +response. It may be called multiple times, or not at all (if there is no +body). + +**`typedef void (*ResponseComplete_CB)( const Response* r, void* userdata )`** + Once a response is completed, this callback is invoked. When the +callback returns, the respsonse object will be destroyed. + +### Response object methods + +When a callback is invoked, a response object is passed to it. The +following member functions can be used to query the response: + +**`const char* getheader( const char* name ) const`** + retrieve the value of a header (returns 0 if not present) + +**`int getstatus() const`** + Get the HTTP status code returned by the server + +**`const char* getreason() const`** + Get the HTTP response reason string returned by the server + +### Error Handling + +If an error occurs, a `Wobbly` is thrown. The `Wobbly::what()` method +returns a text description. + +* * * * * + +Example +------- + +For more examples, see [test.cpp](test.cpp). + + + static int count=0; + + // invoked when response headers have been received + void OnBegin( const happyhttp::Response* r, void* userdata ) + { + printf( "BEGIN (%d %s)\n", r->getstatus(), r->getreason() ); + count = 0; + } + + // invoked to process response body data (may be called multiple times) + void OnData( const happyhttp::Response* r, void* userdata, const unsigned char* data, int n ) + { + fwrite( data,1,n, stdout ); + count += n; + } + + // invoked when response is complete + void OnComplete( const happyhttp::Response* r, void* userdata ) + { + printf( "COMPLETE (%d bytes)\n", count ); + } + + + void TestGET() + { + happyhttp::Connection conn( "www.scumways.com", 80 ); + conn.setcallbacks( OnBegin, OnData, OnComplete, 0 ); + + conn.request( "GET", "/happyhttp/test.php" ); + + while( conn.outstanding() ) + conn.pump(); + } + +* * * * * + +TODO +---- + +- Proxy support +- Add helper functions for URL wrangling +- Improve error text (and maybe some more exception types?) +- HTTP 0.9 support +- Improved documentation and examples + +* * * * * + +License +------- + +HappyHTTP is licensed under the [zlib/libpng +license](http://www.opensource.org/licenses/zlib-license.php). + +You are free to use this library however you wish, but if you +make changes, please send a patch! + +If you use it, let us know. diff --git a/third_party/happyhttp/src/happyhttp.cpp b/third_party/happyhttp/src/happyhttp.cpp new file mode 100644 index 0000000000..9e566735f6 --- /dev/null +++ b/third_party/happyhttp/src/happyhttp.cpp @@ -0,0 +1,938 @@ +/* + * 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. + * + */ + + +#include "happyhttp.h" + +#ifndef _WIN32 +// #include + #include + #include + #include + #include // for gethostbyname() + #include +#else + #include + #define vsnprintf _vsnprintf +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifdef __APPLE__ + #define _stricmp strcasecmp +#endif + + +using namespace std; + + +namespace happyhttp +{ + +#ifdef WIN32 +const char* GetWinsockErrorString( int err ); +#endif + + +//--------------------------------------------------------------------- +// Helper functions +//--------------------------------------------------------------------- + + + +void BailOnSocketError( const char* context ) +{ +#ifdef WIN32 + + int e = WSAGetLastError(); + const char* msg = GetWinsockErrorString( e ); +#else + const char* msg = strerror( errno ); +#endif + throw Wobbly( "%s: %s", context, msg ); +} + + +#ifdef WIN32 + +const char* GetWinsockErrorString( int err ) +{ + switch( err) + { + case 0: return "No error"; + case WSAEINTR: return "Interrupted system call"; + case WSAEBADF: return "Bad file number"; + case WSAEACCES: return "Permission denied"; + case WSAEFAULT: return "Bad address"; + case WSAEINVAL: return "Invalid argument"; + case WSAEMFILE: return "Too many open sockets"; + case WSAEWOULDBLOCK: return "Operation would block"; + case WSAEINPROGRESS: return "Operation now in progress"; + case WSAEALREADY: return "Operation already in progress"; + case WSAENOTSOCK: return "Socket operation on non-socket"; + case WSAEDESTADDRREQ: return "Destination address required"; + case WSAEMSGSIZE: return "Message too long"; + case WSAEPROTOTYPE: return "Protocol wrong type for socket"; + case WSAENOPROTOOPT: return "Bad protocol option"; + case WSAEPROTONOSUPPORT: return "Protocol not supported"; + case WSAESOCKTNOSUPPORT: return "Socket type not supported"; + case WSAEOPNOTSUPP: return "Operation not supported on socket"; + case WSAEPFNOSUPPORT: return "Protocol family not supported"; + case WSAEAFNOSUPPORT: return "Address family not supported"; + case WSAEADDRINUSE: return "Address already in use"; + case WSAEADDRNOTAVAIL: return "Can't assign requested address"; + case WSAENETDOWN: return "Network is down"; + case WSAENETUNREACH: return "Network is unreachable"; + case WSAENETRESET: return "Net connection reset"; + case WSAECONNABORTED: return "Software caused connection abort"; + case WSAECONNRESET: return "Connection reset by peer"; + case WSAENOBUFS: return "No buffer space available"; + case WSAEISCONN: return "Socket is already connected"; + case WSAENOTCONN: return "Socket is not connected"; + case WSAESHUTDOWN: return "Can't send after socket shutdown"; + case WSAETOOMANYREFS: return "Too many references, can't splice"; + case WSAETIMEDOUT: return "Connection timed out"; + case WSAECONNREFUSED: return "Connection refused"; + case WSAELOOP: return "Too many levels of symbolic links"; + case WSAENAMETOOLONG: return "File name too long"; + case WSAEHOSTDOWN: return "Host is down"; + case WSAEHOSTUNREACH: return "No route to host"; + case WSAENOTEMPTY: return "Directory not empty"; + case WSAEPROCLIM: return "Too many processes"; + case WSAEUSERS: return "Too many users"; + case WSAEDQUOT: return "Disc quota exceeded"; + case WSAESTALE: return "Stale NFS file handle"; + case WSAEREMOTE: return "Too many levels of remote in path"; + case WSASYSNOTREADY: return "Network system is unavailable"; + case WSAVERNOTSUPPORTED: return "Winsock version out of range"; + case WSANOTINITIALISED: return "WSAStartup not yet called"; + case WSAEDISCON: return "Graceful shutdown in progress"; + case WSAHOST_NOT_FOUND: return "Host not found"; + case WSANO_DATA: return "No host data of that type was found"; + } + + return "unknown"; +}; + +#endif // WIN32 + + +// return true if socket has data waiting to be read +bool datawaiting( int sock ) +{ + fd_set fds; + FD_ZERO( &fds ); + FD_SET( sock, &fds ); + + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 0; + + int r = select( sock+1, &fds, NULL, NULL, &tv); + if (r < 0) + BailOnSocketError( "select" ); + + if( FD_ISSET( sock, &fds ) ) + return true; + else + return false; +} + + +// Try to work out address from string +// returns 0 if bad +struct in_addr *atoaddr( const char* address) +{ + struct hostent *host; + static struct in_addr saddr; + + // First try nnn.nnn.nnn.nnn form + saddr.s_addr = inet_addr(address); + if (saddr.s_addr != -1) + return &saddr; + + host = gethostbyname(address); + if( host ) + return (struct in_addr *) *host->h_addr_list; + + return 0; +} + + + + + + + +//--------------------------------------------------------------------- +// +// Exception class +// +//--------------------------------------------------------------------- + + +Wobbly::Wobbly( const char* fmt, ... ) +{ + va_list ap; + va_start( ap,fmt); + int n = vsnprintf( m_Message, MAXLEN, fmt, ap ); + va_end( ap ); + if(n==MAXLEN) + m_Message[MAXLEN-1] = '\0'; +} + + + + + + + + +//--------------------------------------------------------------------- +// +// Connection +// +//--------------------------------------------------------------------- +Connection::Connection( const char* host, int port ) : + m_ResponseBeginCB(0), + m_ResponseDataCB(0), + m_ResponseCompleteCB(0), + m_UserData(0), + m_State( IDLE ), + m_Host( host ), + m_Port( port ), + m_Sock(-1) +{ +} + + +void Connection::setcallbacks( + ResponseBegin_CB begincb, + ResponseData_CB datacb, + ResponseComplete_CB completecb, + void* userdata ) +{ + m_ResponseBeginCB = begincb; + m_ResponseDataCB = datacb; + m_ResponseCompleteCB = completecb; + m_UserData = userdata; +} + + +void Connection::connect() +{ + in_addr* addr = atoaddr( m_Host.c_str() ); + if( !addr ) + throw Wobbly( "Invalid network address" ); + + sockaddr_in address; + memset( (char*)&address, 0, sizeof(address) ); + address.sin_family = AF_INET; + address.sin_port = htons( m_Port ); + address.sin_addr.s_addr = addr->s_addr; + + m_Sock = socket( AF_INET, SOCK_STREAM, 0 ); + if( m_Sock < 0 ) + BailOnSocketError( "socket()" ); + +// printf("Connecting to %s on port %d.\n",inet_ntoa(*addr), port); + + if( ::connect( m_Sock, (sockaddr const*)&address, sizeof(address) ) < 0 ) + BailOnSocketError( "connect()" ); +} + + +void Connection::close() +{ +#ifdef WIN32 + if( m_Sock >= 0 ) + ::closesocket( m_Sock ); +#else + if( m_Sock >= 0 ) + ::close( m_Sock ); +#endif + m_Sock = -1; + + // discard any incomplete responses + while( !m_Outstanding.empty() ) + { + delete m_Outstanding.front(); + m_Outstanding.pop_front(); + } +} + + +Connection::~Connection() +{ + close(); +} + +void Connection::request( const char* method, + const char* url, + const char* headers[], + const unsigned char* body, + int bodysize ) +{ + + bool 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 ) + { + const char** h = headers; + while( *h ) + { + const char* name = *h++; + const char* value = *h++; + assert( value != 0 ); // name with no value! + + if( 0==_stricmp( name, "content-length" ) ) + gotcontentlength = true; + } + } + + putrequest( method, url ); + + if( body && !gotcontentlength ) + putheader( "Content-Length", bodysize ); + + if( headers ) + { + const char** h = headers; + while( *h ) + { + const char* name = *h++; + const char* value = *h++; + putheader( name, value ); + } + } + endheaders(); + + if( body ) + send( body, bodysize ); + +} + + + + +void Connection::putrequest( const char* method, const char* url ) +{ + if( m_State != IDLE ) + throw Wobbly( "Request already issued" ); + + m_State = REQ_STARTED; + + char req[ 512 ]; + sprintf( req, "%s %s HTTP/1.1", method, url ); + m_Buffer.push_back( req ); + + putheader( "Host", m_Host.c_str() ); // required for HTTP1.1 + + // don't want any fancy encodings please + putheader("Accept-Encoding", "identity"); + + // Push a new response onto the queue + Response *r = new Response( method, *this ); + m_Outstanding.push_back( r ); +} + + +void Connection::putheader( const char* header, const char* value ) +{ + if( m_State != REQ_STARTED ) + throw Wobbly( "putheader() failed" ); + m_Buffer.push_back( string(header) + ": " + string( value ) ); +} + +void Connection::putheader( const char* header, int numericvalue ) +{ + char buf[32]; + sprintf( buf, "%d", numericvalue ); + putheader( header, buf ); +} + +void Connection::endheaders() +{ + if( m_State != REQ_STARTED ) + throw Wobbly( "Cannot send header" ); + m_State = IDLE; + + m_Buffer.push_back( "" ); + + string msg; + vector< string>::const_iterator it; + for( it = m_Buffer.begin(); it != m_Buffer.end(); ++it ) + msg += (*it) + "\r\n"; + + m_Buffer.clear(); + +// printf( "%s", msg.c_str() ); + send( (const unsigned char*)msg.c_str(), msg.size() ); +} + + + +void Connection::send( const unsigned char* buf, int numbytes ) +{ +// fwrite( buf, 1,numbytes, stdout ); + + if( m_Sock < 0 ) + connect(); + + while( numbytes > 0 ) + { +#ifdef WIN32 + int n = ::send( m_Sock, (const char*)buf, numbytes, 0 ); +#else + int n = ::send( m_Sock, buf, numbytes, 0 ); +#endif + if( n<0 ) + BailOnSocketError( "send()" ); + numbytes -= n; + buf += n; + } +} + + +void Connection::pump() +{ + if( m_Outstanding.empty() ) + return; // no requests outstanding + + assert( m_Sock >0 ); // outstanding requests but no connection! + + if( !datawaiting( m_Sock ) ) + return; // recv will block + + unsigned char buf[ 2048 ]; + int a = recv( m_Sock, (char*)buf, sizeof(buf), 0 ); + if( a<0 ) + BailOnSocketError( "recv()" ); + + if( a== 0 ) + { + // connection has closed + + Response* r = m_Outstanding.front(); + r->notifyconnectionclosed(); + assert( r->completed() ); + delete r; + m_Outstanding.pop_front(); + + // any outstanding requests will be discarded + close(); + } + else + { + int used = 0; + while( used < a && !m_Outstanding.empty() ) + { + + Response* r = m_Outstanding.front(); + int u = r->pump( &buf[used], a-used ); + + // delete response once completed + if( r->completed() ) + { + delete r; + m_Outstanding.pop_front(); + } + 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. + } +} + + + + + + +//--------------------------------------------------------------------- +// +// Response +// +//--------------------------------------------------------------------- + + +Response::Response( const char* method, Connection& conn ) : + m_Connection( conn ), + m_State( STATUSLINE ), + m_Method( method ), + m_Version( 0 ), + m_Status(0), + m_BytesRead(0), + m_Chunked(false), + m_ChunkLeft(0), + m_Length(-1), + m_WillClose(false) +{ +} + + +const char* Response::getheader( const char* name ) const +{ + std::string lname( name ); +#ifdef _MSC_VER + std::transform( lname.begin(), lname.end(), lname.begin(), tolower ); +#else + std::transform( lname.begin(), lname.end(), lname.begin(), ::tolower ); +#endif + + std::map< std::string, std::string >::const_iterator it = m_Headers.find( lname ); + if( it == m_Headers.end() ) + return 0; + else + return it->second.c_str(); +} + + +int Response::getstatus() const +{ + // only valid once we've got the statusline + assert( m_State != STATUSLINE ); + return m_Status; +} + + +const char* Response::getreason() const +{ + // only valid once we've got the statusline + assert( m_State != STATUSLINE ); + return m_Reason.c_str(); +} + + + +// Connection has closed +void Response::notifyconnectionclosed() +{ + if( m_State == COMPLETE ) + return; + + // eof can be valid... + if( m_State == BODY && + !m_Chunked && + m_Length == -1 ) + { + Finish(); // we're all done! + } + else + { + throw Wobbly( "Connection closed unexpectedly" ); + } +} + + + +int Response::pump( const unsigned char* data, int datasize ) +{ + assert( datasize != 0 ); + int count = datasize; + + while( count > 0 && m_State != COMPLETE ) + { + if( m_State == STATUSLINE || + m_State == HEADERS || + m_State == TRAILERS || + m_State == CHUNKLEN || + m_State == CHUNKEND ) + { + // we want to accumulate a line + while( count > 0 ) + { + char c = (char)*data++; + --count; + if( c == '\n' ) + { + // now got a whole line! + switch( m_State ) + { + case STATUSLINE: + ProcessStatusLine( m_LineBuf ); + break; + case HEADERS: + ProcessHeaderLine( m_LineBuf ); + break; + case TRAILERS: + ProcessTrailerLine( m_LineBuf ); + break; + case CHUNKLEN: + ProcessChunkLenLine( m_LineBuf ); + break; + case CHUNKEND: + // just soak up the crlf after body and go to next state + assert( m_Chunked == true ); + m_State = CHUNKLEN; + break; + default: + break; + } + m_LineBuf.clear(); + break; // break out of line accumulation! + } + else + { + if( c != '\r' ) // just ignore CR + m_LineBuf += c; + } + } + } + else if( m_State == BODY ) + { + int bytesused = 0; + if( m_Chunked ) + bytesused = ProcessDataChunked( data, count ); + else + bytesused = ProcessDataNonChunked( data, count ); + data += bytesused; + count -= bytesused; + } + } + + // return number of bytes used + return datasize - count; +} + + + +void Response::ProcessChunkLenLine( std::string const& line ) +{ + // chunklen in hex at beginning of line + m_ChunkLeft = strtol( line.c_str(), NULL, 16 ); + + if( m_ChunkLeft == 0 ) + { + // got the whole body, now check for trailing headers + m_State = TRAILERS; + m_HeaderAccum.clear(); + } + else + { + m_State = BODY; + } +} + + +// handle some body data in chunked mode +// returns number of bytes used. +int Response::ProcessDataChunked( const unsigned char* data, int count ) +{ + assert( m_Chunked ); + + int n = count; + if( n>m_ChunkLeft ) + n = m_ChunkLeft; + + // invoke callback to pass out the data + if( m_Connection.m_ResponseDataCB ) + (m_Connection.m_ResponseDataCB)( this, m_Connection.m_UserData, data, 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 = CHUNKEND; + } + return n; +} + +// handle some body data in non-chunked mode. +// returns number of bytes used. +int Response::ProcessDataNonChunked( const unsigned char* data, int count ) +{ + int n = count; + if( m_Length != -1 ) + { + // we know how many bytes to expect + int remaining = m_Length - m_BytesRead; + if( n > remaining ) + n = remaining; + } + + // invoke callback to pass out the data + if( m_Connection.m_ResponseDataCB ) + (m_Connection.m_ResponseDataCB)( this, m_Connection.m_UserData, data, 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; +} + + +void Response::Finish() +{ + m_State = COMPLETE; + + // invoke the callback + if( m_Connection.m_ResponseCompleteCB ) + (m_Connection.m_ResponseCompleteCB)( this, m_Connection.m_UserData ); +} + + +void Response::ProcessStatusLine( std::string const& line ) +{ + const char* p = line.c_str(); + + // skip any leading space + while( *p && *p == ' ' ) + ++p; + + // get version + while( *p && *p != ' ' ) + m_VersionString += *p++; + while( *p && *p == ' ' ) + ++p; + + // get status code + std::string status; + while( *p && *p != ' ' ) + status += *p++; + while( *p && *p == ' ' ) + ++p; + + // rest of line is reason + while( *p ) + m_Reason += *p++; + + m_Status = atoi( status.c_str() ); + if( m_Status < 100 || m_Status > 999 ) + throw Wobbly( "BadStatusLine (%s)", line.c_str() ); + +/* + printf( "version: '%s'\n", m_VersionString.c_str() ); + printf( "status: '%d'\n", m_Status ); + printf( "reason: '%s'\n", m_Reason.c_str() ); +*/ + + if( m_VersionString == "HTTP:/1.0" ) + m_Version = 10; + else if( 0==m_VersionString.compare( 0,7,"HTTP/1." ) ) + m_Version = 11; + else + throw Wobbly( "UnknownProtocol (%s)", m_VersionString.c_str() ); + // TODO: support for HTTP/0.9 + + + // OK, now we expect headers! + m_State = HEADERS; + m_HeaderAccum.clear(); +} + + +// process accumulated header data +void Response::FlushHeader() +{ + if( m_HeaderAccum.empty() ) + return; // no flushing required + + const char* p = m_HeaderAccum.c_str(); + + std::string header; + std::string value; + while( *p && *p != ':' ) + header += tolower( *p++ ); + + // skip ':' + if( *p ) + ++p; + + // skip space + while( *p && (*p ==' ' || *p=='\t') ) + ++p; + + value = p; // rest of line is value + + m_Headers[ header ] = value; +// printf("header: ['%s': '%s']\n", header.c_str(), value.c_str() ); + + m_HeaderAccum.clear(); +} + + +void Response::ProcessHeaderLine( std::string const& line ) +{ + const char* p = line.c_str(); + if( line.empty() ) + { + FlushHeader(); + // end of headers + + // HTTP code 100 handling (we ignore 'em) + if( m_Status == CONTINUE ) + m_State = STATUSLINE; // reset parsing, expect new status line + else + BeginBody(); // start on body now! + return; + } + + if( isspace(*p) ) + { + // it's a continuation line - just add it to previous data + ++p; + while( *p && isspace( *p ) ) + ++p; + + m_HeaderAccum += ' '; + m_HeaderAccum += p; + } + else + { + // begin a new header + FlushHeader(); + m_HeaderAccum = p; + } +} + + +void Response::ProcessTrailerLine( std::string const& line ) +{ + // TODO: handle trailers? + // (python httplib doesn't seem to!) + if( line.empty() ) + Finish(); + + // just ignore all the trailers... +} + + + +// 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... +void Response::BeginBody() +{ + + m_Chunked = false; + m_Length = -1; // unknown + m_WillClose = false; + + // using chunked encoding? + const char* trenc = getheader( "transfer-encoding" ); + if( trenc && 0==_stricmp( trenc, "chunked") ) + { + m_Chunked = true; + m_ChunkLeft = -1; // unknown + } + + m_WillClose = CheckClose(); + + // length supplied? + const char* contentlen = getheader( "content-length" ); + if( contentlen && !m_Chunked ) + { + m_Length = atoi( contentlen ); + } + + // check for various cases where we expect zero-length body + if( m_Status == NO_CONTENT || + m_Status == 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 + if( m_Connection.m_ResponseBeginCB ) + (m_Connection.m_ResponseBeginCB)( this, m_Connection.m_UserData ); + +/* + 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 = CHUNKLEN; + else + m_State = BODY; +} + + +// return true if we think server will automatically close connection +bool Response::CheckClose() +{ + if( m_Version == 11 ) + { + // HTTP1.1 + // the connection stays open unless "connection: close" is specified. + const char* conn = getheader( "connection" ); + if( conn && 0==_stricmp( 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; +} + + + +} // end namespace happyhttp + + diff --git a/third_party/happyhttp/src/happyhttp.h b/third_party/happyhttp/src/happyhttp.h new file mode 100644 index 0000000000..81cf56c7df --- /dev/null +++ b/third_party/happyhttp/src/happyhttp.h @@ -0,0 +1,333 @@ +/* + * 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. + * + */ + + +#ifndef HAPPYHTTP_H +#define HAPPYHTTP_H + + +#include +#include +#include +#include + + + + +// forward decl +struct in_addr; + +namespace happyhttp +{ + + +class Response; + +// Helper Functions +void BailOnSocketError( const char* context ); +struct in_addr *atoaddr( const char* address); + + +typedef void (*ResponseBegin_CB)( const Response* r, void* userdata ); +typedef void (*ResponseData_CB)( const Response* r, void* userdata, const unsigned char* data, int numbytes ); +typedef void (*ResponseComplete_CB)( const Response* r, void* userdata ); + + +// HTTP status codes +enum { + // 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, +}; + + + +// Exception class + +class Wobbly +{ +public: + Wobbly( const char* fmt, ... ); + const char* what() const + { return m_Message; } +protected: + enum { MAXLEN=256 }; + char m_Message[ MAXLEN ]; +}; + + + +//------------------------------------------------- +// Connection +// +// Handles the socket connection, issuing of requests and managing +// responses. +// ------------------------------------------------ + +class Connection +{ + friend class Response; +public: + // doesn't connect immediately + Connection( const char* host, int port ); + ~Connection(); + + // Set up the response handling callbacks. These will be invoked during + // calls to pump(). + // begincb - called when the responses headers have been received + // datacb - called repeatedly to handle body data + // completecb - response is completed + // userdata is passed as a param to all callbacks. + void setcallbacks( + ResponseBegin_CB begincb, + ResponseData_CB datacb, + ResponseComplete_CB completecb, + void* userdata ); + + // 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. + void connect(); + + // close connection, discarding any pending requests. + void close(); + + // Update the connection (non-blocking) + // Just keep calling this regularly to service outstanding requests. + void pump(); + + // any requests still outstanding? + bool outstanding() const + { return !m_Outstanding.empty(); } + + // --------------------------- + // 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) + void request( const char* method, const char* url, const char* headers[]=0, + const unsigned char* body=0, int bodysize=0 ); + + // --------------------------- + // low-level request interface + // --------------------------- + + // begin request + // method is "GET", "POST" etc... + // url is only path part: eg "/index.html" + void putrequest( const char* method, const char* url ); + + // Add a header to the request (call after putrequest() ) + void putheader( const char* header, const char* value ); + void putheader( const char* header, int numericvalue ); // alternate version + + // Finished adding headers, issue the request. + void endheaders(); + + // send body data if any. + // To be called after endheaders() + void send( const unsigned char* buf, int numbytes ); + +protected: + // some bits of implementation exposed to Response class + + // callbacks + ResponseBegin_CB m_ResponseBeginCB; + ResponseData_CB m_ResponseDataCB; + ResponseComplete_CB m_ResponseCompleteCB; + void* m_UserData; + +private: + enum { IDLE, REQ_STARTED, REQ_SENT } m_State; + std::string m_Host; + int m_Port; + int m_Sock; + std::vector< std::string > m_Buffer; // lines of request + + std::deque< Response* > m_Outstanding; // responses for outstanding requests +}; + + + + + + +//------------------------------------------------- +// Response +// +// Handles parsing of response data. +// ------------------------------------------------ + + +class Response +{ + friend class Connection; +public: + + // retrieve a header (returns 0 if not present) + const char* getheader( const char* name ) const; + + bool completed() const + { return m_State == COMPLETE; } + + + // get the HTTP status code + int getstatus() const; + + // get the HTTP response reason string + const char* getreason() const; + + // true if connection is expected to close after this response. + bool willclose() const + { return m_WillClose; } +protected: + // interface used by Connection + + // only Connection creates Responses. + Response( const char* method, Connection& conn ); + + // pump some data in for processing. + // Returns the number of bytes used. + // Will always return 0 when response is complete. + int pump( const unsigned char* data, int datasize ); + + // tell response that connection has closed + void notifyconnectionclosed(); + +private: + enum { + 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! + } m_State; + + Connection& m_Connection; // to access callback ptrs + std::string m_Method; // req method: "GET", "POST" etc... + + // status line + std::string m_VersionString; // HTTP-Version + int m_Version; // 10: HTTP/1.0 11: HTTP/1.x (where x>=1) + int m_Status; // Status-Code + std::string m_Reason; // Reason-Phrase + + // header/value pairs + std::map m_Headers; + + int m_BytesRead; // body bytes read so far + bool m_Chunked; // response is chunked? + int m_ChunkLeft; // bytes left in current chunk + int m_Length; // -1 if unknown + bool m_WillClose; // connection will close at response end? + + std::string m_LineBuf; // line accumulation for states that want it + std::string m_HeaderAccum; // accumulation buffer for headers + + + void FlushHeader(); + void ProcessStatusLine( std::string const& line ); + void ProcessHeaderLine( std::string const& line ); + void ProcessTrailerLine( std::string const& line ); + void ProcessChunkLenLine( std::string const& line ); + + int ProcessDataChunked( const unsigned char* data, int count ); + int ProcessDataNonChunked( const unsigned char* data, int count ); + + void BeginBody(); + bool CheckClose(); + void Finish(); +}; + + + +} // end namespace happyhttp + + +#endif // HAPPYHTTP_H + + diff --git a/third_party/happyhttp/src/test.cpp b/third_party/happyhttp/src/test.cpp new file mode 100644 index 0000000000..f3d39d1424 --- /dev/null +++ b/third_party/happyhttp/src/test.cpp @@ -0,0 +1,129 @@ +#include "happyhttp.h" +#include +#include + +#ifdef WIN32 +#include +#endif // WIN32 + +int count=0; + +void OnBegin( const happyhttp::Response* r, void* userdata ) +{ + printf( "BEGIN (%d %s)\n", r->getstatus(), r->getreason() ); + count = 0; +} + +void OnData( const happyhttp::Response* r, void* userdata, const unsigned char* data, int n ) +{ + fwrite( data,1,n, stdout ); + count += n; +} + +void OnComplete( const happyhttp::Response* r, void* userdata ) +{ + printf( "COMPLETE (%d bytes)\n", count ); +} + + + +void Test1() +{ + puts("-----------------Test1------------------------" ); + // simple simple GET + happyhttp::Connection conn( "www.scumways.com", 80 ); + conn.setcallbacks( OnBegin, OnData, OnComplete, 0 ); + + conn.request( "GET", "/happyhttp/test.php", 0, 0,0 ); + + while( conn.outstanding() ) + conn.pump(); +} + + + +void Test2() +{ + puts("-----------------Test2------------------------" ); + // POST using high-level request interface + + const char* headers[] = + { + "Connection", "close", + "Content-type", "application/x-www-form-urlencoded", + "Accept", "text/plain", + 0 + }; + + const char* body = "answer=42&name=Bubba"; + + happyhttp::Connection conn( "www.scumways.com", 80 ); + conn.setcallbacks( OnBegin, OnData, OnComplete, 0 ); + conn.request( "POST", + "/happyhttp/test.php", + headers, + (const unsigned char*)body, + strlen(body) ); + + while( conn.outstanding() ) + conn.pump(); +} + +void Test3() +{ + puts("-----------------Test3------------------------" ); + // POST example using lower-level interface + + const char* params = "answer=42&foo=bar"; + int l = strlen(params); + + happyhttp::Connection conn( "www.scumways.com", 80 ); + conn.setcallbacks( OnBegin, OnData, OnComplete, 0 ); + + conn.putrequest( "POST", "/happyhttp/test.php" ); + conn.putheader( "Connection", "close" ); + conn.putheader( "Content-Length", l ); + conn.putheader( "Content-type", "application/x-www-form-urlencoded" ); + conn.putheader( "Accept", "text/plain" ); + conn.endheaders(); + conn.send( (const unsigned char*)params, l ); + + while( conn.outstanding() ) + conn.pump(); +} + + + + +int main( int argc, char* argv[] ) +{ +#ifdef WIN32 + WSAData wsaData; + int code = WSAStartup(MAKEWORD(1, 1), &wsaData); + if( code != 0 ) + { + fprintf(stderr, "shite. %d\n",code); + return 0; + } +#endif //WIN32 + try + { + Test1(); + Test2(); + Test3(); + } + + catch( happyhttp::Wobbly& e ) + { + fprintf(stderr, "Exception:\n%s\n", e.what() ); + } + +#ifdef WIN32 + WSACleanup(); +#endif // WIN32 + + return 0; +} + + +