From e5ff5834f94eaab0da4082be541dc787b6ee038f Mon Sep 17 00:00:00 2001 From: Kongqun Yang Date: Tue, 3 Dec 2013 10:29:40 -0800 Subject: [PATCH] Add HappyHTTP library. We need a http communication library for license requests. HappyHTTP is lightweight and cross platform. Compared to the url fetcher in Chromium, it is simpler and easier to use, and it is much smaller. The source is cloned from https://github.com/Zintinio/HappyHTTP/ SHA 7306b1606a09063ac38c264afe59f0ad0b441750. Change-Id: I21b691452f3d7b5fea34c611012b252e2e3c9e15 --- third_party/happyhttp/src/Makefile | 40 + third_party/happyhttp/src/README.md | 224 ++++++ third_party/happyhttp/src/happyhttp.cpp | 938 ++++++++++++++++++++++++ third_party/happyhttp/src/happyhttp.h | 333 +++++++++ third_party/happyhttp/src/test.cpp | 129 ++++ 5 files changed, 1664 insertions(+) create mode 100644 third_party/happyhttp/src/Makefile create mode 100644 third_party/happyhttp/src/README.md create mode 100644 third_party/happyhttp/src/happyhttp.cpp create mode 100644 third_party/happyhttp/src/happyhttp.h create mode 100644 third_party/happyhttp/src/test.cpp 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; +} + + +