/**
* Relational pipes
* Copyright © 2022 František Kučera (Frantovo.cz, GlobalCode.info)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <string>
#include <sstream>
#include <iostream>
#include <curl/curl.h>
#include "HTTPClient.h"
namespace relpipe {
namespace tr {
namespace http {
class HTTPClient::HTTPClientImpl {
public:
CURL* curl;
char curlErrorBuffer[CURL_ERROR_SIZE];
std::stringstream responseBody;
std::stringstream responseHeaders;
HTTPClientImpl(CURL* curl) : curl(curl) {
}
std::vector<std::string> getResponseHeaders() {
std::vector<std::string> heathers;
std::stringstream name;
std::stringstream value;
std::stringstream* current = &name;
for (char ch = responseHeaders.get(); responseHeaders.good(); ch = responseHeaders.get()) {
if (ch == ':') {
current = &value;
for (char space = responseHeaders.get(); responseHeaders.good() && responseHeaders.peek() == ' '; space = responseHeaders.get()); // skip spaces
} else if (ch == '\n') {
if (name.tellp() > 0 && current == &value) {
heathers.push_back(name.str());
heathers.push_back(value.str());
} else if (name.tellp() > 0 && current == &value) {
// TODO: usually "HTTP/1.1 200 OK" → extract HTTP version and message?
}
name = std::stringstream();
value = std::stringstream();
current = &name;
} else if (ch == '\r') {
// ignore
} else {
current->put(ch);
}
}
return heathers;
}
};
class CurlList {
private:
curl_slist* list = nullptr;
public:
CurlList() = default;
CurlList(const HTTPClient&) = delete;
CurlList& operator=(const CurlList&) = delete;
virtual ~CurlList() {
curl_slist_free_all(list);
}
void append(std::string item) {
list = curl_slist_append(list, item.c_str());
}
curl_slist* getList() {
return list;
}
};
HTTPClient* HTTPClient::open() {
HTTPClient::HTTPClientImpl* impl = new HTTPClient::HTTPClientImpl(curl_easy_init());
typedef size_t(*CurlWriteCallback)(char*, size_t, size_t, HTTPClient::HTTPClientImpl*);
// set response body callback
curl_easy_setopt(impl->curl, CURLOPT_WRITEDATA, impl);
curl_easy_setopt(impl->curl, CURLOPT_WRITEFUNCTION, (CurlWriteCallback)[](char* buffer, size_t size, size_t nmemb, HTTPClient::HTTPClientImpl * impl)->size_t {
size_t r = size * nmemb;
impl->responseBody.write(buffer, r);
return r;
});
// set response headers callback
curl_easy_setopt(impl->curl, CURLOPT_HEADERDATA, impl);
curl_easy_setopt(impl->curl, CURLOPT_HEADERFUNCTION, (CurlWriteCallback)[](char* buffer, size_t size, size_t nmemb, HTTPClient::HTTPClientImpl * impl)->size_t {
size_t r = size * nmemb;
impl->responseHeaders.write(buffer, r);
return r;
});
// set the error buffer
curl_easy_setopt(impl->curl, CURLOPT_ERRORBUFFER, impl->curlErrorBuffer);
// enable HTTP and HTTPS only
curl_easy_setopt(impl->curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
// disable protocol guesswork
curl_easy_setopt(impl->curl, CURLOPT_DEFAULT_PROTOCOL, "no-default-protocol-specify-http-or-https-explicitly-in-url");
return new HTTPClient(impl);
}
HTTPClient::~HTTPClient() {
curl_easy_cleanup(impl->curl);
delete impl;
}
const HTTPClient::Response HTTPClient::exchange(const Request& request) {
HTTPClient::Response response;
// set request method
std::string method;
if (request.method == Method::DELETE) method = "DELETE";
else if (request.method == Method::GET) method = "GET";
else if (request.method == Method::HEAD) method = "HEAD";
else if (request.method == Method::PATCH) method = "PATCH";
else if (request.method == Method::POST) method = "POST";
else if (request.method == Method::PUT) method = "PUT";
else throw std::invalid_argument("Unsupported HTTP method: " + std::to_string((int) request.method));
curl_easy_setopt(impl->curl, CURLOPT_CUSTOMREQUEST, method.c_str());
if (request.method == Method::HEAD) curl_easy_setopt(impl->curl, CURLOPT_NOBODY, 1L);
// set URL
curl_easy_setopt(impl->curl, CURLOPT_URL, request.url.c_str());
// set request headers
CurlList requestHeders;
for (size_t i = 0; i < request.headers.size(); i += 2) requestHeders.append(request.headers[i] + ": " + request.headers[i + 1]); // TODO: validate, no CR/LF...
curl_easy_setopt(impl->curl, CURLOPT_HTTPHEADER, requestHeders.getList());
// clear the error buffer
impl->curlErrorBuffer[0] = 0;
// do HTTP call
CURLcode result = curl_easy_perform(impl->curl);
if (result == CURLE_OK) {
// response code and fill the result object
curl_easy_getinfo(impl->curl, CURLINFO_RESPONSE_CODE, &response.responseCode);
response.headers = impl->getResponseHeaders();
response.body = impl->responseBody.str();
impl->responseBody = std::stringstream();
return response;
} else {
throw Exception(curl_easy_strerror(result), impl->curlErrorBuffer);
}
}
}
}
}