/* * 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 #include "gc.hpp" #include "list.hpp" #include "value.hpp" #include "element.hpp" #include "monad.hpp" #include "../atom/atom.hpp" #include "../json/json.hpp" namespace tuscany { namespace http { /** * CURL library context, one per process. */ class CURLContext { public: CURLContext() { curl_global_init(CURL_GLOBAL_ALL); } ~CURLContext() { curl_global_cleanup(); } }; CURLContext curlContext; /** * Represents a CURL session handle. */ class CURLSession { public: CURLSession() : ch(new CURLHandle()) { } ~CURLSession() { } CURLSession(const CURLSession& c) : ch(c.ch) { } private: class CURLHandle { public: CURLHandle() : h(curl_easy_init()) { } ~CURLHandle() { curl_easy_cleanup(h); h = NULL; } private: CURL* h; friend CURL* handle(const CURLSession& c); }; const gc_ptr ch; friend CURL* handle(const CURLSession& c); }; /** * Returns the CURL handle used by a CURL session. */ CURL* handle(const CURLSession& c) { return c.ch->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; rcx.ilist = fragment(rcx.ilist, size * nmemb); const std::string s = car(rcx.ilist); rcx.ilist = cdr(rcx.ilist); s.copy((char*)ptr, s.length()); return s.length(); } /** * 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(std::string((const char*)ptr, realsize), wcx.accum); return realsize; } /** * Called by CURL to write received header data. */ template size_t headerCallback(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(std::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, std::string(car(h)).c_str()), cdr(h)); } template const failable, std::string> apply(const list >& req, const lambda& reduce, const R& initial, const std::string& url, const std::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 std::ostringstream os; write(cadr(req), os); const std::string s = os.str(); const int sz = s.length(); if (sz < 1400) curl_easy_setopt(ch, CURLOPT_TCP_NODELAY, true); // 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*))headerCallback); 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); // Set the request headers curl_slist* hl = headers(NULL, car(req)); if (hl != NULL) curl_easy_setopt(ch, CURLOPT_HTTPHEADER, hl); // Apply the HTTP verb curl_easy_setopt(ch, CURLOPT_URL, url.c_str()); 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, std::string>(curl_easy_strerror(rc)); long httprc; curl_easy_getinfo (ch, CURLINFO_RESPONSE_CODE, &httprc); if (httprc != 200 && httprc != 201) { std::ostringstream es; es << "HTTP code " << httprc; return mkfailure, std::string>(es.str()); } return mklist(hcx.accum, wcx.accum); } /** * Evaluate an expression remotely, at the given URL. */ const failable evalExpr(const value& expr, const std::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, std::string> jsreq = 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 >, std::string> res = apply >(mklist >(h, content(jsreq)), rcons, list(), url, "POST", ch); if (!hasContent(res)) return mkfailure(reason(res)); // Return result failable, std::string> jsres = json::readJSON(cadr >(content(res)), cx); if (!hasContent(jsres)) return mkfailure(reason(jsres)); const list val = elementsToValues(content(jsres)); const value rval(cadr(cadr(val))); debug(rval, "http::evalExpr::result"); return rval; } /** * Find and return a header. */ const failable header(const std::string& prefix, const list& h) { if (isNil(h)) return mkfailure(std::string("Couldn't find header: ") + prefix); const std::string s = car(h); if (s.find(prefix) != 0) return header(prefix, cdr(h)); const std::string l(s.substr(prefix.length())); return l.substr(0, l.find_first_of("\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 std::string ls(content(l)); return value(ls.substr(ls.find_last_of("/") + 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, std::string> get(const lambda& reduce, const R& initial, const std::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 get(const std::string& url, const CURLSession& ch) { debug(url, "http::get::url"); // Get the contents of the resource at the given URL const failable >, std::string> res = get >(rcons, list(), url, ch); if (!hasContent(res)) return mkfailure(reason(res)); const list ls(reverse(cadr(content(res)))); const std::string ct(content(contentType(car(content(res))))); if (ct == "application/atom+xml;type=entry") { const value val(atom::entryValue(content(atom::readEntry(ls)))); debug(val, "http::get::result"); return val; } // Return the content as a string value std::ostringstream os; write(ls, os); const value val(os.str()); debug(val, "http::get::result"); return val; } /** * HTTP POST. */ const failable post(const value& val, const std::string& url, const CURLSession& ch) { // Convert value to an ATOM entry const failable, std::string> 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 >, std::string> 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 std::string& url, const CURLSession& ch) { // Convert value to an ATOM entry const failable, std::string> 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 >, std::string> 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 std::string& url, const CURLSession& ch) { debug(url, "http::delete::url"); const list > req = mklist(list(), list()); const failable >, std::string> res = apply >(req, rcons, list(), url, "DELETE", ch); if (!hasContent(res)) return mkfailure(reason(res)); debug(true, "http::delete::result"); return value(true); } /** * HTTP client proxy function. */ struct proxy { proxy(const std::string& url, const CURLSession& ch) : url(url), ch(ch) { } const value operator()(const list& args) const { failable val = evalExpr(args, url, ch); if (!hasContent(val)) return value(); return content(val); } const std::string url; const CURLSession ch; }; } } #endif /* tuscany_curl_hpp */