Created
April 16, 2018 01:36
-
-
Save graetzer/d586eb5a90e81b3d691ccb93111ac70b to your computer and use it in GitHub Desktop.
C++ Simple HTTP Client
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// Copyright 2018 Simon Grätzer | |
#include "client.h" | |
#include "defer.h" | |
#include <http_parser.h> | |
#include <sys/types.h> | |
#include <sys/socket.h> | |
#include <netdb.h> | |
#include <arpa/inet.h> | |
#include <unistd.h> | |
#include <iostream> | |
#include <string> | |
#include <sstream> | |
using namespace codepasta::rest; | |
Response ClientImpl::get(std::string const& url, Headers const& headers) const { | |
return sendRequest("GET", url, headers, ""); | |
} | |
Response ClientImpl::post(std::string const& url, Headers const& headers, | |
std::string const& buffer) const { | |
return sendRequest("POST", url, headers, buffer); | |
} | |
/** @brief Resolves the given hostname/port combination | |
* @param server hostname to resolve | |
* @param port port to resolve | |
* @return resolved address(es) | |
*/ | |
struct addrinfo* ResolveHost(const char *server, const char *port) { | |
struct addrinfo hints; | |
memset(&hints, 0, sizeof(struct addrinfo)); | |
hints.ai_family = AF_INET; // ipv4 please | |
hints.ai_socktype = SOCK_STREAM; // request TCP | |
struct addrinfo* result = 0; | |
int errorCode = getaddrinfo(server, port, &hints, &result); // resolve hostname | |
if(errorCode != 0) { // print user-friendly message on error | |
std::cerr << "Name resolution failed" << gai_strerror(errorCode); | |
return 0; | |
} | |
return result; | |
} | |
/** @brief Creates a socket and sets its receive timeout | |
* @param host addrinfo struct pointer returned by ResolveHost/getaddrinfo | |
* @return Returns socket descriptor on success, -1 on failure | |
*/ | |
int CreateSocketWithOptions(struct addrinfo *host) { | |
int fd = socket(host->ai_family, host->ai_socktype, host->ai_protocol); // create socket using provided parameters | |
if(fd == -1) { // print user-friendly message on error | |
std::cerr << "socket"; | |
return -1; | |
} | |
struct timeval delay; | |
memset(&delay, 0, sizeof(struct timeval)); | |
delay.tv_sec = 30; // 30 seconds timeout | |
delay.tv_usec = 0; | |
socklen_t delayLen = sizeof(delay); | |
int status = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &delay, delayLen); // apply timeout to socket | |
if (status == -1) { // print user-friendly message on error | |
close(fd); | |
std::cerr << "setsockopt"; | |
return -1; | |
} | |
return fd; | |
} | |
/// temporary connection data | |
struct ResponseData { | |
bool message_complete = false; | |
bool last_header_was_a_value = false; | |
std::string lastHeaderField; | |
std::string lastHeaderValue; | |
codepasta::rest::Response response; | |
}; | |
static int on_message_began (http_parser* parser) { | |
return 0; | |
} | |
static int on_status (http_parser* parser, const char *at, size_t len) { | |
return 0; | |
} | |
static int on_header_field (http_parser* parser, const char *at, size_t len) { | |
ResponseData* data = static_cast<ResponseData*>(parser->data); | |
if (data->last_header_was_a_value) { | |
data->response.headers.emplace(std::move(data->lastHeaderField), | |
std::move(data->lastHeaderValue)); | |
data->lastHeaderField.assign(at, len); | |
} else { | |
data->lastHeaderField.append(at, len); | |
} | |
data->last_header_was_a_value = false; | |
return 0; | |
} | |
static int on_header_value (http_parser* parser, const char *at, size_t len) { | |
ResponseData* data = static_cast<ResponseData*>(parser->data); | |
if (data->last_header_was_a_value) { | |
data->lastHeaderValue.append(at, len); | |
} else { | |
data->lastHeaderValue.assign(at, len); | |
} | |
data->last_header_was_a_value = true; | |
return 0; | |
} | |
static int on_header_complete (http_parser* parser) { | |
ResponseData* data = static_cast<ResponseData*>(parser->data); | |
data->response.statusCode = static_cast<StatusCode>(parser->status_code); | |
return 0; | |
} | |
static int on_body (http_parser* parser, const char *at, size_t len) { | |
static_cast<ResponseData*>(parser->data)->response.body.append(at, len); | |
return 0; | |
} | |
static int on_message_complete (http_parser* parser) { | |
static_cast<ResponseData*>(parser->data)->message_complete = true; | |
return 0; | |
} | |
Response ClientImpl::sendRequest(std::string const& method, std::string const& url, | |
Headers const& headers, std::string const& body) const { | |
rest::Response res; | |
res.statusCode = rest::StatusCode::Bad; | |
// Step 1. Parse provided URL | |
struct http_parser_url parsedUrl; | |
http_parser_url_init(&parsedUrl); | |
int error = http_parser_parse_url(url.c_str(), url.length(), 0, &parsedUrl); | |
if (error != 0) { | |
std::cerr << "Error parsing url"; | |
return res; | |
} | |
// put hostname, port and path in seperate strings | |
std::string server; | |
if (!(parsedUrl.field_set & (1 << UF_HOST))) { | |
std::cerr << "Url missing host"; | |
return res; | |
} | |
server = url.substr(parsedUrl.field_data[UF_HOST].off, | |
parsedUrl.field_data[UF_HOST].len); | |
size_t pathOff = parsedUrl.field_data[UF_HOST].off + | |
parsedUrl.field_data[UF_HOST].len; | |
std::string port = "80"; | |
if (parsedUrl.field_set & (1 << UF_PORT)) { | |
port = url.substr(parsedUrl.field_data[UF_PORT].off, | |
parsedUrl.field_data[UF_PORT].len); | |
pathOff = parsedUrl.field_data[UF_PORT].off + | |
parsedUrl.field_data[UF_PORT].len; | |
} | |
std::string path = url.substr(pathOff); | |
if (path.empty()) { | |
path = "/"; | |
} | |
// Step 2. resolve hostname and port | |
struct addrinfo *hostAddr = ResolveHost(server.c_str(), port.c_str()); | |
if (hostAddr == nullptr) { | |
std::cerr << "Could not resolver host"; | |
return res; | |
} | |
DEFER(freeaddrinfo(hostAddr)); // free addrinfo(s) | |
// Step 3. create the socket | |
int fd = CreateSocketWithOptions(hostAddr); // create socket | |
if(fd == -1) { // exit if the socket could not be created | |
std::cerr << "Could not open socket" << std::endl; | |
return res; | |
} | |
DEFER(close(fd)); // close socket | |
// Step 4. build the request | |
std::ostringstream requestBuf; | |
requestBuf << method << " " << url | |
<< " HTTP/1.1\r\n" | |
<< "Host: " << server << "\r\n" | |
// << "Accept: */*\r\n" | |
<< "Connection: close\r\n"; | |
// we do not support keeping the conntection open | |
rest::Headers hcopy = headers; | |
if (!body.empty()) { | |
hcopy[rest::Client::kHeaderContentLength] = std::to_string(body.size()); | |
} | |
for (auto pair : hcopy) { | |
requestBuf << pair.first << ": " << pair.second << "\r\n"; | |
} | |
requestBuf << "\r\n";// empty line marks end of header | |
if (!body.empty()) { | |
requestBuf << body; | |
} | |
// Step 5. sending the request | |
if (connect(fd, hostAddr->ai_addr, hostAddr->ai_addrlen) == -1) { | |
std::cerr << "error connect: " << strerror(errno) << std::endl; | |
return res; | |
} | |
std::string req = requestBuf.str(); | |
ssize_t result = 0, total = 0; | |
while (total < req.size()) { | |
result = send(fd, req.c_str()+total, req.size()-total, 0); | |
if (result == -1) break; | |
total += result; | |
} | |
if(result == -1) { | |
std::cerr << "Error sending http request " << strerror(errno); | |
return res; | |
} | |
// Step 6. parsing the response | |
http_parser_settings settings; | |
settings.on_message_begin = on_message_began; | |
settings.on_status = on_status; | |
settings.on_header_field = on_header_field; | |
settings.on_header_value = on_header_value; | |
settings.on_headers_complete = on_header_complete; | |
settings.on_body = on_body; | |
settings.on_message_complete = on_message_complete; | |
http_parser *parser = (http_parser*) malloc(sizeof(http_parser)); | |
http_parser_init(parser, HTTP_RESPONSE); | |
DEFER(free(parser)); | |
ResponseData resData; | |
parser->data = static_cast<void*>(&resData); | |
size_t const len = 32 * 1024; | |
char buf[len]; | |
ssize_t recved; | |
do { | |
recved = recv(fd, buf, len, 0); | |
if (recved < 0) { | |
/* Handle error. */ | |
std::cerr << "Error receiving data on socker" << std::endl; | |
return res; | |
} | |
/* Start up / continue the parser. | |
* Note we pass recved==0 to signal that EOF has been received. | |
*/ | |
size_t nparsed = http_parser_execute(parser, &settings, buf, recved); | |
if (parser->upgrade) { | |
/* handle new protocol */ | |
std::cerr << "Upgrading is not supported" << std::endl; | |
return res; | |
} else if (nparsed != recved) { | |
/* Handle error. Usually just close the connection. */ | |
std::cerr << "Invalid HTTP response in parser" << std::endl; | |
return res; | |
} | |
} while (recved != 0 && !resData.message_complete); | |
if (!resData.lastHeaderField.empty()) { | |
resData.response.headers.emplace(std::move(resData.lastHeaderField), | |
std::move(resData.lastHeaderValue)); | |
} | |
return resData.response; // hopefully RVO | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// Copyright 2018 Simon Grätzer | |
#ifndef REST_CLIENT_IMPL | |
#define REST_CLIENT_IMPL | |
namespace codepasta { | |
namespace rest { | |
/// El-cheapo rest client API | |
class ClientImpl { | |
public: | |
explicit ClientImpl() {} | |
Response get(std::string const& url, Headers const& headers) const; | |
Response post(std::string const& url, Headers const& headers, | |
std::string const& buffer) const; | |
private: | |
Response sendRequest(std::string const& method, std::string const& url, | |
Headers const& headers, std::string const& buffer) const; | |
}; | |
} | |
} | |
#endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef UTIL_DEFER | |
#define UTIL_DEFER | |
/// Use in a function (or scope) as: | |
/// DEFER( <ONE_STATEMENT> ); | |
/// and the statement will be called regardless if the function throws or | |
/// returns regularly. | |
/// Do not put multiple DEFERs on a single source code line (will not | |
/// compile). | |
/// Multiple DEFERs in one scope will be executed in reverse order of | |
/// appearance. | |
/// The idea to this is from | |
/// http://blog.memsql.com/c-error-handling-with-auto/ | |
#define TOKEN_PASTE_WRAPPED(x, y) x##y | |
#define TOKEN_PASTE(x, y) TOKEN_PASTE_WRAPPED(x, y) | |
template <typename T> | |
struct AutoOutOfScope { | |
explicit AutoOutOfScope(T& destructor) : m_destructor(destructor) {} | |
~AutoOutOfScope() { try { m_destructor(); } catch (...) { } } | |
private: | |
T& m_destructor; | |
}; | |
#define DEFER_INTERNAL(Destructor, funcname, objname) \ | |
auto funcname = [&]() { Destructor; }; \ | |
AutoOutOfScope<decltype(funcname)> objname(funcname); | |
#define DEFER(Destructor) \ | |
DEFER_INTERNAL(Destructor, TOKEN_PASTE(auto_fun, __LINE__), \ | |
TOKEN_PASTE(auto_obj, __LINE__)) | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You just need to include https://github.com/nodejs/http-parser