diff --git a/lib/modules/Protocols.pmod/HTTP.pmod/Query.pike b/lib/modules/Protocols.pmod/HTTP.pmod/Query.pike new file mode 100644 index 0000000000000000000000000000000000000000..7807b872bcb102f664a67beb518ac270c66235e3 --- /dev/null +++ b/lib/modules/Protocols.pmod/HTTP.pmod/Query.pike @@ -0,0 +1,586 @@ +/* +**! module Protocols +**! submodule HTTP +**! class Query +**! +**! Open and execute a HTTP query. +**! +**! method object thread_request(string server,int port,string query); +**! method object thread_request(string server,int port,string query,mapping headers,void|string data); +**! Create a new query object and begin the query. +**! +**! The query is executed in a background thread; +**! call '() in this object to wait for the request +**! to complete. +**! +**! 'query' is the first line sent to the HTTP server; +**! for instance "GET /index.html HTTP/1.1". +**! +**! headers will be encoded and sent after the first line, +**! and data will be sent after the headers. +**! +**! returns the called object +**! +**! method object set_callbacks(function request_ok,function request_fail,mixed ...extra) +**! method object async_request(string server,int port,string query); +**! method object async_request(string server,int port,string query,mapping headers,void|string data); +**! Setup and run an asynchronous request, +**! otherwise similar to thread_request. +**! +**! request_ok(object httpquery,...extra args) +**! will be called when connection is complete, +**! and headers are parsed. +**! +**! request_fail(object httpquery,...extra args) +**! is called if the connection fails. +**! +**! returns the called object +**! +**! variable int ok +**! Tells if the connection is successfull. +**! variable int errno +**! Errno copied from the connection. +**! +**! variable mapping headers +**! Headers as a mapping. +**! +**! variable string protocol +**! Protocol string, ie "HTTP/1.0". +**! +**! variable int status +**! variable string status_desc +**! Status number and description (ie, 200 and "ok"). +**! +**! variable mapping hostname_cache +**! Set this to a global mapping if you want to use a cache, +**! prior of calling *request(). +**! +**! variable mapping async_dns +**! Set this to an array of Protocols.DNS.async_clients, +**! if you wish to limit the number of outstanding DNS +**! requests. Example: +**! async_dns=allocate(20,Protocols.DNS.async_client)(); +**! +**! method int `()() +**! Wait for connection to complete. +**! returns 1 on successfull connection, 0 if failed +**! +**! method array cast("array") +**! Gives back ({mapping headers,string data, +**! string protocol,int status,string status_desc}); +**! +**! method mapping cast("mapping") +**! Gives back +**! headers | +**! (["protocol":protocol, +**! "status":status number, +**! "status_desc":status description, +**! "data":data]); +**! +**! method string cast("string") +**! Gives back the answer as a string. +**! +**! method string data() +**! Gives back the data as a string. +**! +**! object(pseudofile) file() +**! object(pseudofile) file(mapping newheaders,void|mapping removeheaders) +**! object(pseudofile) datafile(); +**! Gives back a pseudo-file object, +**! with the method read() and close(). +**! This could be used to copy the file to disc at +**! a proper tempo. +**! +**! datafile() doesn't give the complete request, +**! just the data. +**! +**! newheaders, removeheaders is applied as: +**! <tt>(oldheaders|newheaders))-removeheaders</tt> +**! Make sure all new and remove-header indices are lower case. +**! +**! void async_fetch(function done_callback); +**! Fetch all data in background. +*/ + +#define error(S) throw( ({(S),backtrace()}) ) + +/****** variables **************************************************/ + +// open + +int errno; +int ok; + +mapping headers; + +string protocol; +int status; +string status_desc; + +int timeout=120; // seconds + +// internal + +object con; +string request; + +string buf="",headerbuf=""; +int datapos; +object conthread; + +function request_ok,request_fail; +array extra_args; + +/****** internal stuff *********************************************/ + +static void ponder_answer() +{ + // read until we have all headers + + int i=0; + + for (;;) + { + string s; + + if (i<0) i=0; + if ((i=search(buf,"\r\n\r\n",i))!=-1) break; + + s=con->read(8192,1); + if (s=="") { i=strlen(buf); break; } + + i=strlen(buf)-3; + buf+=s; + } + + headerbuf=buf[..i-1]; + datapos=i+4; + + // split headers + + headers=([]); + sscanf(headerbuf,"%s%*[ ]%d%*[ ]%s%*[\r\n]",protocol,status,status_desc); + foreach ((headerbuf/"\r\n")[1..],string s) + { + string n,d; + sscanf(s,"%[!-9;-~]%*[ \t]:%*[ \t]%s",n,d); + headers[lower_case(n)]=d; + } + + // done + ok=1; + remove_call_out(async_timeout); + + if (request_ok) request_ok(this_object(),@extra_args); +} + +static void connect(string server,int port) +{ + if (catch { con->connect(server,port); }) + { + if (!(errno=con->errno())) errno=22; /* EINVAL */ + destruct(con); + con=0; + ok=0; + return; + } + + con->write(request); + + ponder_answer(); +} + +static void async_close() +{ + con->set_blocking(); + ponder_answer(); +} + +static void async_read(mixed dummy,string s) +{ + buf+=s; + if (-1!=search(buf,"\r\n\r\n")) + { + con->set_blocking(); + ponder_answer(); + } +} + +static void async_write() +{ + con->set_blocking(); + con->write(request); + con->set_nonblocking(async_read,0,async_close); +} + +static void async_connected() +{ + con->set_nonblocking(async_read,async_write,async_close); + con->write(""); +} + +static void async_failed() +{ + if (con) errno=con->errno; else errno=113; // EHOSTUNREACH + ok=0; + if (request_fail) request_fail(this_object(),@extra_args); + remove_call_out(async_timeout); +} + +static void async_timeout() +{ + errno=110; // timeout + if (con) + { + catch { con->close(); }; + destruct(con); + } + con=0; + async_failed(); +} + +void async_got_host(string server,int port) +{ + if (!server) + { + async_failed(); + catch { destruct(con); }; // we may be destructed here + return; + } + if (catch + { + con=Stdio.File(); + if (!con->open_socket()) + error("HTTP.Query(): can't open socket; "+strerror(con->errno)+"\n"); + }) + { + return; // got timeout already, we're destructed + } + + con->set_nonblocking(0,async_connected,async_failed); +// werror(server+"\n"); + + if (catch { con->connect(server,port); }) + { + if (!(errno=con->errno())) errno=22; /* EINVAL */ + destruct(con); + con=0; + ok=0; + async_failed(); + } +} + +void async_fetch_read(mixed dummy,string data) +{ + buf+=data; +} + +void async_fetch_close() +{ + con->set_blocking(); + destruct(con); + con=0; + request_ok(@extra_args); +} + +/****** utilities **************************************************/ + +string headers_encode(mapping h) +{ + if (!h || !sizeof(h)) return ""; + return Array.map( indices(h), + lambda(string hname,mapping headers) + { + if (stringp(headers[hname])) + return String.capitalize(hname) + + ": " + headers[hname]; + }, h )*"\r\n" + "\r\n"; +} + +/****** helper methods *********************************************/ + +mapping hostname_cache=([]); +array async_dns=0; + +void dns_lookup_callback(string name,string ip,function callback, + mixed ...extra) +{ + hostname_cache[name]=ip; + if (functionp(callback)) + callback(ip,@extra); +} + +void dns_lookup_async(string hostname,function callback,mixed ...extra) +{ + string id; + if (hostname=="") + { + call_out(callback,0,0,@extra); // can't lookup that... + return; + } + sscanf(hostname,"%*[0-9.]",id); + if (hostname==id || + (id=hostname_cache[hostname])) + { + call_out(callback,0,id,@extra); + return; + } + + if (!async_dns) + Protocols.DNS.async_client() + ->host_to_ip(hostname,dns_lookup_callback,callback,@extra); + else + async_dns[random(sizeof(async_dns))]-> + host_to_ip(hostname,dns_lookup_callback,callback,@extra); +} + +string dns_lookup(string hostname) +{ + string id; + sscanf(hostname,"%*[0-9.]",id); + if (hostname==id || + (id=hostname_cache[hostname])) + return id; + + array hosts=((Protocols.DNS.client()->gethostbyname(hostname)|| + ({0,0}))[1]+({0})); + return hosts[random(sizeof(hosts))]; +} + + +/****** called methods *********************************************/ + +object set_callbacks(function(object,mixed...:mixed) _ok, + function(object,mixed...:mixed) _fail, + mixed ...extra) +{ + extra_args=extra; + request_ok=_ok; + request_fail=_fail; + return this_object(); +} + +object thread_request(string server,int port,string query, + void|mapping|string headers,void|string data) +{ + // start open the connection + + con=Stdio.File(); + if (!con->open_socket()) + error("HTTP.Query(): can't open socket; "+strerror(con->errno)+"\n"); + + string server1=dns_lookup(server); + + if (server1) server=server1; // cheaty, if host doesn't exist + + conthread=thread_create(connect,server,port); + + // prepare the request + + if (!data) data=""; + + if (!headers) headers=""; + else if (mappingp(headers)) + { + headers=mkmapping(Array.map(indices(headers),lower_case), + values(headers)); + + if (data!="") headers->content_length=strlen(data); + + headers=headers_encode(headers); + } + + request=query+"\r\n"+headers+"\r\n"+data; + + return this_object(); +} + +object async_request(string server,int port,string query, + void|mapping|string headers,void|string data) +{ + // start open the connection + + call_out(async_timeout,timeout); + + dns_lookup_async(server,async_got_host,port); + + // prepare the request + + if (!data) data=""; + + if (!headers) headers=""; + else if (mappingp(headers)) + { + headers=mkmapping(Array.map(indices(headers),lower_case), + values(headers)); + + if (data!="") headers->content_length=strlen(data); + + headers=headers_encode(headers); + } + + request=query+"\r\n"+headers+"\r\n"+data; + + return this_object(); +} + +int `()() +{ + // wait for completion + if (conthread) conthread->wait(); + return ok; +} + +string data() +{ + `()(); + int len=(int)headers["content-length"]; + int l; + if (zero_type(len)) + l=0x7fffffff; + else + l=len-strlen(buf)+4+strlen(headerbuf); + if (l>0 && con) + { + string s=con->read(l); + buf+=s; + } + // note! this can't handle keep-alive � HEAD requests. + return buf[datapos..]; +} + +array|mapping|string cast(string to) +{ + switch (to) + { + case "mapping": + `()(); + return headers| + (["data":data(), + "protocol":protocol, + "status":status, + "status_desc":status_desc]); + case "array": + `()(); + return ({headers,data(),protocol,status,status_desc}); + case "string": + `()(); + data(); + return buf; + } + error("HTTP.Query: can't cast to "+to+"\n"); +} + +class PseudoFile +{ + string buf; + object con; + int len; + int p=0; + + void create(object _con,string _buf,int _len) + { + con=_con; + buf=_buf; + len=_len; + if (!con) len=strlen(buf); + } + + string read(int n) + { + string s; + + if (p+n>len) n=len-p; + + if (strlen(buf)<n && con) + buf+=con->read(n-strlen(buf)); + + s=buf[..n-1]; + buf=buf[n..]; + p+=strlen(s); + return s; + } + + void close() + { + catch { con->close(); destruct(con); }; + con=0; // forget + } +}; + +object file(void|mapping newheader,void|mapping removeheader) +{ + `()(); + mapping h=headers; + int len; + if (newheader||removeheader) + { + h=(h|(newheader||([])))-(removeheader||([])); + string hbuf=headers_encode(h); + if (hbuf=="") hbuf="\r\n"; + if (zero_type(headers["content-length"])) + len=0x7fffffff; + else + len=strlen(protocol+" "+status+" "+status_desc)+2+ + strlen(hbuf)+2+(int)headers["content-length"]; + return PseudoFile(con, + protocol+" "+status+" "+status_desc+"\r\n"+ + hbuf+"\r\n"+buf[datapos..],len); + } + if (zero_type(headers["content-length"])) + len=0x7fffffff; + else + strlen(headerbuf)+4+(int)h["content-length"]; + return PseudoFile(con,buf,len); +} + +object datafile() +{ + `()(); + return PseudoFile(con,buf[datapos..],(int)headers["content-length"]); +} + +void destroy() +{ + catch { con->close(); destruct(con); }; +} + +void async_fetch(function callback,array ... extra) +{ + if (!con) + { + callback(@extra); // nothing to do, stupid... + return; + } + extra_args=extra; + request_ok=callback; + con->set_nonblocking(async_fetch_read,0,async_fetch_close); +} + +/************************ example *****************************/ + +#if 0 + +object o=HTTP.Query(); + +void ok() +{ + write("ok...\n"); + write(sprintf("%O\n",o->headers)); + exit(0); +} + +void fail() +{ + write("fail\n"); + exit(0); +} + +int main() +{ + o->set_callbacks(ok,fail); + o->async_request("www.roxen.com",80,"HEAD / HTTP/1.0"); + return -1; +} + +#endif