Skip to content

Instantly share code, notes, and snippets.

@graetzer
Created April 16, 2018 01:36
Show Gist options
  • Save graetzer/d586eb5a90e81b3d691ccb93111ac70b to your computer and use it in GitHub Desktop.
Save graetzer/d586eb5a90e81b3d691ccb93111ac70b to your computer and use it in GitHub Desktop.
C++ Simple HTTP Client
/// 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
}
/// 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
#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
@graetzer
Copy link
Author

You just need to include https://github.com/nodejs/http-parser

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment