/* HTTP protocol implementation (request and response). $Id: HTTP.mm 24 2008-06-10 19:49:57Z l.knecht $ */ use net, io, array const GET="GET" const HEAD="HEAD" const POST="POST" /** Encode a string to URI format. @param s the string. @return the URI encoded string. */ function encodeuri(s) res=""; for i=0 to len(s)-1 do ch=substr(s,i,1); if ch>="0" and ch<="9" or ch>="a" and ch<="z" or ch>="A" and ch<="Z" or ch="-" or ch="_" or ch="." or ch="~" then res=res+ch elsif ch=" " then res=res+"+" else res=res+"%"+hexstr(code(ch,0)) end end; return res end /** Decode a string from URI format. @param s the URI string. @return the decoded string. */ function decodeuri(s) res=""; i=0; while i < len(s) do ch=substr(s,i,1); if ch="+" then ch=" " elsif ch="%" then if i+2 < len(s) then ch=char(hexnum(substr(s,i+1,2))); i+=2 end end; res=res+ch; i++ end; return res end /** Encode an array of parameters. @param params the parameter array (with keys). May be null. @return the encoded and concatenated parameters. */ function encodeparams(params) res=""; if params#null then for name in keys(params) do if len(res)#0 then res=res+"&" end; res=res+encodeuri(name)+"="+encodeuri(params[name]) end end; return res end class Socket host; port; stream; keepAlive; // null for no keep alive, timeout (seconds) for keep alive fields; cookies; at; chunklen; dumpin; dumpout; /** Connect this socket to a stream. @param stream the stream to connect to. If null, a stream is created from host and port. */ function connect(stream=null) if stream=null then stream=net.conn(host, port) end; io.flush(stream, false); this.stream=stream end /** Create a new HTTP socket, either from a TCP/IP socket or a host/port pair. @param hostOrStream a host name, or a socket stream. @param port a port number, or null if creating from a socket. @param keepAlive the value for the keepAlive field. If null, a new connection will be created for each request. If keepAlive=null and port=null, only one request can be sent without re-init. */ function init(hostOrStream, port=null, keepAlive=300) this.port=port; this.keepAlive=keepAlive; if port=null then stream=hostOrStream; host=null else host=hostOrStream; connect() end; if cookies=null then cookies=[] end end /** Write a string to the socket. @param s the string. */ function write(s) if dumpout#null then io.write(dumpout, s) end; io.write(stream, s) end /** Write a string and a CRLF pair to the socket. @param s the string. */ function writeln(s) if dumpout#null then io.writeln(dumpout, s) end; io.writeln(stream, s) end /** Flush the socket, sending all output. */ function flush() io.flush(stream) end /** Close the socket. */ function close() if stream#null then io.close(stream); stream=null end end /** Send a request. @param method the request method (GET, HEAD, POST). @param path the request path. @param params the request parameters. @param fields the extra header fields. */ function request(method, path, params=null, fields=null) if stream=null then connect() end; write(method); write(" "); write(path); if method=..GET or method=..HEAD then s=encodeparams(params); if len(s)#0 then if index(path,"?")<0 then write("?") end; write(s) end end; writeln(" HTTP/1.1"); if host#null then writeln("Host: " + host) end; if keepAlive#null then writeln("Keep-Alive: " + keepAlive); writeln("Connection: keep-alive") else writeln("Connection: close") end; if cookies#null then for name in keys(cookies) do write("Cookie: "); write(name); write("="); writeln(cookies[name]) end end; if fields#null then for name in keys(fields) do write(name); write(": "); writeln(fields[name]) end end; if method=..POST then s=encodeparams(params); writeln("Content-Type: application/x-www-form-urlencoded"); writeln("Content-Length: " + len(s)); writeln(""); write(s) else writeln("") end; flush() end /** Read a number of bytes from the socket. @param len the number of bytes to read. @return the string read. */ function read(len) s=io.read(stream, len); if dumpin#null and s#null then io.write(dumpin, s) end; return s end /** Read a line up to a maximum length from the socket. @param maxlen the maximum number of bytes to read. @return the string read (without line end mark). */ function readln(maxlen=1024) s=io.readln(stream, maxlen); if dumpin#null and s#null then io.writeln(dumpin, s) end; if s#null then io.read(stream, 1) end; return s end /** Read the HTTP headers (internal function). */ function readHeaders() s=readln(); while s#null and len(s)#0 do i=index(s, ":"); if i>0 then name=substr(s,0,i); do i++ until i>=len(s) or code(s,i)>32; s=substr(s,i); if lower(name)="set-cookie" then i=index(s,"="); if i>0 then name=substr(s,0,i); s=substr(s,i+1); i=index(s,";"); if i>=0 then s=substr(s,0,i) end; cookies[name]=s end else fields[name]=s; if lower(name)="transfer-encoding" and lower(s)="chunked" then chunklen=-1 end end end; s=readln() end end /** Handle a response, i.e. parse a response header. This sets up various internal fields, and allows to subsequently read the content via readContent(). @return the response code, e.g. 200 for OK. */ function handleResponse() code=null; fields=array.new(0, true); at=0; chunklen=null; s=readln(); if s=null or len(s)=0 then return null end; i=index(s, " "); if i>=0 then i++; j=index(s, " ", i); if j<0 then code=substr(s, i) else code=substr(s, i, j-i) end; try code=num(code) catch e by end // ignore end; readHeaders(); return code end /** Get a field from the response header. @param name the field name (e.g. "Content-Type"), not case sensitive. @return the field contents, or null if there is no such field. */ function field(name) return fields[name] end /** Get a numeric field from the response header. @param name the field name (e.g. "Content-Length"), not case sensitive. @return the field value, or -1 if there is no such field or it does not contain a valid decimal number. */ function numfield(name) try return num(field(name)) catch e by return -1 end end /** Read up to a given number of bytes from the content. @param len the number of bytes to read. @return the content, or null if there is no more content to read. */ function readContent(len) if chunklen#null then if at>=chunklen and chunklen#0 then // read next chunk if chunklen>0 then readln() end; chunklen=hexnum(readln()); at=0; // if at end, read more header fields if chunklen=0 then readHeaders() end end; avail=chunklen-at else avail=numfield('Content-Length')-at end; if avail<=0 then if keepAlive=null then close() end; return null end; if len>avail then len=avail end; s=read(len); at+=len(s); return s end /** Discard all remaining bytes of content. */ function discardContent() while readContent(1024)#null do end end end