diff options
Diffstat (limited to 'sca-cpp/branches/lightweight-sca/modules/http/http.hpp')
-rw-r--r-- | sca-cpp/branches/lightweight-sca/modules/http/http.hpp | 1042 |
1 files changed, 1042 insertions, 0 deletions
diff --git a/sca-cpp/branches/lightweight-sca/modules/http/http.hpp b/sca-cpp/branches/lightweight-sca/modules/http/http.hpp new file mode 100644 index 0000000000..408b9fdee5 --- /dev/null +++ b/sca-cpp/branches/lightweight-sca/modules/http/http.hpp @@ -0,0 +1,1042 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* $Rev$ $Date$ */ + +#ifndef tuscany_http_hpp +#define tuscany_http_hpp + +/** + * CURL HTTP client functions. + */ + +#include <unistd.h> +#include <curl/curl.h> +#include <curl/easy.h> +#include <apr.h> +#include <apr_lib.h> +#include <apr_network_io.h> +#include <apr_portable.h> +#include <apr_poll.h> +#include <apr_uri.h> + +#include "string.hpp" +#include "gc.hpp" +#include "list.hpp" +#include "value.hpp" +#include "element.hpp" +#include "monad.hpp" +#include "parallel.hpp" +#include "../scheme/io.hpp" +#include "../atom/atom.hpp" +#include "../rss/rss.hpp" +#include "../json/json.hpp" + +namespace tuscany { +namespace http { + +/** + * Enable CURL verbose debug log. + */ +//#define WANT_MAINTAINER_CURL_VERBOSE + +/** + * CURL library runtime, one per process. + */ +class CURLRuntime { +public: + CURLRuntime() { + curl_global_init(CURL_GLOBAL_ALL); + } +} curlRuntime; + +/** + * Represents a CURL session handle. + */ +class CURLSession { +public: + CURLSession() : h(NULL), p(), sock(NULL), wpollset(NULL), wpollfd(NULL), rpollset(NULL), rpollfd(NULL), owner(false), ca(""), cert(""), key(""), cookie(""), timeout(0) { + } + + CURLSession(const string& ca, const string& cert, const string& key, const string& cookie, const int timeout) : h(NULL), p(), sock(NULL), wpollset(NULL), wpollfd(NULL), rpollset(NULL), rpollfd(NULL), owner(true), ca(ca), cert(cert), key(key), cookie(cookie), timeout(timeout) { + } + + CURLSession(const CURLSession& c) : h(c.h), p(c.p), sock(c.sock), wpollset(c.wpollset), wpollfd(c.wpollfd), rpollset(c.rpollset), rpollfd(c.rpollfd), owner(false), ca(c.ca), cert(c.cert), key(c.key), cookie(c.cookie), timeout(c.timeout) { + } + + const CURLSession& operator=(const CURLSession& c) { + if(this == &c) + return *this; + h = c.h; + p = c.p; + sock = c.sock; + wpollset = c.wpollset; + wpollfd = c.wpollfd; + rpollset = c.rpollset; + rpollfd = c.rpollfd; + owner = false; + ca = c.ca; + cert = c.cert; + key = c.key; + cookie = c.cookie; + timeout = c.timeout; + return *this; + } + + ~CURLSession() { + if (!owner) + return; + if (h == NULL) + return; + curl_easy_cleanup(h); + } + +private: + CURL* h; + gc_child_pool p; + apr_socket_t* sock; + apr_pollset_t* wpollset; + apr_pollfd_t* wpollfd; + apr_pollset_t* rpollset; + apr_pollfd_t* rpollfd; + bool owner; + + friend CURL* handle(const CURLSession& cs); + friend apr_socket_t* sock(const CURLSession& cs); + friend const failable<CURL*> setup(const string& url, CURLSession& cs); + friend const failable<bool> cleanup(CURLSession& cs); + friend const failable<bool> connect(const string& url, CURLSession& cs); + friend const failable<bool> send(const char* c, const size_t l, CURLSession& cs); + friend const failable<size_t> recv(char* c, const size_t l, CURLSession& cs); + +public: + string ca; + string cert; + string key; + string cookie; + int timeout; +}; + +/** + * Returns the CURL handle used by a CURL session. + */ +CURL* handle(const CURLSession& cs) { + return cs.h; +} + +/** + * Return an apr_socket_t for the socket used by a CURL session. + */ +apr_socket_t* sock(const CURLSession& cs) { + return cs.sock; +} + +/** + * Convert a socket fd to an apr_socket_t. + */ +apr_socket_t* sock(const int sd, const gc_pool& p) { + int fd = sd; + apr_socket_t* s = NULL; + apr_os_sock_put(&s, &fd, pool(p)); + return s; +} + +/** + * Convert a CURL return code to an error string. + */ +const string curlreason(CURLcode rc) { + return curl_easy_strerror(rc); +} + +/** + * Convert an APR status to an error string. + */ +const string apreason(apr_status_t rc) { + char buf[256]; + return apr_strerror(rc, buf, sizeof(buf)); +} + +/** + * Escape a URI or a query argument. + */ +const char escape_c2x[] = "0123456789ABCDEF"; + +const string escape(const string& unesc, const char* reserv) { + char* copy = (char*)apr_palloc(gc_current_pool(), 3 * length(unesc) + 3); + const unsigned char* s = (const unsigned char *)c_str(unesc); + unsigned char* d = (unsigned char*)copy; + unsigned c; + while ((c = *s)) { + if (!apr_isalnum(c) && !strchr(reserv, c)) { + *d++ = '%'; + *d++ = escape_c2x[c >> 4]; + *d++ = escape_c2x[c & 0xf]; + } + else { + *d++ = (unsigned char)c; + } + ++s; + } + *d = '\0'; + return copy; +} + +const string escapeURI(const string& uri) { + debug(uri, "http::escapeURI::uri"); + const string e = escape(uri, "?$-_.+!*'(),:@&=/~%"); + debug(e, "http::escapeURI::result"); + return e; +} + +const string escapeArg(const string& arg) { + debug(arg, "http::escapeArg::arg"); + const string e = escape(arg, "-_.~"); + debug(e, "http::escapeArg::result"); + return e; +} + +/** + * Return true if a URI is absolute. + */ +const bool isAbsolute(const string& uri) { + return contains(uri, "://"); +} + +/** + * Parse a URI and return its host name. + */ +const string hostName(const string& uri, const gc_pool& p) { + apr_uri_t u; + const apr_status_t rc = apr_uri_parse(pool(p), c_str(uri), &u); + if (rc != APR_SUCCESS) + return ""; + if (u.hostname == NULL) + return ""; + return u.hostname; +} + +/** + * Parse a URI and return its scheme. + */ +const string scheme(const string& uri, const gc_pool& p) { + apr_uri_t u; + const apr_status_t rc = apr_uri_parse(pool(p), c_str(uri), &u); + if (rc != APR_SUCCESS) + return ""; + if (u.scheme == NULL) + return ""; + return u.scheme; +} + +/** + * Return the first subdomain name in a host name. + */ +const string subDomain(const string& host) { + return substr(host, 0, find(host, '.')); +} + +/** + * Return the top domain name in a host name. + */ +const string topDomain(const string& host) { + const size_t d = find(host, '.'); + return d == length(host) ? host : substr(host, d + 1); +} + +/** + * Setup a CURL session + */ +const failable<CURL*> setup(const string& url, CURLSession& cs) { + + // Init CURL session + if (cs.h != NULL) + cleanup(cs); + cs.h = curl_easy_init(); + CURL* ch = cs.h; + curl_easy_setopt(ch, CURLOPT_USERAGENT, "libcurl/1.0"); +#ifdef WANT_MAINTAINER_CURL_VERBOSE + curl_easy_setopt(ch, CURLOPT_VERBOSE, true); +#endif + + // Setup protocol options + curl_easy_setopt(ch, CURLOPT_TCP_NODELAY, true); + curl_easy_setopt(ch, CURLOPT_FOLLOWLOCATION, true); + curl_easy_setopt(ch, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); + curl_easy_setopt(ch, CURLOPT_NOSIGNAL, 1); + curl_easy_setopt(ch, CURLOPT_TIMEOUT, cs.timeout); + + // Setup SSL options + if (cs.ca != "") { + debug(cs.ca, "http::setup::ca"); + curl_easy_setopt(ch, CURLOPT_CAINFO, c_str(cs.ca)); + curl_easy_setopt(ch, CURLOPT_SSL_VERIFYPEER, true); + curl_easy_setopt(ch, CURLOPT_SSL_VERIFYHOST, 2); + } else + curl_easy_setopt(ch, CURLOPT_SSL_VERIFYPEER, false); + if (cs.cert != "") { + debug(cs.cert, "http::setup::cert"); + curl_easy_setopt(ch, CURLOPT_SSLCERT, c_str(cs.cert)); + curl_easy_setopt(ch, CURLOPT_SSLCERTTYPE, "PEM"); + } + if (cs.key != "") { + debug(cs.key, "http::setup::key"); + curl_easy_setopt(ch, CURLOPT_SSLKEY, c_str(cs.key)); + curl_easy_setopt(ch, CURLOPT_SSLKEYTYPE, "PEM"); + } + if (cs.cookie != "") { + debug(cs.cookie, "http::setup::cookie"); + curl_easy_setopt(ch, CURLOPT_COOKIE, c_str(cs.cookie)); + } + + // Set up HTTP basic auth if requested + apr_uri_t u; + apr_pool_t* p = gc_current_pool(); + const apr_status_t prc = apr_uri_parse(p, c_str(url), &u); + if (prc == APR_SUCCESS) { + if (u.user != NULL) { + debug(u.user, "http::setup::user"); + curl_easy_setopt(ch, CURLOPT_USERNAME, u.user); + } + if (u.password != NULL) { + debug(u.password, "http::setup::pass"); + curl_easy_setopt(ch, CURLOPT_PASSWORD, u.password); + } + if (u.user != NULL || u.password != NULL) { + curl_easy_setopt(ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + + // Set target URL, omitting the user:password part + curl_easy_setopt(ch, CURLOPT_URL, c_str(escapeURI(apr_uri_unparse(p, &u, APR_URI_UNP_OMITUSERINFO)))); + + return ch; + } + } + + // Set target URL + curl_easy_setopt(ch, CURLOPT_URL, c_str(escapeURI(url))); + + return ch; +} + +/** + * Cleanup a CURL session + */ +const failable<bool> cleanup(CURLSession& cs) { + if (cs.h == NULL) + return true; + curl_easy_cleanup(cs.h); + cs.h = NULL; + return true; +} + +/** + * Context passed to the read callback function. + */ +class CURLReadContext { +public: + CURLReadContext(const list<string>& ilist) : ilist(ilist) { + } + list<string> ilist; +}; + +/** + * Called by CURL to read data to send. + */ +size_t readCallback(void *ptr, size_t size, size_t nmemb, void *data) { + CURLReadContext& rcx = *static_cast<CURLReadContext*>(data); + if (isNil(rcx.ilist)) + return 0; + const list<string> f(fragment(rcx.ilist, size * nmemb)); + const string s = car(f); + rcx.ilist = cdr(f); + memcpy(ptr, c_str(s), length(s)); + return length(s); +} + +/** + * Context passed to CURL write callback function. + */ +template<typename R> class CURLWriteContext { +public: + CURLWriteContext(const lambda<R(const string&, const R)>& reduce, const R& accum) : reduce(reduce), accum(accum) { + } + const lambda<R(const string&, const R)> reduce; + R accum; +}; + +/** + * Called by CURL to write received data. + */ +template<typename R> size_t writeCallback(void *ptr, size_t size, size_t nmemb, void *data) { + CURLWriteContext<R>& wcx = *(static_cast<CURLWriteContext<R>*> (data)); + const size_t realsize = size * nmemb; + wcx.accum = wcx.reduce(string((const char*)ptr, realsize), wcx.accum); + return realsize; +} + +/** + * Apply an HTTP verb to a list containing a list of headers and a list of content, and + * a reduce function used to process the response. + */ +curl_slist* headers(curl_slist* cl, const list<string>& h) { + if (isNil(h)) + return cl; + return headers(curl_slist_append(cl, c_str(string(car(h)))), cdr(h)); +} + +template<typename R> const failable<list<R> > apply(const list<list<string> >& hdr, const lambda<R(const string&, const R)>& reduce, const R& initial, const string& url, const string& verb, CURLSession& cs) { + debug(url, "http::apply::url"); + debug(verb, "http::apply::verb"); + + // Setup the CURL session + const failable<CURL*> fch = setup(url, cs); + if (!hasContent(fch)) { + cleanup(cs); + return mkfailure<list<R>>(fch); + } + CURL* ch = content(fch); + + // Set the request headers + curl_slist* hl = headers(NULL, car(hdr)); + if (hl != NULL) + curl_easy_setopt(ch, CURLOPT_HTTPHEADER, hl); + + // Convert request body to a string + // TODO use HTTP chunking instead + ostringstream os; + write(cadr(hdr), os); + const string s = str(os); + const size_t sz = length(s); + + // Setup the read, write header and write data callbacks + CURLReadContext rcx(mklist(s)); + curl_easy_setopt(ch, CURLOPT_READFUNCTION, (size_t (*)(void*, size_t, size_t, void*))readCallback); + curl_easy_setopt(ch, CURLOPT_READDATA, &rcx); + CURLWriteContext<R> hcx(reduce, initial); + curl_easy_setopt(ch, CURLOPT_HEADERFUNCTION, (size_t (*)(void*, size_t, size_t, void*))(writeCallback<R>)); + curl_easy_setopt(ch, CURLOPT_HEADERDATA, &hcx); + CURLWriteContext<R> wcx(reduce, initial); + curl_easy_setopt(ch, CURLOPT_WRITEFUNCTION, (size_t (*)(void*, size_t, size_t, void*))(writeCallback<R>)); + curl_easy_setopt(ch, CURLOPT_WRITEDATA, &wcx); + + // Apply the HTTP verb + if (verb == "POST") { + curl_easy_setopt(ch, CURLOPT_POST, true); + curl_easy_setopt(ch, CURLOPT_POSTFIELDSIZE, sz); + } else if (verb == "PUT") { + curl_easy_setopt(ch, CURLOPT_UPLOAD, true); + curl_easy_setopt(ch, CURLOPT_INFILESIZE, sz); + } else if (verb == "PATCH") { + curl_easy_setopt(ch, CURLOPT_UPLOAD, true); + curl_easy_setopt(ch, CURLOPT_CUSTOMREQUEST, "PATCH"); + curl_easy_setopt(ch, CURLOPT_INFILESIZE, sz); + } else if (verb == "DELETE") + curl_easy_setopt(ch, CURLOPT_CUSTOMREQUEST, "DELETE"); + const CURLcode rc = curl_easy_perform(ch); + + // Free the headers + if (hl != NULL) + curl_slist_free_all(hl); + + // Return the HTTP return code or content + if (rc) { + cleanup(cs); + return mkfailure<list<R> >(string(curl_easy_strerror(rc))); + } + long httprc; + curl_easy_getinfo (ch, CURLINFO_RESPONSE_CODE, &httprc); + if (httprc != 200 && httprc != 201) { + cleanup(cs); + ostringstream es; + es << "HTTP code " << httprc; + return mkfailure<list<R> >(str(es)); + } + + cleanup(cs); + return mklist<R>(hcx.accum, wcx.accum); +} + +/** + * Evaluate an expression remotely, at the given URL. + */ +const failable<value> evalExpr(const value& expr, const string& url, CURLSession& cs) { + debug(url, "http::evalExpr::url"); + debug(expr, "http::evalExpr::input"); + + // Convert expression to a JSON-RPC request + js::JSContext cx; + const failable<list<string> > jsreq = json::jsonRequest(1, car<value>(expr), cdr<value>(expr), cx); + if (!hasContent(jsreq)) + return mkfailure<value>(jsreq); + + // POST it to the URL + const list<string> h = mklist<string>("Content-Type: application/json-rpc"); + const failable<list<list<string> > > res = apply<list<string> >(mklist<list<string> >(h, content(jsreq)), rcons<string>, list<string>(), url, "POST", cs); + if (!hasContent(res)) + return mkfailure<value>(res); + + // Parse and return JSON-RPC result + const failable<value> rval = json::jsonResultValue(cadr<list<string> >(content(res)), cx); + debug(rval, "http::evalExpr::result"); + if (!hasContent(rval)) + return mkfailure<value>(rval); + return content(rval); +} + +/** + * Find and return a header. + */ +const maybe<string> header(const char* prefix, const list<string>& h) { + if (isNil(h)) + return maybe<string>(); + const string s = car(h); + if (find(s, prefix) != 0) + return header(prefix, cdr(h)); + const string l(substr(s, length(prefix))); + return substr(l, 0, find_first_of(l, "\r\n")); +} + +/** + * Find and return a location header. + */ +const string location(const list<string>& h) { + const maybe<string> l = header("Location: ", h); + return hasContent(l)? content(l) : ""; +} + +/** + * Convert a location to an entry id. + */ +const value entryId(const failable<string> l) { + if (!hasContent(l)) + return list<value>(); + const string ls(content(l)); + return value(mklist<value>(string(substr(ls, find_last(ls, '/') + 1)))); +} + +/** + * Find and return a content-type header. + */ +const string contentType(const list<string>& h) { + const maybe<string> ct = header("Content-Type: ", h); + return hasContent(ct)? content(ct) : ""; +} + +/** + * HTTP GET, return the resource at the given URL. + */ +template<typename R> const failable<list<R> > get(const lambda<R(const string&, const R)>& reduce, const R& initial, const string& url, CURLSession& cs) { + debug(url, "http::get::url"); + const list<list<string> > req = mklist(list<string>(), list<string>()); + return apply(req, reduce, initial, url, "GET", cs); +} + +/** + * HTTP GET, return a list of values representing the resource at the given URL. + */ +const failable<value> getcontent(const string& url, CURLSession& cs) { + debug(url, "http::get::url"); + + // Get the contents of the resource at the given URL + const failable<list<list<string> > > res = get<list<string>>(rcons<string>, list<string>(), url, cs); + if (!hasContent(res)) + return mkfailure<value>(res); + const list<string> ls(reverse(cadr(content(res)))); + + // Return the content as a list of values + const value val(mkvalues(ls)); + debug(val, "http::get::result"); + return val; +} + +/** + * Convert an HTTP content response to a value. + */ +const failable<value> responseValue(const list<list<string> > res) { + + // Parse the returned content + const string ct = contentType(car(res)); + debug(ct, "http::responseValue::contentType"); + + const list<string> ls(reverse(cadr(res))); + debug(ls, "http::responseValue::content"); + + if (atom::isATOMEntry(ls)) { + // Read an ATOM entry + const value val(elementsToValues(content(atom::readATOMEntry(ls)))); + debug(val, "http::responseValue::result"); + return val; + } + if (contains(ct, "application/atom+xml") || atom::isATOMFeed(ls)) { + // Read an ATOM feed + const value val(elementsToValues(content(atom::readATOMFeed(ls)))); + debug(val, "http::responseValue::result"); + return val; + } + if (contains(ct, "application/rss+xml") || rss::isRSSFeed(ls)) { + // Read an RSS feed + const value val(elementsToValues(content(rss::readRSSFeed(ls)))); + debug(val, "http::responseValue::result"); + return val; + } + if (contains(ct, "text/javascript") || contains(ct, "application/json") || json::isJSON(ls)) { + // Read a JSON document + js::JSContext cx; + const value val(json::jsonValues(content(json::readJSON(ls, cx)))); + debug(val, "http::responseValue::result"); + return val; + } + if (contains(ct, "application/x-javascript")) { + // Read a JSON document enclosed in a javascript function call + // Extract the JSON out of the enclosing parenthesis + ostringstream os; + write(ls, os); + const string s = str(os); + const size_t fp = find(s, '('); + const size_t lp = find_last(s, ')'); + const list<string> jls = mklist<string>(substr(s, fp + 1, lp - (fp + 1))); + debug(jls, "http::responseValue::javascript::content"); + + js::JSContext cx; + const value val(json::jsonValues(content(json::readJSON(jls, cx)))); + debug(val, "http::responseValue::result"); + return val; + } + if (contains(ct, "text/xml") || contains(ct, "application/xml") || isXML(ls)) { + // Read an XML document + const value val(elementsToValues(readXML(ls))); + debug(val, "http::responseValue::result"); + return val; + } + + // Return the content type and a content list + const value val(mklist<value>(ct, mkvalues(ls))); + debug(val, "http::responseValue::result"); + return val; +} + +/** + * HTTP GET, return a list of values representing the resource at the given URL. + */ +const failable<value> get(const string& url, CURLSession& cs) { + debug(url, "http::get::url"); + + // Get the contents of the resource at the given URL + const failable<list<list<string> > > res = get<list<string> >(rcons<string>, list<string>(), url, cs); + if (!hasContent(res)) + return mkfailure<value>(res); + + // Parse the returned content + return responseValue(content(res)); +} + +/** + * Form an HTTP content request. + */ +const failable<list<list<string> > > writeRequest(const failable<list<string> >& ls, const string& ct) { + if (!hasContent(ls)) + return mkfailure<list<list<string> > >(ls); + const list<list<string> > req = mklist<list<string> >(mklist<string>(string("Content-Type: ") + ct), content(ls)); + debug(req, "http::writeRequest::req"); + return req; +} + +/** + * Convert a value to an HTTP content request. + */ +const failable<list<list<string> > > contentRequest(const value& c, unused const string& url) { + + // Check if the client requested a specific format + //TODO derive that from given URL + const list<value> fmt = assoc<value>("format", list<value>()); + + // Write as a scheme value if requested by the client + if (!isNil(fmt) && cadr(fmt) == "scheme") + return writeRequest(mklist<string>(scheme::writeValue(c)), "text/plain; charset=utf-8"); + + // Write a simple value as a JSON value + if (!isList(c)) { + js::JSContext cx; + if (isSymbol(c)) { + const list<value> lc = mklist<value>(mklist<value>("name", value(string(c)))); + debug(lc, "http::contentRequest::symbol"); + return writeRequest(json::writeJSON(valuesToElements(lc), cx), "application/json; charset=utf-8"); + } + const list<value> lc = mklist<value>(mklist<value>("value", c)); + debug(lc, "http::contentRequest::value"); + return writeRequest(json::writeJSON(valuesToElements(lc), cx), "application/json; charset=utf-8"); + } + + // Write an empty list as a JSON empty value + if (isNil((list<value>)c)) { + js::JSContext cx; + debug(list<value>(), "http::contentRequest::empty"); + return writeRequest(json::writeJSON(list<value>(), cx), "application/json; charset=utf-8"); + } + + // Write content-type / content-list pair + if (isString(car<value>(c)) && !isNil(cdr<value>(c)) && isList(cadr<value>(c))) + return writeRequest(convertValues<string>(cadr<value>(c)), car<value>(c)); + + // Write an assoc value as JSON + if (isSymbol(car<value>(c)) && !isNil(cdr<value>(c))) { + js::JSContext cx; + const list<value> lc = mklist<value>(c); + debug(lc, "http::contentRequest::assoc"); + debug(valuesToElements(lc), "http::contentRequest::assoc::element"); + return writeRequest(json::writeJSON(valuesToElements(lc), cx), "application/json; charset=utf-8"); + } + + // Write value as JSON if requested by the client + if (!isNil(fmt) && cadr(fmt) == "json") { + js::JSContext cx; + return writeRequest(json::writeJSON(valuesToElements(c), cx), "application/json; charset=utf-8"); + } + + // Convert list of values to element values + const list<value> e = valuesToElements(c); + debug(e, "http::contentRequest::elements"); + + // Write an ATOM feed or entry + if (isList(car<value>(e)) && !isNil(car<value>(e))) { + const list<value> el = car<value>(e); + if (isSymbol(car<value>(el)) && car<value>(el) == element && !isNil(cdr<value>(el)) && isSymbol(cadr<value>(el)) && elementHasChildren(el) && !elementHasValue(el)) { + if (cadr<value>(el) == atom::feed) + return writeRequest(atom::writeATOMFeed(e), "application/atom+xml; charset=utf-8"); + if (cadr<value>(el) == atom::entry) + return writeRequest(atom::writeATOMEntry(e), "application/atom+xml; charset=utf-8"); + } + } + + // Write any other compound value as a JSON value + js::JSContext cx; + return writeRequest(json::writeJSON(e, cx), "application/json; charset=utf-8"); +} + +/** + * HTTP POST. + */ +const failable<value> post(const value& val, const string& url, CURLSession& cs) { + debug(url, "http::post::url"); + + // Convert value to a content request + const failable<list<list<string> > > req = contentRequest(val, url); + if (!hasContent(req)) + return mkfailure<value>(req); + debug(content(req), "http::post::input"); + + // POST it to the URL + const failable<list<list<string> > > res = apply<list<string>>(content(req), rcons<string>, list<string>(), url, "POST", cs); + if (!hasContent(res)) + return mkfailure<value>(res); + + // Return the new entry id from the HTTP location header, if any + const string loc = location(car(content(res))); + if (length(loc) != 0) { + const value eid(entryId(location(car(content(res))))); + debug(eid, "http::post::result"); + return eid; + } + + // Return the returned content + return responseValue(content(res)); +} + +/** + * HTTP PUT. + */ +const failable<value> put(const value& val, const string& url, CURLSession& cs) { + debug(url, "http::put::url"); + + // Convert value to a content request + const failable<list<list<string> > > req = contentRequest(val, url); + if (!hasContent(req)) + return mkfailure<value>(req); + debug(content(req), "http::put::input"); + + // PUT it to the URL + const failable<list<list<string> > > res = apply<list<string> >(content(req), rcons<string>, list<string>(), url, "PUT", cs); + if (!hasContent(res)) + return mkfailure<value>(res); + + debug(true, "http::put::result"); + return value(true); +} + +/** + * HTTP PATCH. + */ +const failable<value> patch(const value& val, const string& url, CURLSession& cs) { + debug(url, "http::put::patch"); + + // Convert value to a content request + const failable<list<list<string> > > req = contentRequest(val, url); + if (!hasContent(req)) + return mkfailure<value>(req); + debug(content(req), "http::patch::input"); + + // PATCH it to the URL + const failable<list<list<string> > > res = apply<list<string> >(content(req), rcons<string>, list<string>(), url, "PATCH", cs); + if (!hasContent(res)) + return mkfailure<value>(res); + + debug(true, "http::patch::result"); + return value(true); +} + +/** + * HTTP DELETE. + */ +const failable<value, string> del(const string& url, CURLSession& cs) { + debug(url, "http::delete::url"); + + const list<list<string> > req = mklist(list<string>(), list<string>()); + const failable<list<list<string> > > res = apply<list<string> >(req, rcons<string>, list<string>(), url, "DELETE", cs); + if (!hasContent(res)) + return mkfailure<value>(res); + + debug(true, "http::delete::result"); + return value(true); +} + +/** + * Returns the current host name. + */ +const string hostName() { + char h[256]; + if (gethostname(h, 256) == -1) + return "localhost"; + return h; +} + +/** + * Create an APR pollfd for a socket. + */ +apr_pollfd_t* pollfd(apr_socket_t* s, const int e, const gc_pool& p) { + apr_pollfd_t* pfd = gc_new<apr_pollfd_t>(p); + pfd->p = pool(p); + pfd->desc_type = APR_POLL_SOCKET; + pfd->reqevents = (apr_int16_t)e; + pfd->rtnevents = (apr_int16_t)e; + pfd->desc.s = s; + pfd->client_data = NULL; + return pfd; +} + +/** + * Connect to a URL. + */ +const failable<bool> connect(const string& url, CURLSession& cs) { + debug(url, "http::connect::url"); + + // Setup the CURL session + const failable<CURL*> fch = setup(url, cs); + if (!hasContent(fch)) { + cleanup(cs); + return mkfailure<bool>(fch); + } + CURL* ch = content(fch); + + // Connect + curl_easy_setopt(ch, CURLOPT_CONNECT_ONLY, true); + const CURLcode rc = curl_easy_perform(ch); + if (rc) { + cleanup(cs); + return mkfailure<bool>(string(curl_easy_strerror(rc))); + } + + // Convert the connected socket to an apr_socket_t + int sd; + const CURLcode grc = curl_easy_getinfo(ch, CURLINFO_LASTSOCKET, &sd); + if (grc) { + cleanup(cs); + return mkfailure<bool>(string(curl_easy_strerror(grc))); + } + cs.sock = sock(sd, cs.p); + + // Create pollsets and pollfds which can be used to poll the socket + apr_status_t rpcrc = apr_pollset_create(&cs.rpollset, 1, pool(cs.p), 0); + if (rpcrc != APR_SUCCESS) { + cleanup(cs); + return mkfailure<bool>(apreason(rpcrc)); + } + cs.rpollfd = pollfd(cs.sock, APR_POLLIN, cs.p); + apr_pollset_add(cs.rpollset, cs.rpollfd); + apr_status_t wpcrc = apr_pollset_create(&cs.wpollset, 1, pool(cs.p), 0); + if (wpcrc != APR_SUCCESS) { + cleanup(cs); + return mkfailure<bool>(apreason(wpcrc)); + } + cs.wpollfd = pollfd(cs.sock, APR_POLLOUT, cs.p); + apr_pollset_add(cs.wpollset, cs.wpollfd); + + return true; +} + +/** + * Send an array of chars. + */ +const failable<bool> send(const char* c, const size_t l, CURLSession& cs) { + + // Send the data + size_t wl = 0; + const CURLcode rc = curl_easy_send(cs.h, c, (size_t)l, &wl); + if (rc == CURLE_OK && wl == (size_t)l) + return true; + if (rc != CURLE_AGAIN) { + cleanup(cs); + return mkfailure<bool>(curlreason(rc)); + } + + // If the socket was not ready, wait for it to become ready + const apr_pollfd_t* pollfds; + apr_int32_t pollcount; + apr_status_t pollrc = apr_pollset_poll(cs.wpollset, -1, &pollcount, &pollfds); + if (pollrc != APR_SUCCESS) + return mkfailure<bool>(apreason(pollrc)); + + // Send what's left + return send(c + wl, l - wl, cs); +} + +/** + * Receive an array of chars. + */ +const failable<size_t> recv(char* c, const size_t l, CURLSession& cs) { + + // Receive data + size_t rl; + const CURLcode rc = curl_easy_recv(cs.h, c, (size_t)l, &rl); + if (rc == CURLE_OK) + return (size_t)rl; + if (rc == 1) + return 0; + if (rc != CURLE_AGAIN) { + cleanup(cs); + return mkfailure<size_t>(curlreason(rc)); + } + + // If the socket was not ready, wait for it to become ready + const apr_pollfd_t* pollfds; + apr_int32_t pollcount; + apr_status_t pollrc = apr_pollset_poll(cs.rpollset, -1, &pollcount, &pollfds); + if (pollrc != APR_SUCCESS) { + cleanup(cs); + return mkfailure<size_t>(apreason(pollrc)); + } + + // Receive again + return recv(c, l, cs); +} + +/** + * Converts a list of key value pairs to a query string. + */ +ostringstream& queryString(const list<list<value> > args, ostringstream& os) { + if (isNil(args)) + return os; + const list<value> arg = car(args); + debug(arg, "http::queryString::arg"); + if (isNil(arg) || isNil(cdr(arg))) + return queryString(cdr(args), os); + os << car(arg) << "=" << c_str(cadr(arg)); + if (!isNil(cdr(args))) + os << "&"; + return queryString(cdr(args), os); +} + +const string queryString(const list<list<value> > args) { + ostringstream os; + return str(queryString(args, os)); +} + +/** + * Filter path segment in a list of arguments. + */ +const bool filterPath(const value& arg) { + return isString(arg) || isSymbol(arg); +} + +/** + * Filter query string arguments in a list of arguments. + */ +const bool filterQuery(const value& arg) { + return isList(arg); +} + +/** + * Escape a query string argument. + */ +const value escapeQuery(const value& arg) { + return arg; + //return mklist<value>(car<value>(arg), escapeArg(cadr<value>(arg))); +} + +/** + * HTTP client proxy function. + */ +struct proxy { + proxy(const string& uri, const string& ca, const string& cert, const string& key, const string& cookie, const int timeout, const gc_pool& p) : p(p), uri(uri), ca(ca), cert(cert), key(key), cookie(cookie), cs(*(new (gc_new<CURLSession>(p)) CURLSession(ca, cert, key, cookie, timeout))) { + } + + const value operator()(const list<value>& args) const { + debug(args, "http::proxy::args"); + const value fun = car(args); + if (fun == "get") { + const list<value> lp = filter<value>(filterPath, cadr(args)); + debug(lp, "http::proxy::path"); + const list<value> lq = map<value, value>(escapeQuery, filter<value>(filterQuery, cadr(args))); + debug(lp, "http::proxy::query"); + const value p = path(lp); + const value q = queryString(lq); + const failable<value> val = get(uri + p + (q != ""? string("?") + q : string("")), cs); + return content(val); + } + if (fun == "post") { + const failable<value> val = post(caddr(args), uri + path(cadr(args)), cs); + return content(val); + } + if (fun == "put") { + const failable<value> val = put(caddr(args), uri + path(cadr(args)), cs); + return content(val); + } + if (fun == "patch") { + const failable<value> val = patch(caddr(args), uri + path(cadr(args)), cs); + return content(val); + } + if (fun == "delete") { + const failable<value> val = del(uri + path(cadr(args)), cs); + return content(val); + } + const failable<value> val = evalExpr(args, uri, cs); + return content(val); + } + + const gc_pool p; + const string uri; + const string ca; + const string cert; + const string key; + const string cookie; + CURLSession& cs; +}; + +} +} + +#endif /* tuscany_http_hpp */ |