|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#ifdef HTTP_CLIENT_DEBUG |
# define TRACE(X...)werror("%s:%d: %s",basename(__FILE__),__LINE__,sprintf(X)) |
#else |
# define TRACE(X...)0 |
#endif |
|
protected constant DEFAULT_MAXTIME = 60; |
|
|
|
public Result sync_get(Protocols.HTTP.Session.URL uri, |
void|Arguments args, |
void|Session session) |
{ |
return do_safe_method("GET", uri, args, false, session); |
} |
|
|
|
public Result sync_post(Protocols.HTTP.Session.URL uri, |
void|Arguments args, |
void|Session session) |
{ |
return do_safe_method("POST", uri, args, false, session); |
} |
|
|
|
public Result sync_put(Protocols.HTTP.Session.URL uri, |
void|Arguments args, |
void|Session session) |
{ |
return do_safe_method("PUT", uri, args, false, session); |
} |
|
|
|
public Result sync_delete(Protocols.HTTP.Session.URL uri, |
void|Arguments args, |
void|Session session) |
{ |
return do_safe_method("DELETE", uri, args, false, session); |
} |
|
|
|
public void async_get(Protocols.HTTP.Session.URL uri, |
void|Arguments args, |
void|Session session) |
{ |
do_safe_method("GET", uri, args, true, session); |
} |
|
|
|
public void async_post(Protocols.HTTP.Session.URL uri, |
void|Arguments args, |
void|Session session) |
{ |
do_safe_method("POST", uri, args, true, session); |
} |
|
|
|
public void async_put(Protocols.HTTP.Session.URL uri, |
void|Arguments args, |
void|Session session) |
{ |
do_safe_method("PUT", uri, args, true, session); |
} |
|
|
|
public void async_delete(Protocols.HTTP.Session.URL uri, |
void|Arguments args, |
void|Session session) |
{ |
do_safe_method("DELETE", uri, args, true, session); |
} |
|
|
|
public Result do_safe_method(string http_method, |
Protocols.HTTP.Session.URL uri, |
void|Arguments args, |
void|bool async, |
void|Session session) |
{ |
if (!args) { |
args = Arguments(); |
} |
|
Result res; |
Session s = session || Session(); |
if (args && args->follow_redirects > -1) { |
s->follow_redirects = args->follow_redirects; |
} |
object qr; |
Thread.Queue q; |
mixed co_maxtime; |
|
if (args->maxtime) s->maxtime = args->maxtime; |
if (args->timeout) s->timeout = args->timeout; |
|
if (!async) { |
q = Thread.Queue(); |
} |
|
function(Result:void) cb = lambda (Result r) { |
TRACE("Got callback: %O\n", r); |
res = r; |
q && q->write("@"); |
if (async) { |
if (r->ok && args->on_success) { |
args->on_success(res); |
} |
else if (!r->ok && args->on_failure) { |
args->on_failure(r); |
} |
qr = 0; |
s = 0; |
} |
}; |
|
qr = s->async_do_method_url(http_method, uri, |
args->variables, |
args->data, |
args->headers, |
0, |
cb, |
cb, |
args->extra_args || ({})); |
|
if (!query_has_maxtime()) { |
TRACE("No maxtime in Protocols.HTTP.Query. Set external max timeout: %O\n", |
s->maxtime || DEFAULT_MAXTIME); |
co_maxtime = call_out(lambda () { |
TRACE("Timeout callback: %O\n", qr); |
|
res = Failure(([ |
"status" : 504, |
"status_desc" : "Gateway timeout", |
"host" : qr->con->host, |
"headers" : qr->con->headers, |
"url" : qr->url_requested |
])); |
|
qr->set_callbacks(0, 0, 0, 0); |
qr->con->set_callbacks(0, 0, 0); |
destruct(qr); |
s = 0; |
|
q && q->write("@"); |
|
if (async) { |
if (args->on_failure) { |
args->on_failure(res); |
} |
|
qr = 0; |
s = 0; |
} |
}, s->maxtime || DEFAULT_MAXTIME); |
} |
|
if (!async) { |
q->read(); |
} |
|
if (co_maxtime && co_maxtime[0]) { |
TRACE("Remove timeout callout\n"); |
remove_call_out(co_maxtime); |
} |
|
if (!async) { |
q = 0; |
qr = 0; |
s = 0; |
} |
|
return res; |
} |
|
|
|
|
protected int(-1..1) _query_has_maxtime = -1; |
protected bool query_has_maxtime() |
{ |
if (_query_has_maxtime != -1) { |
return _query_has_maxtime == 1; |
} |
|
Protocols.HTTP.Query q = Protocols.HTTP.Query(); |
_query_has_maxtime = (int) has_index(q, "maxtime"); |
destruct(q); |
return _query_has_maxtime == 1; |
} |
|
|
class Arguments |
{ |
|
int timeout; |
|
|
int maxtime; |
|
|
|
int follow_redirects = -1; |
|
|
mapping(string:string) headers; |
|
|
mapping(string:mixed) variables; |
|
|
void|string|mapping|Stdio.Stream data; |
|
|
function(Result:void) on_success; |
|
|
function(Result:void) on_failure; |
|
|
array(mixed) extra_args; |
|
|
|
|
protected void create(void|mapping(string:mixed) args) |
{ |
if (args) { |
foreach (args; string key; mixed value) { |
if (has_index(this, key)) { |
if ((< "variables", "headers" >)[key]) { |
|
value = mkmapping(indices(value), map(values(value), |
lambda (mixed s) { |
return (string) s; |
})); |
} |
|
this[key] = value; |
} |
else { |
error("Unknown argument %O!\n", key); |
} |
} |
} |
} |
} |
|
|
|
|
|
|
class Result |
{ |
|
protected mapping result; |
|
|
public bool `ok(); |
|
|
public mapping `headers() |
{ |
return result->headers; |
} |
|
|
public string `host() |
{ |
return result->host; |
} |
|
|
|
public int `status() |
{ |
return result->status; |
} |
|
|
public string `status_description() |
{ |
return result->status_desc; |
} |
|
|
public string `url() |
{ |
return result->url; |
} |
|
|
public array(mixed) `extra_args() |
{ |
return result->extra_args; |
} |
|
|
protected void create(mapping _result) |
{ |
|
result = _result; |
} |
|
} |
|
|
|
|
|
|
|
class Success |
{ |
inherit Result; |
|
public bool `ok() { return true; } |
|
|
public string `data() |
{ |
string data = result->data; |
|
if (content_encoding && content_encoding == "gzip") { |
data = Gz.uncompress(data[10..<8], true); |
} |
|
return data; |
} |
|
|
public int `length() |
{ |
return headers && (int)headers["content-length"]; |
} |
|
|
public string `content_type() |
{ |
if (string ct = (headers && headers["content-type"])) { |
sscanf (ct, "%s;", ct); |
return ct; |
} |
} |
|
|
|
public string `charset() |
{ |
if (string ce = (headers && headers["content-type"])) { |
if (sscanf (ce, "%*s;%*scharset=%s", ce) == 3) { |
if (ce[0] == '"' || ce[0] == '\'') { |
ce = ce[1..<1]; |
} |
return ce; |
} |
} |
} |
|
|
public string `content_encoding() |
{ |
if (string ce = (headers && headers["content-encoding"])) { |
return ce; |
} |
} |
} |
|
|
|
|
|
|
|
class Failure |
{ |
inherit Result; |
public bool `ok() { return false; } |
} |
|
|
|
public class Session |
{ |
inherit Protocols.HTTP.Session : parent; |
|
public int(0..) maxtime, timeout; |
public int maximum_connections_per_server = 20; |
|
Request async_do_method_url(string method, |
URL url, |
void|mapping query_variables, |
void|string|mapping|Stdio.Stream data, |
void|mapping extra_headers, |
function callback_headers_ok, |
function callback_data_ok, |
function callback_fail, |
array callback_arguments) |
{ |
if (stringp(url)) { |
url = Standards.URI(url); |
} |
|
|
|
if (!extra_headers || !extra_headers->host || !extra_headers->Host) { |
extra_headers = extra_headers || ([]); |
|
TRACE("Set host in headers: %O\n", url); |
|
if (url->scheme == "http") { |
extra_headers->host = url->host; |
if (url->port != 80) { |
extra_headers->host += ":" + url->port; |
} |
} |
else if (url->scheme == "https") { |
extra_headers->host = url->host; |
if (url->port != 443) { |
extra_headers->host += ":" + url->port; |
} |
} |
|
if (!sizeof(extra_headers)) { |
extra_headers = 0; |
} |
|
TRACE("Host header set?: %O\n", extra_headers); |
} |
|
if (upper_case(method) == "POST") { |
TRACE("Got post request: %O, %O, %O, %O\n", |
url, query_variables, data, extra_headers); |
|
|
bool has_content_len = false; |
bool has_content_type = false; |
|
if (extra_headers) { |
array(string) lc_headers = map(indices(extra_headers), lower_case); |
|
if (has_value(lc_headers, "content-length")) { |
has_content_len = true; |
} |
|
if (has_value(lc_headers, "content-type")) { |
has_content_type = true; |
} |
} |
|
if (!has_content_len) { |
mapping(string:string) qvars = url->get_query_variables(); |
data = data||""; |
|
if (qvars && sizeof(qvars)) { |
if (!query_variables) { |
query_variables = qvars; |
} |
else { |
query_variables |= qvars; |
} |
} |
|
if (sizeof(data) && query_variables) { |
data += "&" + Protocols.HTTP.http_encode_query(query_variables); |
} |
else if (query_variables) { |
data = Protocols.HTTP.http_encode_query(query_variables); |
} |
|
if (!extra_headers) { |
extra_headers = ([]); |
} |
|
extra_headers["Content-Length"] = (string) sizeof(data); |
|
if (!has_content_type) { |
extra_headers["Content-Type"] = "application/x-www-form-urlencoded"; |
} |
|
query_variables = 0; |
} |
} |
|
TRACE("Request: %O\n", url); |
|
|
|
|
|
|
|
return ::async_do_method_url(method, url, query_variables, data, |
extra_headers, callback_headers_ok, |
callback_data_ok, callback_fail, |
callback_arguments); |
} |
|
|
class Request |
{ |
inherit parent::Request; |
|
protected void set_extra_args_in_result(mapping(string:mixed) r) |
{ |
if (extra_callback_arguments && sizeof(extra_callback_arguments) > 1) { |
r->extra_args = extra_callback_arguments[1..]; |
} |
} |
|
protected void async_fail(SessionQuery q) |
{ |
TRACE("fail q: %O -> %O\n", q, ::url_requested); |
|
mapping ret = ([ |
"status" : q->status, |
"status_desc" : q->status_desc, |
"host" : q->host, |
"headers" : copy_value(q->headers), |
"url" : ::url_requested |
]); |
|
TRACE("Ret: %O\n", ret); |
|
set_extra_args_in_result(ret); |
|
|
con->set_callbacks(0, 0); |
|
function fc = fail_callback; |
set_callbacks(0, 0, 0); |
extra_callback_arguments = 0; |
|
if (fc) { |
fc(Failure(ret)); |
} |
} |
|
|
protected void async_ok(SessionQuery q) |
{ |
TRACE("async_ok: %O -> %s!\n", q->host, ::url_requested); |
|
::check_for_cookies(); |
|
if (con->status >= 300 && con->status < 400 && |
con->headers->location && follow_redirects) |
{ |
Standards.URI loc = Standards.URI(con->headers->location,url_requested); |
TRACE("New location: %O -> %O (%O)\n", url_requested, loc, con->headers); |
|
if (loc->scheme == "http" || loc->scheme == "https") { |
con->set_callbacks(0, 0); |
::destroy(); |
follow_redirects--; |
do_async(prepare_method("GET", loc)); |
return; |
} |
} |
|
|
con->set_callbacks(0, 0); |
|
if (data_callback) { |
con->async_fetch(async_data); |
} |
else { |
extra_callback_arguments = 0; |
} |
} |
|
|
protected void async_data() |
{ |
mapping ret = ([ |
"host" : con->host, |
"status" : con->status, |
"status_desc" : con->status_desc, |
"headers" : copy_value(con->headers), |
"data" : con->data(), |
"url" : ::url_requested |
]); |
|
set_extra_args_in_result(ret); |
|
|
con->set_callbacks(0, 0); |
|
if (data_callback) { |
data_callback(Success(ret)); |
} |
|
extra_callback_arguments = 0; |
} |
|
void destroy() |
{ |
TRACE("Destructor called in Request: %O\n", ::url_requested); |
::set_callbacks(0, 0, 0); |
::destroy(); |
} |
} |
|
|
class SessionQuery |
{ |
inherit parent::SessionQuery; |
|
protected void create() |
{ |
#if constant (this::maxtime) |
TRACE("# Query has maxtime\n"); |
if (Session::maxtime) { |
this::maxtime = Session::maxtime; |
} |
#endif |
|
if (Session::timeout) { |
this::timeout = Session::timeout; |
} |
} |
|
|
|
protected Stdio.Stream source_stream; |
protected int source_stream_length; |
|
this_program async_request (string server, |
int port, |
string query, |
void|mapping|string headers, |
void|string|Stdio.Stream data) |
{ |
if (objectp(data)) { |
set_source_stream(data); |
data = UNDEFINED; |
} |
if (source_stream) { |
if (data) |
error ("String data not allowed in streaming mode.\n"); |
if (stringp (headers)) |
error ("String headers not allowed in streaming mode.\n"); |
|
if (!headers) |
headers = ([]); |
headers["Content-Length"] = source_stream_length; |
} |
|
::async_request (server, port, query, headers, data); |
} |
|
protected void async_write() |
{ |
::async_write(); |
if (!con->query_write_callback() && source_stream) { |
|
Stdio.sendfile (0, source_stream, 0, source_stream_length, 0, con, |
lambda(int bytes_sent) { |
source_stream->close(); source_stream = 0; |
}); |
} |
} |
|
void set_source_stream (Stdio.Stream s) |
{ |
source_stream = s; |
source_stream_length = s->stat()->size; |
} |
|
this_program sync_request(string server, |
int port, |
string query, |
void|mapping|string http_headers, |
void|string|Stdio.Stream data) |
{ |
if (objectp(data)) { |
|
|
|
string tmp = data->read(0x7fffffff); |
data->close(); |
data = tmp; |
} |
return ::sync_request(server, port, query, http_headers, data); |
} |
|
|
} |
} |
|
|