/* * 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_curl_hpp #define tuscany_curl_hpp /** * CURL HTTP client functions. */ #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 "../atom/atom.hpp" #include "../json/json.hpp" namespace tuscany { namespace http { /** * 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(const string& ca = "", const string& cert = "", const string& key = "") : h(curl_easy_init()), owner(true), ca(ca), cert(cert), key(key) { } CURLSession(const CURLSession& c) : h(c.h), owner(false), ca(c.ca), cert(c.cert), key(c.key) { } ~CURLSession() { if (!owner) return; if (h == NULL) return; curl_easy_cleanup(h); } private: CURL* h; const bool owner; friend CURL* handle(const CURLSession& c); public: const string ca; const string cert; const string key; }; /** * Returns the CURL handle used by a CURL session. */ CURL* handle(const CURLSession& c) { return c.h; } /** * Context passed to the read callback function. */ class CURLReadContext { public: CURLReadContext(const list& ilist) : ilist(ilist) { } list 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(data); if (isNil(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; R accum; }; /** * Called by CURL to write received data. */ template size_t writeCallback(void *ptr, size_t size, size_t nmemb, void *data) { CURLWriteContext& wcx = *(static_cast*> (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& h) { if (isNil(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) { // Init the curl session CURL* ch = handle(cs); curl_easy_reset(ch); curl_easy_setopt(ch, CURLOPT_USERAGENT, "libcurl/1.0"); //TODO use HTTP chunking, for now just convert request to a single string ostringstream os; write(cadr(hdr), os); const string s = str(os); const int sz = length(s); // Setup the read, header and write 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); // 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); // Setup SSL options if (cs.ca != "") { debug(cs.ca, "http::apply::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); } if (cs.cert != "") { debug(cs.cert, "http::apply::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::apply::key"); curl_easy_setopt(ch, CURLOPT_SSLKEY, c_str(cs.key)); curl_easy_setopt(ch, CURLOPT_SSLKEYTYPE, "PEM"); } // Set the request headers curl_slist* hl = headers(NULL, car(hdr)); if (hl != NULL) curl_easy_setopt(ch, CURLOPT_HTTPHEADER, hl); // Apply the HTTP verb curl_easy_setopt(ch, CURLOPT_URL, c_str(url)); 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 == "DELETE") curl_easy_setopt(ch, CURLOPT_CUSTOMREQUEST, "DELETE"); const CURLcode rc = curl_easy_perform(ch); if (hl != NULL) curl_slist_free_all(hl); // Return the HTTP return code or content if (rc) return mkfailure >(string(curl_easy_strerror(rc))); long httprc; curl_easy_getinfo (ch, CURLINFO_RESPONSE_CODE, &httprc); if (httprc != 200 && httprc != 201) { ostringstream es; es << "HTTP code " << httprc; return mkfailure >(str(es)); } 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& ch) { debug(url, "http::evalExpr::url"); debug(expr, "http::evalExpr::input"); // Convert expression to a JSON-RPC request json::JSONContext cx; const failable > jsreq = json::jsonRequest(1, car(expr), cdr(expr), cx); if (!hasContent(jsreq)) return mkfailure(reason(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", ch); if (!hasContent(res)) return mkfailure(reason(res)); // Parse and return JSON-RPC result const failable rval = json::jsonResultValue(cadr >(content(res)), cx); if (!hasContent(rval)) return mkfailure(reason(rval)); debug(content(rval), "http::evalExpr::result"); return content(rval); } /** * Find and return a header. */ const failable header(const char* prefix, const list& h) { if (isNil(h)) return mkfailure(string("Couldn't find header: ") + prefix); 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 failable location(const list& h) { return header("Location: ", h); } /** * Convert a location to an entry id. */ const failable entryId(const failable l) { if (!hasContent(l)) return mkfailure(reason(l)); const string ls(content(l)); return value(mklist(string(substr(ls, find_last(ls, '/') + 1)))); } /** * Find and return a content-type header. */ const failable contentType(const list& h) { return header("Content-Type: ", h); } /** * HTTP GET, return the resource at the given URL. */ template const failable > get(const lambda& reduce, const R& initial, const string& url, const CURLSession& ch) { debug(url, "http::get::url"); const list > req = mklist(list(), list()); return apply(req, reduce, initial, url, "GET", ch); } /** * HTTP GET, return a list of values representing the resource at the given URL. */ const failable getcontent(const string& url, const CURLSession& ch) { debug(url, "http::get::url"); // Get the contents of the resource at the given URL const failable > > res = get>(rcons, list(), url, ch); if (!hasContent(res)) return mkfailure(reason(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; } /** * HTTP GET, return a list of values representing the resource at the given URL. */ const failable get(const string& url, const CURLSession& ch) { debug(url, "http::get::url"); // Get the contents of the resource at the given URL const failable > > res = get >(rcons, list(), url, ch); if (!hasContent(res)) return mkfailure(reason(res)); const list ls(reverse(cadr(content(res)))); const string ct(content(contentType(car(content(res))))); if (ct == "application/atom+xml;type=entry") { const value val(atom::entryValue(content(atom::readATOMEntry(ls)))); debug(val, "http::get::result"); return val; } // Return the content as a list of values const value val(mkvalues(ls)); debug(val, "http::get::result"); return val; } /** * HTTP POST. */ const failable post(const value& val, const string& url, const CURLSession& ch) { // Convert value to an ATOM entry const failable > entry = atom::writeATOMEntry(atom::entryValuesToElements(val)); if (!hasContent(entry)) return mkfailure(reason(entry)); debug(url, "http::post::url"); debug(content(entry), "http::post::input"); // POST it to the URL const list h = mklist("Content-Type: application/atom+xml"); const list > req = mklist >(h, content(entry)); const failable > > res = apply>(req, rcons, list(), url, "POST", ch); if (!hasContent(res)) return mkfailure(reason(res)); // Return the new entry id from the HTTP location header const failable eid(entryId(location(car(content(res))))); debug(eid, "http::post::result"); return eid; } /** * HTTP PUT. */ const failable put(const value& val, const string& url, const CURLSession& ch) { // Convert value to an ATOM entry const failable > entry = atom::writeATOMEntry(atom::entryValuesToElements(val)); if (!hasContent(entry)) return mkfailure(reason(entry)); debug(url, "http::put::url"); debug(content(entry), "http::put::input"); // PUT it to the URL const list h = mklist("Content-Type: application/atom+xml"); const list > req = mklist >(h, content(entry)); const failable > > res = apply >(req, rcons, list(), url, "PUT", ch); if (!hasContent(res)) return mkfailure(reason(res)); debug(true, "http::put::result"); return value(true); } /** * HTTP DELETE. */ const failable del(const string& url, const CURLSession& ch) { debug(url, "http::delete::url"); const list > req = mklist(list(), list()); const failable > > res = apply >(req, rcons, list(), url, "DELETE", ch); if (!hasContent(res)) return mkfailure(reason(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; } /** * HTTP client proxy function. */ struct proxy { proxy(const string& uri, const string& ca, const string& cert, const string& key) : uri(uri), ca(ca), cert(cert), key(key) { } const value operator()(const list& args) const { CURLSession cs(ca, cert, key); failable val = evalExpr(args, uri, cs); if (!hasContent(val)) return value(); return content(val); } const string uri; const string ca; const string cert; const string key; }; } } #endif /* tuscany_curl_hpp */