/* * 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 #include #include #include #include #include #include #include #include #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())), 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())), 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; 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 setup(const string& url, const CURLSession& cs); friend const failable cleanup(const CURLSession& cs); friend const failable connect(const string& url, const CURLSession& cs); friend const failable send(const char* const c, const size_t l, const CURLSession& cs); friend const failable 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 setup(const string& url, const CURLSession& cs); friend const failable cleanup(const CURLSession& cs); friend const failable connect(const string& url, const CURLSession& cs); friend const failable send(const char* const c, const size_t l, const CURLSession& cs); friend const failable 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 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 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& ilist) : ilist(ilist) { } gc_mutable_ref > 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 (isNull((const list)rcx.ilist)) return 0; const list 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 class CURLWriteContext { public: CURLWriteContext(const lambda& reduce, const R& accum) : reduce(reduce), accum(accum) { } const lambda reduce; gc_mutable_ref accum; }; /** * Called by CURL to write received data. */ template size_t writeCallback(void *ptr, size_t size, size_t nmemb, void *data) { CURLWriteContext& wcx = *(CURLWriteContext*)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& h) { if (isNull(h)) return cl; return headers(curl_slist_append(cl, c_str(string(car(h)))), cdr(h)); } template const failable > apply(const list >& hdr, const lambda& 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 fch = setup(url, cs); if (!hasContent(fch)) { cleanup(cs); return mkfailure>(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 hcx(reduce, initial); curl_easy_setopt(ch, CURLOPT_HEADERFUNCTION, (size_t (*)(void*, size_t, size_t, void*))(writeCallback)); curl_easy_setopt(ch, CURLOPT_HEADERDATA, &hcx); CURLWriteContext wcx(reduce, initial); curl_easy_setopt(ch, CURLOPT_WRITEFUNCTION, (size_t (*)(void*, size_t, size_t, void*))(writeCallback)); 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 >(string(curl_easy_strerror(rc))); } long httprc; curl_easy_getinfo (ch, CURLINFO_RESPONSE_CODE, &httprc); if (httprc != 200 && httprc != 201) { cleanup(cs); return mkfailure >(string("HTTP code not 200"), (int)httprc, (httprc != 301 && httprc != 302 && httprc != 404)); } cleanup(cs); return mklist(hcx.accum, wcx.accum); } /** * Evaluate an expression remotely, at the given URL. */ const failable 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 > jsreq = json::jsonRequest(1, car(expr), cdr(expr)); if (!hasContent(jsreq)) return mkfailure(jsreq); // POST it to the URL const list h = mklist("Content-Type: application/json-rpc"); const failable > > res = apply >(mklist >(h, content(jsreq)), rcons, list(), url, "POST", cs); if (!hasContent(res)) return mkfailure(res); // Parse and return JSON-RPC result const failable rval = json::jsonResultValue(cadr >(content(res))); debug(rval, "http::evalExpr::result"); if (!hasContent(rval)) return mkfailure(rval); return content(rval); } /** * Find and return a header. */ const maybe header(const char* const prefix, const list& h) { if (isNull(h)) return maybe(); 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& h) { const maybe l = header("Location: ", h); return hasContent(l)? content(l) : emptyString; } /** * Convert a location to an entry id. */ const value entryId(const failable l) { if (!hasContent(l)) return nilListValue; const string ls(content(l)); return value(mklist(string(substr(ls, find_last(ls, '/') + 1)))); } /** * Find and return a content-type header. */ const string contentType(const list& h) { const maybe ct = header("Content-Type: ", h); return hasContent(ct)? content(ct) : emptyString; } /** * HTTP GET, return the resource at the given URL. */ template const failable > get(const lambda& reduce, const R& initial, const string& url, const CURLSession& cs) { debug(url, "http::get::url"); const list > req = mklist(list(), list()); return apply(req, reduce, initial, url, "GET", cs); } /** * HTTP GET, return a list of values representing the resource at the given URL. */ const failable getcontent(const string& url, const CURLSession& cs) { debug(url, "http::get::url"); // Get the contents of the resource at the given URL const failable > > res = get>(rcons, list(), url, cs); if (!hasContent(res)) return mkfailure(res); const list 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 responseValue(const list > res) { // Parse the returned content const string ct = contentType(car(res)); debug(ct, "http::responseValue::contentType"); const list 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 jls = mklist(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(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 get(const string& url, const CURLSession& cs) { debug(url, "http::get::url"); // Get the contents of the resource at the given URL const failable > > res = get >(rcons, list(), url, cs); if (!hasContent(res)) return mkfailure(res); // Parse the returned content return responseValue(content(res)); } /** * Form an HTTP content request. */ const failable > > writeRequest(const failable >& ls, const string& ct) { if (!hasContent(ls)) return mkfailure > >(ls); const list > req = mklist >(mklist(string("Content-Type: ") + ct), content(ls)); debug(req, "http::writeRequest::req"); return req; } /** * Convert a value to an HTTP content request. */ const failable > > contentRequest(const value& c, unused const string& url) { // Write in the format requested by the client, if any const list fmt = assoc("format", nilListValue); if (!isNull(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 (isNull((list)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(c)) && !isNull(cdr(c)) && isList(cadr(c))) return writeRequest(convertValues(cadr(c)), car(c)); // Write an assoc value as a JSON value if (isSymbol(car(c)) && !isNull(cdr(c))) { debug(c, "http::contentRequest::assoc"); return writeRequest(json::writeValue(c), "application/json; charset=utf-8"); } // Write an ATOM feed or entry const list e = valuesToElements(c); if (isList(car(e)) && !isNull(car(e))) { const list el = car(e); if (isSymbol(car(el)) && car(el) == element && !isNull(cdr(el)) && isSymbol(cadr(el)) && elementHasChildren(el) && !elementHasValue(el)) { if (cadr(el) == atom::feed) return writeRequest(atom::writeATOMFeed(e), "application/atom+xml; charset=utf-8"); if (cadr(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 post(const value& val, const string& url, const CURLSession& cs) { debug(url, "http::post::url"); // Convert value to a content request const failable > > req = contentRequest(val, url); if (!hasContent(req)) return mkfailure(req); debug(content(req), "http::post::input"); // POST it to the URL const failable > > res = apply>(content(req), rcons, list(), url, "POST", cs); if (!hasContent(res)) return mkfailure(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 put(const value& val, const string& url, const CURLSession& cs) { debug(url, "http::put::url"); // Convert value to a content request const failable > > req = contentRequest(val, url); if (!hasContent(req)) return mkfailure(req); debug(content(req), "http::put::input"); // PUT it to the URL const failable > > res = apply >(content(req), rcons, list(), url, "PUT", cs); if (!hasContent(res)) return mkfailure(res); debug(true, "http::put::result"); return trueValue; } /** * HTTP PATCH. */ const failable patch(const value& val, const string& url, const CURLSession& cs) { debug(url, "http::put::patch"); // Convert value to a content request const failable > > req = contentRequest(val, url); if (!hasContent(req)) return mkfailure(req); debug(content(req), "http::patch::input"); // PATCH it to the URL const failable > > res = apply >(content(req), rcons, list(), url, "PATCH", cs); if (!hasContent(res)) return mkfailure(res); debug(true, "http::patch::result"); return trueValue; } /** * HTTP DELETE. */ const failable del(const string& url, const CURLSession& cs) { debug(url, "http::delete::url"); const list > req = mklist(list(), list()); const failable > > res = apply >(req, rcons, list(), url, "DELETE", cs); if (!hasContent(res)) return mkfailure(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(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 connect(const string& url, const CURLSession& cs) { debug(url, "http::connect::url"); // Setup the CURL session const failable fch = setup(url, cs); if (!hasContent(fch)) { cleanup(cs); return mkfailure(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(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(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(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(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 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(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(apreason(pollrc)); // Send what's left return send(c + wl, l - wl, cs); } /** * Receive an array of chars. */ const failable 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(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(apreason(pollrc)); } // Receive again return recv(c, l, cs); } /** * Converts a list of key value pairs to a query string. */ ostringstream& queryString(const list& args, ostringstream& os) { if (isNull(args)) return os; const list arg = car(args); debug(arg, "http::queryString::arg"); if (isNull(arg) || isNull(cdr(arg))) return queryString(cdr(args), os); os << car(arg) << "=" << c_str(cadr(arg)); if (!isNull(cdr(args))) os << "&"; return queryString(cdr(args), os); } const string queryString(const list& 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(car(arg), escapeArg(cadr(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& args) const { debug(args, "http::proxy::args"); const value fun = car(args); if (fun == "get") { const list lp = filter(filterPath, cadr(args)); debug(lp, "http::proxy::path"); const list lq = map(escapeQuery, filter(filterQuery, cadr(args))); debug(lq, "http::proxy::query"); const value q = queryString(lq); const failable val = get(uri + (string)path(lp) + (q != emptyString? string("?") + (string)q : emptyString), *cs); return content(val); } if (fun == "post") { const failable val = post(caddr(args), uri + (string)path(cadr(args)), *cs); return content(val); } if (fun == "put") { const failable val = put(caddr(args), uri + (string)path(cadr(args)), *cs); return content(val); } if (fun == "patch") { const failable val = patch(caddr(args), uri + (string)path(cadr(args)), *cs); return content(val); } if (fun == "delete") { const failable val = del(uri + (string)path(cadr(args)), *cs); return content(val); } const failable val = evalExpr(args, uri, *cs); return content(val); } private: const string uri; const perthread_ptr cs; const perthread_ptr 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()> newsession = [ca, cert, key, cookie, timeout, cp]() -> const gc_ptr { const gc_scoped_pool sp(pool(cp)); return new (gc_new()) http::CURLSession(ca, cert, key, cookie, timeout); }; return *(new (gc_new >()) perthread_ptr(newsession)); } }; } } #endif /* tuscany_http_hpp */