happyhttp.nut 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. /*
  2. * HappyHTTP - a simple HTTP library
  3. * Version 0.1
  4. *
  5. * Copyright (c) 2006 Ben Campbell
  6. *
  7. * This software is provided 'as-is', without any express or implied
  8. * warranty. In no event will the authors be held liable for any damages
  9. * arising from the use of this software.
  10. *
  11. * Permission is granted to anyone to use this software for any purpose,
  12. * including commercial applications, and to alter it and redistribute it
  13. * freely, subject to the following restrictions:
  14. *
  15. * 1. The origin of this software must not be misrepresented; you must not
  16. * claim that you wrote the original software. If you use this software in a
  17. * product, an acknowledgment in the product documentation would be
  18. * appreciated but is not required.
  19. *
  20. * 2. Altered source versions must be plainly marked as such, and must not
  21. * be misrepresented as being the original software.
  22. *
  23. * 3. This notice may not be removed or altered from any source distribution.
  24. *
  25. *
  26. *Ported to SquiLu http://code.google.com/p/squilu/ by Domingo Alvarez Duarte
  27. */
  28. // HTTP status codes
  29. enum HTTP_status_code {
  30. // 1xx informational
  31. CONTINUE = 100,
  32. SWITCHING_PROTOCOLS = 101,
  33. PROCESSING = 102,
  34. // 2xx successful
  35. OK = 200,
  36. CREATED = 201,
  37. ACCEPTED = 202,
  38. NON_AUTHORITATIVE_INFORMATION = 203,
  39. NO_CONTENT = 204,
  40. RESET_CONTENT = 205,
  41. PARTIAL_CONTENT = 206,
  42. MULTI_STATUS = 207,
  43. IM_USED = 226,
  44. // 3xx redirection
  45. MULTIPLE_CHOICES = 300,
  46. MOVED_PERMANENTLY = 301,
  47. FOUND = 302,
  48. SEE_OTHER = 303,
  49. NOT_MODIFIED = 304,
  50. USE_PROXY = 305,
  51. TEMPORARY_REDIRECT = 307,
  52. // 4xx client error
  53. BAD_REQUEST = 400,
  54. UNAUTHORIZED = 401,
  55. PAYMENT_REQUIRED = 402,
  56. FORBIDDEN = 403,
  57. NOT_FOUND = 404,
  58. METHOD_NOT_ALLOWED = 405,
  59. NOT_ACCEPTABLE = 406,
  60. PROXY_AUTHENTICATION_REQUIRED = 407,
  61. REQUEST_TIMEOUT = 408,
  62. CONFLICT = 409,
  63. GONE = 410,
  64. LENGTH_REQUIRED = 411,
  65. PRECONDITION_FAILED = 412,
  66. REQUEST_ENTITY_TOO_LARGE = 413,
  67. REQUEST_URI_TOO_LONG = 414,
  68. UNSUPPORTED_MEDIA_TYPE = 415,
  69. REQUESTED_RANGE_NOT_SATISFIABLE = 416,
  70. EXPECTATION_FAILED = 417,
  71. UNPROCESSABLE_ENTITY = 422,
  72. LOCKED = 423,
  73. FAILED_DEPENDENCY = 424,
  74. UPGRADE_REQUIRED = 426,
  75. // 5xx server error
  76. INTERNAL_SERVER_ERROR = 500,
  77. NOT_IMPLEMENTED = 501,
  78. BAD_GATEWAY = 502,
  79. SERVICE_UNAVAILABLE = 503,
  80. GATEWAY_TIMEOUT = 504,
  81. HTTP_VERSION_NOT_SUPPORTED = 505,
  82. INSUFFICIENT_STORAGE = 507,
  83. NOT_EXTENDED = 510,
  84. };
  85. //-------------------------------------------------
  86. // Connection
  87. //
  88. // Handles the socket connection, issuing of requests and managing
  89. // responses.
  90. // ------------------------------------------------
  91. enum Connection_State { IDLE, REQ_STARTED, REQ_SENT };
  92. class HappyHttpConnection {
  93. m_Port = null;
  94. m_Host = null;
  95. m_State = null;
  96. m_Sock = null;
  97. m_Buffer = null; // lines of request
  98. m_Outstanding = null; // responses for outstanding requests
  99. // doesn't connect immediately
  100. constructor(host, port){
  101. m_Host = host;
  102. m_Port = port;
  103. m_Buffer = [];
  104. m_Outstanding = [];
  105. m_State =Connection_State.IDLE;
  106. }
  107. // Don't need to call connect() explicitly as issuing a request will
  108. // call it automatically if needed.
  109. // But it could block (for name lookup etc), so you might prefer to
  110. // call it in advance.
  111. function connect(){
  112. m_Sock = socket.tcp();
  113. m_Sock.connect(m_Host, m_Port);
  114. m_Sock.settimeout(0.01);
  115. }
  116. // close connection, discarding any pending requests.
  117. function close(){
  118. m_Sock.close();
  119. m_Sock = null;
  120. m_Outstanding.clear();
  121. }
  122. // Update the connection (non-blocking)
  123. // Just keep calling this regularly to service outstanding requests.
  124. function pump(milisec=10) { //10 miliseconds to prevent high cpu load
  125. if( m_Outstanding.isempty() ) return; // no requests outstanding
  126. assert( m_Sock != null ); // outstanding requests but no connection!
  127. if( !datawaiting( m_Sock, milisec) ) return; // recv will block
  128. local rc = m_Sock.receive(8192); //2048
  129. local rc_status = rc[1];
  130. if( (rc_status == socket.IO_DONE) || rc_status == socket.IO_TIMEOUT) {
  131. //case socket.IO_CLOSED:
  132. } else {
  133. if(rc[0].len() == 0) throw(format("socket io error %d", rc_status));
  134. }
  135. local buf = rc[0];
  136. local a = buf.len();
  137. if( a== 0 )
  138. {
  139. // connection has closed
  140. local r = m_Outstanding[0];
  141. r->notifyconnectionclosed();
  142. assert( r->completed() );
  143. m_Outstanding.remove(0);
  144. // any outstanding requests will be discarded
  145. close();
  146. }
  147. else
  148. {
  149. local used = 0;
  150. while( used < a && !m_Outstanding.isempty() )
  151. {
  152. local r = m_Outstanding[0];
  153. local u = r->pump( buf, used, a-used );
  154. if(u == 0) throw("Bad data received !");
  155. // delete response once completed
  156. if( r->completed() ) m_Outstanding.remove(0);
  157. used += u;
  158. }
  159. // NOTE: will lose bytes if response queue goes empty
  160. // (but server shouldn't be sending anything if we don't have
  161. // anything outstanding anyway)
  162. assert( used == a ); // all bytes should be used up by here.
  163. }
  164. }
  165. // any requests still outstanding?
  166. function outstanding() { return m_Outstanding && !m_Outstanding.isempty(); }
  167. // ---------------------------
  168. // high-level request interface
  169. // ---------------------------
  170. // method is "GET", "POST" etc...
  171. // url is only path part: eg "/index.html"
  172. // headers is array of name/value pairs, terminated by a null-ptr
  173. // body & bodysize specify body data of request (eg values for a form)
  174. function request( method, url, headers=null, body=null, bodysize=0){
  175. local gotcontentlength = false; // already in headers?
  176. // check headers for content-length
  177. // TODO: check for "Host" and "Accept-Encoding" too
  178. // and avoid adding them ourselves in putrequest()
  179. if( headers ) gotcontentlength = table_rawget(headers, "content-length", false);
  180. putrequest( method, url );
  181. if( body && !gotcontentlength ) putheader( "Content-Length", bodysize );
  182. if( headers ) foreach(name, value in headers) putheader( name, value );
  183. endheaders();
  184. if( body ) send( body, bodysize );
  185. }
  186. // ---------------------------
  187. // low-level request interface
  188. // ---------------------------
  189. // begin request
  190. // method is "GET", "POST" etc...
  191. // url is only path part: eg "/index.html"
  192. function putrequest( method, url ){
  193. if( m_State != Connection_State.IDLE ) throw ("Request already issued" );
  194. if( !method || !url ) throw ( "Method and url can't be NULL" );
  195. m_State = Connection_State.REQ_STARTED;
  196. local req = format("%s %s HTTP/1.1", method, url);
  197. m_Buffer.push( req );
  198. putheader( "Host", m_Host ); // required for HTTP1.1
  199. // don't want any fancy encodings please
  200. putheader("Accept-Encoding", "identity");
  201. // Push a new response onto the queue
  202. local r = new HappyHttpResponse( method, url, this );
  203. m_Outstanding.push( r );
  204. }
  205. // Add a header to the request (call after putrequest() )
  206. function putheader( header, value ){
  207. if( m_State != Connection_State.REQ_STARTED ) throw ( "putheader() failed" );
  208. m_Buffer.push( format("%s: %s", header, value.tostring()) );
  209. }
  210. // Finished adding headers, issue the request.
  211. function endheaders(){
  212. if( m_State != Connection_State.REQ_STARTED ) throw "Cannot send header";
  213. m_State = Connection_State.IDLE;
  214. m_Buffer.push( "\r\n" ); //for double "\r\n\r\n"
  215. local msg = m_Buffer.concat("\r\n");
  216. m_Buffer.clear();
  217. send( msg , msg.len() );
  218. }
  219. // send body data if any.
  220. // To be called after endheaders()
  221. function send( buf, numbytes ){
  222. if( !m_Sock ) connect();
  223. local n = m_Sock.send( buf , 0, numbytes );
  224. //print(m_Sock.getfd(), n, buf);
  225. if(n != numbytes) throw("Could not send ");
  226. }
  227. // return true if socket has data waiting to be read
  228. static function datawaiting( sock , milisec)
  229. {
  230. try {
  231. local rc = socket.select( [sock], [], milisec ? milisec / 1000.0 : 0);
  232. local rar = rc[0];
  233. if(rar.len() == 1 && rar[0] == sock) return true;
  234. return false;
  235. }
  236. catch(e){
  237. if(e == "timeout") return false;
  238. else throw(e);
  239. }
  240. }
  241. function response_begin( r ) {}
  242. function response_data( r, data, data_idx, numbytes ) {}
  243. function response_complete( r ) {}
  244. };
  245. enum Response_state {
  246. STATUSLINE, // start here. status line is first line of response.
  247. HEADERS, // reading in header lines
  248. BODY, // waiting for some body data (all or a chunk)
  249. CHUNKLEN, // expecting a chunk length indicator (in hex)
  250. CHUNKEND, // got the chunk, now expecting a trailing blank line
  251. TRAILERS, // reading trailers after body.
  252. COMPLETE, // response is complete!
  253. };
  254. class HappyHttpResponse {
  255. m_Connection = null; // to access callback ptrs
  256. m_Method = null; // req method: "GET", "POST" etc...
  257. m_Url = null; // req url: /image.php?d=2...
  258. m_VersionString = null; // HTTP-Version
  259. m_Version = null; // 10: HTTP/1.0 11: HTTP/1.x (where x>=1)
  260. m_Status = null; // Status-Code
  261. m_Reason = null; // Reason-Phrase
  262. m_Headers = null; // header/value pairs
  263. m_BytesRead = null; // body bytes read so far
  264. m_Chunked = null; // response is chunked?
  265. m_ChunkLeft = null; // bytes left in current chunk
  266. m_Length = null; // -1 if unknown
  267. m_WillClose = null; // connection will close at response end?
  268. m_LineBuf = null; // line accumulation for states that want it
  269. m_HeaderAccum = null; // accumulation buffer for headers
  270. m_State = null;
  271. // only Connection creates Responses.
  272. constructor(method, url, conn){
  273. m_Method = method;
  274. m_Url = url;
  275. m_Connection = conn;
  276. m_Version = 0;
  277. m_Status = 0;
  278. m_BytesRead = 0;
  279. m_Chunked = false;
  280. m_ChunkLeft = 0;
  281. m_Length = -1;
  282. m_WillClose = false;
  283. m_Headers = {};
  284. m_HeaderAccum = "";
  285. m_LineBuf = "";
  286. m_State = Response_state.STATUSLINE;
  287. }
  288. // retrieve a header (returns null if not present)
  289. function getheader( name ){
  290. local lname = name.tolower();
  291. return table_get(m_Headers, lname, null);
  292. }
  293. function completed() { return m_State == Response_state.COMPLETE; }
  294. // get the HTTP status code
  295. function getstatus(){
  296. // only valid once we've got the statusline
  297. assert( m_State != Response_state.STATUSLINE );
  298. return m_Status;
  299. }
  300. // get the HTTP response reason string
  301. function getreason(){
  302. // only valid once we've got the statusline
  303. assert( m_State != Response_state.STATUSLINE );
  304. return m_Reason;
  305. }
  306. // true if connection is expected to close after this response.
  307. function willclose() { return m_WillClose; }
  308. function getconnection(){return m_Connection;}
  309. function geturl() { return m_Url; }
  310. function getLength() { return m_Length;}
  311. // pump some data in for processing.
  312. // Returns the number of bytes used.
  313. // Will always return 0 when response is complete.
  314. function pump( data, start, datasize ){
  315. assert( datasize != 0 );
  316. local count = datasize;
  317. local data_idx = 0;
  318. local last_count = 0;
  319. while( count > 0 && m_State != Response_state.COMPLETE && last_count != count)
  320. {
  321. last_count = count;
  322. if( m_State == Response_state.STATUSLINE ||
  323. m_State == Response_state.HEADERS ||
  324. m_State == Response_state.TRAILERS ||
  325. m_State == Response_state.CHUNKLEN ||
  326. m_State == Response_state.CHUNKEND )
  327. {
  328. // we want to accumulate a line
  329. local pos = data.find("\n", data_idx);
  330. if( pos >= 0 )
  331. {
  332. count -= pos-data_idx+1;
  333. local new_pos = pos;
  334. if(pos > 0 && data[pos-1] == '\r') --new_pos;
  335. m_LineBuf = data.slice(data_idx, new_pos);
  336. data_idx = pos+1;
  337. // now got a whole line!
  338. switch( m_State )
  339. {
  340. case Response_state.STATUSLINE:
  341. ProcessStatusLine( m_LineBuf );
  342. break;
  343. case Response_state.HEADERS:
  344. ProcessHeaderLine( m_LineBuf );
  345. break;
  346. case Response_state.TRAILERS:
  347. ProcessTrailerLine( m_LineBuf );
  348. break;
  349. case Response_state.CHUNKLEN:
  350. ProcessChunkLenLine( m_LineBuf );
  351. break;
  352. case Response_state.CHUNKEND:
  353. // just soak up the crlf after body and go to next state
  354. assert( m_Chunked == true );
  355. m_State = Response_state.CHUNKLEN;
  356. break;
  357. default:
  358. break;
  359. }
  360. m_LineBuf = "";
  361. }
  362. }
  363. else if( m_State == Response_state.BODY )
  364. {
  365. local bytesused = 0;
  366. if( m_Chunked )
  367. bytesused = ProcessDataChunked( data, data_idx, count );
  368. else
  369. bytesused = ProcessDataNonChunked( data, data_idx, count );
  370. data_idx += bytesused;
  371. count -= bytesused;
  372. }
  373. //os.sleep(1);
  374. }
  375. // return number of bytes used
  376. return datasize - count;
  377. }
  378. // tell response that connection has closed
  379. function notifyconnectionclosed(){
  380. if( m_State == Response_state.COMPLETE ) return;
  381. // eof can be valid...
  382. if( m_State == Response_state.BODY &&
  383. !m_Chunked && m_Length <= 0 ) Finish(); // we're all done!
  384. else throw ( "Connection closed unexpectedly" );
  385. }
  386. function FlushHeader(){
  387. if( m_HeaderAccum.isempty() ) return; // no flushing required
  388. local rc = m_HeaderAccum.match("([^:]+):%s*(.+)");
  389. if(!rc) throw(format("Invalid header (%s)", m_HeaderAccum));
  390. m_Headers[ rc[0].tolower() ] <- rc[1];
  391. m_HeaderAccum = "";
  392. }
  393. function ProcessStatusLine( line ){
  394. //HTTP/1.1 200 OK
  395. local rc = line.match("%s*(%S+)%s+(%d+)%s+(.+)");
  396. if(!rc) throw(format("BadStatusLine (%s)", line));
  397. m_VersionString = rc[0];
  398. m_Status = rc[1].tointeger();
  399. m_Reason = rc[2];
  400. if( m_Status < 100 || m_Status > 999 ) throw ( format("BadStatusLine (%s)", line) );
  401. if( m_VersionString == "HTTP:/1.0" ) m_Version = 10;
  402. else if( m_VersionString.startswith( "HTTP/1." ) ) m_Version = 11;
  403. else throw ( format("UnknownProtocol (%s)", m_VersionString) );
  404. // TODO: support for HTTP/0.9
  405. // OK, now we expect headers!
  406. m_State = Response_state.HEADERS;
  407. m_HeaderAccum = "";
  408. }
  409. function ProcessHeaderLine( line ){
  410. if( line.isempty() )
  411. {
  412. FlushHeader();
  413. // end of headers
  414. // HTTP code 100 handling (we ignore 'em)
  415. if( m_Status == HTTP_status_code.CONTINUE )
  416. m_State = Response_state.STATUSLINE; // reset parsing, expect new status line
  417. else
  418. BeginBody(); // start on body now!
  419. return;
  420. }
  421. if( line[0] == ' ' )
  422. {
  423. // it's a continuation line - just add it to previous data
  424. m_HeaderAccum += line;
  425. }
  426. else
  427. {
  428. // begin a new header
  429. FlushHeader();
  430. m_HeaderAccum = line;
  431. }
  432. }
  433. function ProcessTrailerLine(line){
  434. // TODO: handle trailers?
  435. // (python httplib doesn't seem to!)
  436. if( line.isempty() ) Finish();
  437. // just ignore all the trailers...
  438. }
  439. function ProcessChunkLenLine( line ){
  440. // chunklen in hex at beginning of line
  441. m_ChunkLeft = line.tointeger(16);
  442. if( m_ChunkLeft == 0 )
  443. {
  444. // got the whole body, now check for trailing headers
  445. m_State = Response_state.TRAILERS;
  446. m_HeaderAccum = "";
  447. }
  448. else
  449. {
  450. m_State = Response_state.BODY;
  451. }
  452. }
  453. function ProcessDataChunked( data, data_idx, count ){
  454. assert( m_Chunked );
  455. local n = count;
  456. if( n>m_ChunkLeft ) n = m_ChunkLeft;
  457. // invoke callback to pass out the data
  458. m_Connection.response_data( this, data, data_idx, n );
  459. m_BytesRead += n;
  460. m_ChunkLeft -= n;
  461. assert( m_ChunkLeft >= 0);
  462. if( m_ChunkLeft == 0 )
  463. {
  464. // chunk completed! now soak up the trailing CRLF before next chunk
  465. m_State = Response_state.CHUNKEND;
  466. }
  467. return n;
  468. }
  469. function ProcessDataNonChunked( data, data_idx, count ){
  470. local n = count;
  471. if( m_Length != -1 )
  472. {
  473. // we know how many bytes to expect
  474. local remaining = m_Length - m_BytesRead;
  475. if( n > remaining ) n = remaining;
  476. }
  477. // invoke callback to pass out the data
  478. m_Connection.response_data( this, data, data_idx, n );
  479. m_BytesRead += n;
  480. // Finish if we know we're done. Else we're waiting for connection close.
  481. if( m_Length != -1 && m_BytesRead == m_Length ) Finish();
  482. return n;
  483. }
  484. // OK, we've now got all the headers read in, so we're ready to start
  485. // on the body. But we need to see what info we can glean from the headers
  486. // first...
  487. function BeginBody(){
  488. m_Chunked = false;
  489. m_Length = -1; // unknown
  490. m_WillClose = false;
  491. // using chunked encoding?
  492. local trenc = getheader( "transfer-encoding" );
  493. if( trenc && trenc == "chunked" )
  494. {
  495. m_Chunked = true;
  496. m_ChunkLeft = -1; // unknown
  497. }
  498. m_WillClose = CheckClose();
  499. // length supplied?
  500. local contentlen = getheader( "content-length" );
  501. if( contentlen && !m_Chunked )
  502. {
  503. m_Length = contentlen.tointeger();
  504. }
  505. // check for various cases where we expect zero-length body
  506. if( m_Status == HTTP_status_code.NO_CONTENT ||
  507. m_Status == HTTP_status_code.NOT_MODIFIED ||
  508. ( m_Status >= 100 && m_Status < 200 ) || // 1xx codes have no body
  509. m_Method == "HEAD" )
  510. {
  511. m_Length = 0;
  512. }
  513. // if we're not using chunked mode, and no length has been specified,
  514. // assume connection will close at end.
  515. if( !m_WillClose && !m_Chunked && m_Length == -1 ) m_WillClose = true;
  516. // Invoke the user callback, if any
  517. m_Connection.response_begin( this );
  518. /*
  519. printf("---------BeginBody()--------\n");
  520. printf("Length: %d\n", m_Length );
  521. printf("WillClose: %d\n", (int)m_WillClose );
  522. printf("Chunked: %d\n", (int)m_Chunked );
  523. printf("ChunkLeft: %d\n", (int)m_ChunkLeft );
  524. printf("----------------------------\n");
  525. */
  526. // now start reading body data!
  527. if( m_Chunked ) m_State = Response_state.CHUNKLEN;
  528. else m_State = Response_state.BODY;
  529. }
  530. function CheckClose(){
  531. if( m_Version == 11 )
  532. {
  533. // HTTP1.1
  534. // the connection stays open unless "connection: close" is specified.
  535. local conn = getheader( "connection" );
  536. if( conn && conn == "close" ) return true;
  537. else return false;
  538. }
  539. // Older HTTP
  540. // keep-alive header indicates persistant connection
  541. if( getheader( "keep-alive" ) ) return false;
  542. // TODO: some special case handling for Akamai and netscape maybe?
  543. // (see _check_close() in python httplib.py for details)
  544. return true;
  545. }
  546. function Finish(){
  547. m_State = Response_state.COMPLETE;
  548. // invoke the callback
  549. m_Connection.response_complete( this );
  550. }
  551. }
  552. /*
  553. class MyHappyHttpConnection extends HappyHttpConnection {
  554. count = null;
  555. constructor(host, port){
  556. base.constructor(host, port);
  557. count = 0;
  558. }
  559. function response_begin( r ) {
  560. printf( "BEGIN (%d %s)\n", r->getstatus(), r->getreason() );
  561. count = 0;
  562. }
  563. function response_data( r, data, data_idx, numbytes ) {
  564. print( data );
  565. count += numbytes;
  566. }
  567. function response_complete( r ) {
  568. printf( "COMPLETE (%d bytes)\n", count );
  569. }
  570. }
  571. function test_happyhttp(){
  572. local start = os.getmillicount();
  573. local conn = MyHappyHttpConnection( "www.scumways.com", 80);
  574. conn.request( "GET", "/happyhttp/test.php" );
  575. while( conn.outstanding() ) conn.pump();
  576. print(format("Took : %d ms", os.getmillispan(start)));
  577. }
  578. test_happyhttp();
  579. */