Prechádzať zdrojové kódy

Separate happyhttp an added copyright.

mingodad 13 rokov pred
rodič
commit
c8d727a944
2 zmenil súbory, kde vykonal 659 pridanie a 629 odobranie
  1. 657 0
      ourbiz/happyhttp.nut
  2. 2 629
      ourbiz/ourbiz-client.nut

+ 657 - 0
ourbiz/happyhttp.nut

@@ -0,0 +1,657 @@
+/*
+ * 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 Connection {
+	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;
+	}
+
+	// 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.empty() ) 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(2048);
+		switch(rc[1]){
+			case socket.IO_DONE:
+			case socket.IO_TIMEOUT:
+				break;
+			default:
+				throw(format("socket io error %d", rc[1]));
+		}
+		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.empty() )
+			{
+				local r = m_Outstanding[0];
+				local u = r->pump( buf, used, a-used );
+
+				// 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.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)
+	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 = headers.get("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 Response( 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( "" );
+		m_Buffer.push( "" );
+		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 );
+		if(n != numbytes) throw("Could not send ");
+	}
+
+	// return true if socket has data waiting to be read
+	static function datawaiting( sock , milisec=0)
+	{
+		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, 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 Response {
+	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 m_Headers.get(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;
+
+		while( count > 0 && m_State != Response_state.COMPLETE )
+		{
+			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, count );
+				else
+					bytesused = ProcessDataNonChunked( data, count );
+				data += bytesused;
+				count -= bytesused;
+			}
+		}
+		// 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.empty() ) 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.empty() )
+		{
+			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.empty() ) 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, 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, 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, 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, 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 MyConnection extends Connection {
+	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, numbytes ) {
+		print( data );
+		count += numbytes;
+	}
+	function response_complete( r ) {
+		printf( "COMPLETE (%d bytes)\n", count );
+	}
+}
+
+function test_happyhttp(){
+	local conn = MyConnection( "www.scumways.com", 80);
+	conn.request( "GET", "/happyhttp/test.php" );
+	while( conn.outstanding() ) conn.pump();
+}

+ 2 - 629
ourbiz/ourbiz-client.nut

@@ -61,633 +61,6 @@ Digest username="%s", realm="%s", nonce="%s", uri="%s", qop="%s", nc="%s", cnonc
 //local auth = AuthDigest();
 //local auth = AuthDigest();
 //auth.parse_authenticate([==[Digest username="mingote", realm="ourbiz.dadbiz.es", nonce="7f1de29e6da19d22b51c68001e7e0e54", uri="/DB/GetList?list=sales_tax_rates", response="08cfa598e5945d3aada09308017da216", qop=auth, nc=00000002, cnonce="bf5f336c1827bbb2"]==]);
 //auth.parse_authenticate([==[Digest username="mingote", realm="ourbiz.dadbiz.es", nonce="7f1de29e6da19d22b51c68001e7e0e54", uri="/DB/GetList?list=sales_tax_rates", response="08cfa598e5945d3aada09308017da216", qop=auth, nc=00000002, cnonce="bf5f336c1827bbb2"]==]);
 
 
+dofile("happyhttp.nut");
 
 
-// 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 Connection {
-	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;
-	}
-
-	// 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.empty() ) 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(2048);
-		switch(rc[1]){
-			case socket.IO_DONE:
-			case socket.IO_TIMEOUT:
-				break;
-			default:
-				throw(format("socket io error %d", rc[1]));
-		}
-		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.empty() )
-			{
-				local r = m_Outstanding[0];
-				local u = r->pump( buf, used, a-used );
-
-				// 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.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)
-	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 = headers.get("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 Response( 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( "" );
-		m_Buffer.push( "" );
-		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 );
-		if(n != numbytes) throw("Could not send ");
-	}
-
-	// return true if socket has data waiting to be read
-	static function datawaiting( sock , milisec=0)
-	{
-		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, 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 Response {
-	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 m_Headers.get(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;
-
-		while( count > 0 && m_State != Response_state.COMPLETE )
-		{
-			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, count );
-				else
-					bytesused = ProcessDataNonChunked( data, count );
-				data += bytesused;
-				count -= bytesused;
-			}
-		}
-		// 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.empty() ) 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.empty() )
-		{
-			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.empty() ) 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, 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, 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, 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, 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 MyConnection extends Connection {
-	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, numbytes ) {
-		print( data );
-		count += numbytes;
-	}
-	function response_complete( r ) {
-		printf( "COMPLETE (%d bytes)\n", count );
-	}
-}
-
-local conn = MyConnection( "www.scumways.com", 80);
-
-conn.request( "GET", "/happyhttp/test.php" );
-
-while( conn.outstanding() ) conn.pump();
+test_happyhttp();