/** * @file connection.cpp * @brief implementation of the connection class * @author Daniel Schauenberg */ #include "restclient-cpp/connection.h" #include #include #include #include #include #include #include #include "restclient-cpp/restclient.h" #include "restclient-cpp/helpers.h" /** * @brief constructor for the Connection object * * @param baseUrl - base URL for the connection to use * */ RestClient::Connection::Connection(const std::string& baseUrl) : lastRequest(), headerFields() { this->curlHandle = curl_easy_init(); if (!this->curlHandle) { throw std::runtime_error("Couldn't initialize curl handle"); } this->baseUrl = baseUrl; this->timeout = 0; this->followRedirects = false; this->maxRedirects = -1l; this->noSignal = false; } RestClient::Connection::~Connection() { if (this->curlHandle) { curl_easy_cleanup(this->curlHandle); } } // getters/setters /** * @brief get diagnostic information about the connection object * * @return RestClient::Connection::Info struct */ RestClient::Connection::Info RestClient::Connection::GetInfo() { RestClient::Connection::Info ret; ret.baseUrl = this->baseUrl; ret.headers = this->GetHeaders(); ret.timeout = this->timeout; ret.followRedirects = this->followRedirects; ret.maxRedirects = this->maxRedirects; ret.noSignal = this->noSignal; ret.basicAuth.username = this->basicAuth.username; ret.basicAuth.password = this->basicAuth.password; ret.customUserAgent = this->customUserAgent; ret.lastRequest = this->lastRequest; ret.certPath = this->certPath; ret.certType = this->certType; ret.keyPath = this->keyPath; ret.keyPassword = this->keyPassword; ret.uriProxy = this->uriProxy; return ret; } /** * @brief append a header to the internal map * * @param key for the header field * @param value for the header field * */ void RestClient::Connection::AppendHeader(const std::string& key, const std::string& value) { this->headerFields[key] = value; } /** * @brief set the custom headers map. This will replace the currently * configured headers with the provided ones. If you want to add additional * headers, use AppendHeader() * * @param headers to set */ void RestClient::Connection::SetHeaders(RestClient::HeaderFields headers) { #if __cplusplus >= 201103L this->headerFields = std::move(headers); #else this->headerFields = headers; #endif } /** * @brief get all custom headers set on the connection * * @returns a RestClient::HeaderFields map containing the custom headers */ RestClient::HeaderFields RestClient::Connection::GetHeaders() { return this->headerFields; } /** * @brief configure whether to follow redirects on this connection * * @param follow - boolean whether to follow redirects */ void RestClient::Connection::FollowRedirects(bool follow) { this->followRedirects = follow; this->maxRedirects = -1l; } /** * @brief configure whether to follow redirects on this connection * * @param follow - boolean whether to follow redirects * @param maxRedirects - int indicating the maximum number of redirect to follow (-1 unlimited) */ void RestClient::Connection::FollowRedirects(bool follow, int maxRedirects) { this->followRedirects = follow; this->maxRedirects = maxRedirects; } /** * @brief set custom user agent for connection. This gets prepended to the * default restclient-cpp/RESTCLIENT_VERSION string * * @param userAgent - custom userAgent prefix * */ void RestClient::Connection::SetUserAgent(const std::string& userAgent) { this->customUserAgent = userAgent; } /** * @brief set custom Certificate Authority (CA) path * * @param caInfoFilePath - The path to a file holding the certificates used to * verify the peer with. See CURLOPT_CAINFO * */ void RestClient::Connection::SetCAInfoFilePath(const std::string& caInfoFilePath) { this->caInfoFilePath = caInfoFilePath; } /** * @brief get the user agent to add to the request * * @return user agent as std::string */ std::string RestClient::Connection::GetUserAgent() { std::string prefix; if (this->customUserAgent.length() > 0) { prefix = this->customUserAgent + " "; } return std::string(prefix + ""); } /** * @brief set timeout for connection * * @param seconds - timeout in seconds * */ void RestClient::Connection::SetTimeout(int seconds) { this->timeout = seconds; } /** * @brief switch off curl signals for connection (see CURLOPT_NONSIGNAL). By * default signals are used, except when timeout is given. * * @param no - set to true switches signals off * */ void RestClient::Connection::SetNoSignal(bool no) { this->noSignal = no; } /** * @brief set username and password for basic auth * * @param username * @param password * */ void RestClient::Connection::SetBasicAuth(const std::string& username, const std::string& password) { this->basicAuth.username = username; this->basicAuth.password = password; } /** * @brief set certificate path * * @param path to certificate file * */ void RestClient::Connection::SetCertPath(const std::string& cert) { this->certPath = cert; } /** * @brief set certificate type * * @param certificate type (e.g. "PEM" or "DER") * */ void RestClient::Connection::SetCertType(const std::string& certType) { this->certType = certType; } /** * @brief set key path * * @param path to key file * */ void RestClient::Connection::SetKeyPath(const std::string& keyPath) { this->keyPath = keyPath; } /** * @brief set key password * * @param key password * */ void RestClient::Connection::SetKeyPassword(const std::string& keyPassword) { this->keyPassword = keyPassword; } /** * @brief set HTTP proxy address and port * * @param proxy address with port number * */ void RestClient::Connection::SetProxy(const std::string& uriProxy) { std::string uriProxyUpper = uriProxy; // check if the provided address is prefixed with "http" std::transform(uriProxyUpper.begin(), uriProxyUpper.end(), uriProxyUpper.begin(), ::toupper); if ((uriProxy.length() > 0) && (uriProxyUpper.compare(0, 4, "HTTP") != 0)) { this->uriProxy = "http://" + uriProxy; } else { this->uriProxy = uriProxy; } } /** * @brief helper function to get called from the actual request methods to * prepare the curlHandle for transfer with generic options, perform the * request and record some stats from the last request and then reset the * handle with curl_easy_reset to its default state. This will keep things * like connections and session ID intact but makes sure you can change * parameters on the object for another request. * * @param uri URI to query * @param ret Reference to the Response struct that should be filled * * @return 0 on success and 1 on error */ RestClient::Response RestClient::Connection::performCurlRequest(const std::string& uri, const std::string method_type) { // init return type RestClient::Response ret = {}; std::string url = std::string(this->baseUrl + uri); std::string headerString; CURLcode res = CURLE_OK; curl_slist* headerList = NULL; /** set query URL */ curl_easy_setopt(this->curlHandle, CURLOPT_URL, url.c_str()); if (method_type.compare("download") != 0) { /** set callback function */ curl_easy_setopt(this->curlHandle, CURLOPT_WRITEFUNCTION, Helpers::write_callback); /** set data object to pass to callback function */ curl_easy_setopt(this->curlHandle, CURLOPT_WRITEDATA, &ret); } /** set the header callback function */ curl_easy_setopt(this->curlHandle, CURLOPT_HEADERFUNCTION, Helpers::header_callback); /** callback object for headers */ curl_easy_setopt(this->curlHandle, CURLOPT_HEADERDATA, &ret); /** set http headers */ for (HeaderFields::const_iterator it = this->headerFields.begin(); it != this->headerFields.end(); ++it) { headerString = it->first; headerString += ": "; headerString += it->second; headerList = curl_slist_append(headerList, headerString.c_str()); } curl_easy_setopt(this->curlHandle, CURLOPT_HTTPHEADER, headerList); // set basic auth if configured if (this->basicAuth.username.length() > 0) { std::string authString = std::string(this->basicAuth.username + ":" + this->basicAuth.password); curl_easy_setopt(this->curlHandle, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); curl_easy_setopt(this->curlHandle, CURLOPT_USERPWD, authString.c_str()); } /** set user agent */ curl_easy_setopt(this->curlHandle, CURLOPT_USERAGENT, this->GetUserAgent().c_str()); // set timeout if (this->timeout) { curl_easy_setopt(this->curlHandle, CURLOPT_TIMEOUT, this->timeout); // dont want to get a sig alarm on timeout curl_easy_setopt(this->curlHandle, CURLOPT_NOSIGNAL, 1); } // set follow redirect if (this->followRedirects == true) { curl_easy_setopt(this->curlHandle, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(this->curlHandle, CURLOPT_MAXREDIRS, static_cast(this->maxRedirects)); } if (this->noSignal) { // multi-threaded and prevent entering foreign signal handler (e.g. JNI) curl_easy_setopt(this->curlHandle, CURLOPT_NOSIGNAL, 1); } // if provided, supply CA path if (!this->caInfoFilePath.empty()) { curl_easy_setopt(this->curlHandle, CURLOPT_CAINFO, this->caInfoFilePath.c_str()); } // set cert file path if (!this->certPath.empty()) { curl_easy_setopt(this->curlHandle, CURLOPT_SSLCERT, this->certPath.c_str()); } // set cert type if (!this->certType.empty()) { curl_easy_setopt(this->curlHandle, CURLOPT_SSLCERTTYPE, this->certType.c_str()); } // set key file path if (!this->keyPath.empty()) { curl_easy_setopt(this->curlHandle, CURLOPT_SSLKEY, this->keyPath.c_str()); } // set key password if (!this->keyPassword.empty()) { curl_easy_setopt(this->curlHandle, CURLOPT_KEYPASSWD, this->keyPassword.c_str()); } // set web proxy address if (!this->uriProxy.empty()) { curl_easy_setopt(this->curlHandle, CURLOPT_PROXY, uriProxy.c_str()); curl_easy_setopt(this->curlHandle, CURLOPT_HTTPPROXYTUNNEL, 1L); } res = curl_easy_perform(this->curlHandle); if (res != CURLE_OK) { switch (res) { case CURLE_OPERATION_TIMEDOUT: ret.code = res; ret.body = "Operation Timeout."; break; case CURLE_SSL_CERTPROBLEM: ret.code = res; ret.body = curl_easy_strerror(res); break; default: ret.body = "Failed to query."; ret.body += curl_easy_strerror(res); ret.code = -1; } } else { int64_t http_code = 0; curl_easy_getinfo(this->curlHandle, CURLINFO_RESPONSE_CODE, &http_code); ret.code = static_cast(http_code); } curl_easy_getinfo(this->curlHandle, CURLINFO_TOTAL_TIME, &this->lastRequest.totalTime); curl_easy_getinfo(this->curlHandle, CURLINFO_NAMELOOKUP_TIME, &this->lastRequest.nameLookupTime); curl_easy_getinfo(this->curlHandle, CURLINFO_CONNECT_TIME, &this->lastRequest.connectTime); curl_easy_getinfo(this->curlHandle, CURLINFO_APPCONNECT_TIME, &this->lastRequest.appConnectTime); curl_easy_getinfo(this->curlHandle, CURLINFO_PRETRANSFER_TIME, &this->lastRequest.preTransferTime); curl_easy_getinfo(this->curlHandle, CURLINFO_STARTTRANSFER_TIME, &this->lastRequest.startTransferTime); curl_easy_getinfo(this->curlHandle, CURLINFO_REDIRECT_TIME, &this->lastRequest.redirectTime); curl_easy_getinfo(this->curlHandle, CURLINFO_REDIRECT_COUNT, &this->lastRequest.redirectCount); // free header list curl_slist_free_all(headerList); // reset curl handle curl_easy_reset(this->curlHandle); return ret; } /** * @brief HTTP GET method * * @param url to query * * @return response struct */ RestClient::Response RestClient::Connection::get(const std::string& url) { return this->performCurlRequest(url); } /** * @brief HTTP POST method * * @param url to query * @param data HTTP POST body * * @return response struct */ RestClient::Response RestClient::Connection::post(const std::string& url, const std::string& data) { /** Now specify we want to POST data */ curl_easy_setopt(this->curlHandle, CURLOPT_POST, 1L); /** set post fields */ curl_easy_setopt(this->curlHandle, CURLOPT_POSTFIELDS, data.c_str()); curl_easy_setopt(this->curlHandle, CURLOPT_POSTFIELDSIZE, data.size()); return this->performCurlRequest(url); } /** * @brief HTTP PUT method * * @param url to query * @param data HTTP PUT body * * @return response struct */ RestClient::Response RestClient::Connection::put(const std::string& url, const std::string& data) { /** initialize upload object */ RestClient::Helpers::UploadObject up_obj; up_obj.data = data.c_str(); up_obj.length = data.size(); /** Now specify we want to PUT data */ curl_easy_setopt(this->curlHandle, CURLOPT_PUT, 1L); curl_easy_setopt(this->curlHandle, CURLOPT_UPLOAD, 1L); /** set read callback function */ curl_easy_setopt(this->curlHandle, CURLOPT_READFUNCTION, RestClient::Helpers::read_callback); /** set data object to pass to callback function */ curl_easy_setopt(this->curlHandle, CURLOPT_READDATA, &up_obj); /** set data size */ curl_easy_setopt(this->curlHandle, CURLOPT_INFILESIZE, static_cast(up_obj.length)); return this->performCurlRequest(url); } /** * @brief HTTP DELETE method * * @param url to query * * @return response struct */ RestClient::Response RestClient::Connection::del(const std::string& url) { /** we want HTTP DELETE */ const char* http_delete = "DELETE"; /** set HTTP DELETE METHOD */ curl_easy_setopt(this->curlHandle, CURLOPT_CUSTOMREQUEST, http_delete); return this->performCurlRequest(url); } /** * @brief HTTP HEAD method * * @param url to query * * @return response struct */ RestClient::Response RestClient::Connection::head(const std::string& url) { /** we want HTTP HEAD */ const char* http_head = "HEAD"; /** set HTTP HEAD METHOD */ curl_easy_setopt(this->curlHandle, CURLOPT_CUSTOMREQUEST, http_head); curl_easy_setopt(this->curlHandle, CURLOPT_NOBODY, 1L); return this->performCurlRequest(url); } /** * @brief HTTP GET method * * @param url to query * * @return response struct */ RestClient::Response RestClient::Connection::download(const std::string& uri, const std::string& file_to_save) { Response ret; FILE* fp = fopen(file_to_save.c_str(), "wb"); if (!fp) { LOGD("!!! Failed to create file on the disk\n"); ret.body = "!!! Failed to create file on the disk"; return ret; } /** set callback function */ curl_easy_setopt(this->curlHandle, CURLOPT_WRITEFUNCTION, Helpers::download_callback); /** set data object to pass to callback function */ curl_easy_setopt(this->curlHandle, CURLOPT_WRITEDATA, fp); ret = this->performCurlRequest(uri, "download"); fflush(fp); fclose(fp); return ret; }