| /* |
| * 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() : p(), h(*(new (gc_new<handles>()) handles())), owner(false), ca(emptyString), cert(emptyString), key(emptyString), cookie(emptyString), timeout(0) { |
| } |
| |
| CURLSession(const string& ca, const string& cert, const string& key, const string& cookie, const int timeout) : p(), h(*(new (gc_new<handles>()) handles())), owner(true), ca(ca), cert(cert), key(key), cookie(cookie), timeout(timeout) { |
| } |
| |
| CURLSession(const CURLSession& c) : p(c.p), h(c.h), owner(false), ca(c.ca), cert(c.cert), key(c.key), cookie(c.cookie), timeout(c.timeout) { |
| } |
| |
| CURLSession& operator=(const CURLSession& c) = delete; |
| |
| ~CURLSession() { |
| if (!owner) |
| return; |
| if (h.h == NULL) |
| return; |
| debug(h.h, "http::~CURLSession::cleanup::h"); |
| curl_easy_cleanup(h.h); |
| } |
| |
| private: |
| class handles { |
| public: |
| handles() : h(NULL), sock(NULL), wpollset(NULL), wpollfd(NULL), rpollset(NULL), rpollfd(NULL) { |
| } |
| |
| handles(const handles& c) : h(c.h), sock(c.sock), wpollset(c.wpollset), wpollfd(c.wpollfd), rpollset(c.rpollset), rpollfd(c.rpollfd) { |
| } |
| |
| private: |
| CURL* h; |
| apr_socket_t* sock; |
| apr_pollset_t* wpollset; |
| apr_pollfd_t* wpollfd; |
| apr_pollset_t* rpollset; |
| apr_pollfd_t* rpollfd; |
| |
| friend class CURLSession; |
| friend CURL* const handle(const CURLSession& cs); |
| friend apr_socket_t* const sock(const CURLSession& cs); |
| friend const failable<CURL*> setup(const string& url, const CURLSession& cs); |
| friend const failable<bool> cleanup(const CURLSession& cs); |
| friend const failable<bool> connect(const string& url, const CURLSession& cs); |
| friend const failable<bool> send(const char* const c, const size_t l, const CURLSession& cs); |
| friend const failable<size_t> recv(char* const c, const size_t l, const CURLSession& cs); |
| }; |
| |
| const gc_child_pool p; |
| handles& h; |
| const bool owner; |
| |
| friend CURL* const handle(const CURLSession& cs); |
| friend apr_socket_t* const sock(const CURLSession& cs); |
| friend const failable<CURL*> setup(const string& url, const CURLSession& cs); |
| friend const failable<bool> cleanup(const CURLSession& cs); |
| friend const failable<bool> connect(const string& url, const CURLSession& cs); |
| friend const failable<bool> send(const char* const c, const size_t l, const CURLSession& cs); |
| friend const failable<size_t> recv(char* const c, const size_t l, const CURLSession& cs); |
| |
| public: |
| const string ca; |
| const string cert; |
| const string key; |
| const string cookie; |
| const int timeout; |
| }; |
| |
| /** |
| * Returns the CURL handle used by a CURL session. |
| */ |
| CURL* const handle(const CURLSession& cs) { |
| return cs.h.h; |
| } |
| |
| /** |
| * Return an apr_socket_t for the socket used by a CURL session. |
| */ |
| apr_socket_t* const sock(const CURLSession& cs) { |
| return cs.h.sock; |
| } |
| |
| /** |
| * Convert a socket fd to an apr_socket_t. |
| */ |
| apr_socket_t* const 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(const CURLcode rc) { |
| return curl_easy_strerror(rc); |
| } |
| |
| /** |
| * Convert an APR status to an error string. |
| */ |
| const string apreason(const 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* const 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 emptyString; |
| if (u.hostname == NULL) |
| return emptyString; |
| 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 emptyString; |
| if (u.scheme == NULL) |
| return emptyString; |
| 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, const CURLSession& cs) { |
| |
| // Init CURL session |
| if (cs.h.h != NULL) |
| cleanup(cs); |
| cs.h.h = curl_easy_init(); |
| debug(cs.h.h, "http::setup::init::h"); |
| CURL* const ch = cs.h.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_DNS_USE_GLOBAL_CACHE, 0); |
| curl_easy_setopt(ch, CURLOPT_TIMEOUT, cs.timeout); |
| |
| // Setup SSL options |
| if (cs.ca != emptyString) { |
| 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 != emptyString) { |
| 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 != emptyString) { |
| 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 != emptyString) { |
| 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* const 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(const CURLSession& cs) { |
| if (cs.h.h == NULL) |
| return true; |
| debug(cs.h.h, "http::cleanup::cleanup::h"); |
| curl_easy_cleanup(cs.h.h); |
| cs.h.h = NULL; |
| return true; |
| } |
| |
| /** |
| * Context passed to the read callback function. |
| */ |
| class CURLReadContext { |
| public: |
| CURLReadContext(const list<string>& ilist) : ilist(ilist) { |
| } |
| |
| gc_mutable_ref<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 = *(CURLReadContext*)data; |
| if (isNil((const list<string>)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<const R(const string&, const R)>& reduce, const R& accum) : reduce(reduce), accum(accum) { |
| } |
| |
| const lambda<const R(const string&, const R)> reduce; |
| gc_mutable_ref<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 = *(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* const 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<const R(const string&, const R)>& reduce, const R& initial, const string& url, const string& verb, const 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* const ch = content(fch); |
| |
| // Set the request headers |
| curl_slist* hl = headers(NULL, car(hdr)); |
| hl = curl_slist_append(hl, "X-Accept: text/x-scheme; charset=utf-8"); |
| 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); |
| return mkfailure<list<R> >(string("HTTP code not 200"), (int)httprc, (httprc != 301 && httprc != 302 && httprc != 404)); |
| } |
| |
| 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, const CURLSession& cs) { |
| debug(url, "http::evalExpr::url"); |
| debug(expr, "http::evalExpr::input"); |
| |
| // Convert expression to a JSON-RPC request |
| const failable<list<string> > jsreq = json::jsonRequest(1, car<value>(expr), cdr<value>(expr)); |
| 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))); |
| 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* const 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) : emptyString; |
| } |
| |
| /** |
| * Convert a location to an entry id. |
| */ |
| const value entryId(const failable<string> l) { |
| if (!hasContent(l)) |
| return nilListValue; |
| 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) : emptyString; |
| } |
| |
| /** |
| * HTTP GET, return the resource at the given URL. |
| */ |
| template<typename R> const failable<list<R> > get(const lambda<const R(const string&, const R)>& reduce, const R& initial, const string& url, const 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, const 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 (contains(ct, "text/x-scheme")) { |
| // Read a Scheme value |
| ostringstream os; |
| write(ls, os); |
| istringstream is(str(os)); |
| const value val(content(scheme::readValue(is))); |
| debug(val, "http::responseValue::result"); |
| return val; |
| } |
| 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 |
| const value val(content(json::readValue(ls))); |
| 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"); |
| |
| const value val(content(json::readValue(jls))); |
| debug(val, "http::responseValue::result"); |
| return val; |
| } |
| if (contains(ct, "text/xml") || contains(ct, "application/xml") || xml::isXML(ls)) { |
| // Read an XML document |
| const value val(elementsToValues(content(xml::readElements(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, const 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) { |
| |
| // Write in the format requested by the client, if any |
| const list<value> fmt = assoc<value>("format", nilListValue); |
| if (!isNil(fmt)) { |
| if (cadr(fmt) == "scheme") |
| return writeRequest(scheme::writeValue(c), "text/x-scheme; charset=utf-8"); |
| if (cadr(fmt) == "json") |
| return writeRequest(json::writeValue(c), "application/json; charset=utf-8"); |
| if (cadr(fmt) == "xml") |
| return writeRequest(xml::writeElements(valuesToElements(c)), "text/xml; charset=utf-8"); |
| } |
| |
| // Write a simple value as a JSON value |
| if (!isList(c)) { |
| debug(c, "http::contentRequest::value"); |
| return writeRequest(json::writeValue(c), "application/json; charset=utf-8"); |
| } |
| |
| // Write an empty list as a JSON value |
| if (isNil((list<value>)c)) { |
| debug(nilListValue, "http::contentRequest::empty"); |
| return writeRequest(json::writeValue(c), "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 a JSON value |
| if (isSymbol(car<value>(c)) && !isNil(cdr<value>(c))) { |
| debug(c, "http::contentRequest::assoc"); |
| return writeRequest(json::writeValue(c), "application/json; charset=utf-8"); |
| } |
| |
| // Write an ATOM feed or entry |
| const list<value> e = valuesToElements(c); |
| 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 |
| return writeRequest(json::writeValue(c), "application/json; charset=utf-8"); |
| } |
| |
| /** |
| * HTTP POST. |
| */ |
| const failable<value> post(const value& val, const string& url, const 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, const 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 trueValue; |
| } |
| |
| /** |
| * HTTP PATCH. |
| */ |
| const failable<value> patch(const value& val, const string& url, const 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 trueValue; |
| } |
| |
| /** |
| * HTTP DELETE. |
| */ |
| const failable<value, string> del(const string& url, const 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 trueValue; |
| } |
| |
| /** |
| * 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* const pollfd(apr_socket_t* const 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, const 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* const 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.h.sock = sock(sd, cs.p); |
| |
| // Create pollsets and pollfds which can be used to poll the socket |
| const apr_status_t rpcrc = apr_pollset_create(&cs.h.rpollset, 1, pool(cs.p), 0); |
| if (rpcrc != APR_SUCCESS) { |
| cleanup(cs); |
| return mkfailure<bool>(apreason(rpcrc)); |
| } |
| cs.h.rpollfd = pollfd(cs.h.sock, APR_POLLIN, cs.p); |
| apr_pollset_add(cs.h.rpollset, cs.h.rpollfd); |
| const apr_status_t wpcrc = apr_pollset_create(&cs.h.wpollset, 1, pool(cs.p), 0); |
| if (wpcrc != APR_SUCCESS) { |
| cleanup(cs); |
| return mkfailure<bool>(apreason(wpcrc)); |
| } |
| cs.h.wpollfd = pollfd(cs.h.sock, APR_POLLOUT, cs.p); |
| apr_pollset_add(cs.h.wpollset, cs.h.wpollfd); |
| |
| return true; |
| } |
| |
| /** |
| * Send an array of chars. |
| */ |
| const failable<bool> send(const char* const c, const size_t l, const CURLSession& cs) { |
| |
| // Send the data |
| size_t wl = 0; |
| const CURLcode rc = curl_easy_send(cs.h.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; |
| const apr_status_t pollrc = apr_pollset_poll(cs.h.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* const c, const size_t l, const CURLSession& cs) { |
| |
| // Receive data |
| size_t rl; |
| const CURLcode rc = curl_easy_recv(cs.h.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; |
| const apr_status_t pollrc = apr_pollset_poll(cs.h.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. |
| */ |
| class proxy { |
| public: |
| proxy(const string& uri, const string& ca, const string& cert, const string& key, const string& cookie, const int timeout) : uri(uri), cs(mksession(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(lq, "http::proxy::query"); |
| const value q = queryString(lq); |
| const failable<value> val = get(uri + (string)path(lp) + (q != emptyString? string("?") + (string)q : emptyString), *cs); |
| return content(val); |
| } |
| if (fun == "post") { |
| const failable<value> val = post(caddr(args), uri + (string)path(cadr(args)), *cs); |
| return content(val); |
| } |
| if (fun == "put") { |
| const failable<value> val = put(caddr(args), uri + (string)path(cadr(args)), *cs); |
| return content(val); |
| } |
| if (fun == "patch") { |
| const failable<value> val = patch(caddr(args), uri + (string)path(cadr(args)), *cs); |
| return content(val); |
| } |
| if (fun == "delete") { |
| const failable<value> val = del(uri + (string)path(cadr(args)), *cs); |
| return content(val); |
| } |
| const failable<value> val = evalExpr(args, uri, *cs); |
| return content(val); |
| } |
| |
| private: |
| const string uri; |
| const perthread_ptr<http::CURLSession> cs; |
| |
| |
| const perthread_ptr<http::CURLSession> mksession(const string& ca, const string& cert, const string& key, const string& cookie, const int timeout) { |
| const gc_pool cp = gc_current_pool(); |
| const lambda<const gc_ptr<http::CURLSession>()> newsession = [ca, cert, key, cookie, timeout, cp]() -> const gc_ptr<http::CURLSession> { |
| const gc_scoped_pool sp(pool(cp)); |
| return new (gc_new<http::CURLSession>()) http::CURLSession(ca, cert, key, cookie, timeout); |
| }; |
| return *(new (gc_new<perthread_ptr<http::CURLSession> >()) perthread_ptr<CURLSession>(newsession)); |
| } |
| }; |
| |
| } |
| } |
| |
| #endif /* tuscany_http_hpp */ |