This commit is contained in:
Benjamin Sergeant 2019-02-28 21:18:27 -08:00
parent 6d56f7223a
commit 285c12775a
9 changed files with 271 additions and 135 deletions

View File

@ -9,15 +9,19 @@
#include "IXWebSocketHttpHeaders.h"
#include "IXSocketFactory.h"
#include <iostream>
#include <sstream>
#include <iomanip>
#include <vector>
#include <cstring>
#include <zlib.h>
namespace ix
{
const std::string HttpClient::kPost = "POST";
const std::string HttpClient::kGet = "GET";
const std::string HttpClient::kHead = "HEAD";
HttpClient::HttpClient()
{
@ -29,32 +33,29 @@ namespace ix
}
HttpResponse HttpClient::request(
const std::string& url,
const std::string& verb,
const std::string& body,
HttpRequestArgs args)
const HttpRequestArgs& args,
int redirects)
{
uint64_t uploadSize = 0;
uint64_t downloadSize = 0;
int code = 0;
WebSocketHttpHeaders headers;
std::string payload;
std::string protocol, host, path, query;
int port;
bool websocket = false;
if (!parseUrl(args.url, protocol, host, path, query, port))
if (!UrlParser::parse(url, protocol, host, path, query, port, websocket))
{
std::stringstream ss;
ss << "Cannot parse url: " << args.url;
return std::make_tuple(code, headers, payload, ss.str());
}
if (protocol != "http" && protocol != "https")
{
std::stringstream ss;
ss << "Invalid protocol: " << protocol
<< " for url " << args.url
<< " . Supported protocols are http and https";
return std::make_tuple(code, headers, payload, ss.str());
ss << "Cannot parse url: " << url;
return std::make_tuple(code, HttpErrorCode_UrlMalformed,
headers, payload, ss.str(),
uploadSize, downloadSize);
}
bool tls = protocol == "https";
@ -63,7 +64,9 @@ namespace ix
if (!_socket)
{
return std::make_tuple(code, headers, payload, errorMsg);
return std::make_tuple(code, HttpErrorCode_CannotCreateSocket,
headers, payload, errorMsg,
uploadSize, downloadSize);
}
// Build request string
@ -84,7 +87,7 @@ namespace ix
ss << it.first << ": " << it.second << "\r\n";
}
if (verb == "POST")
if (verb == kPost)
{
ss << "Content-Length: " << body.size() << "\r\n";
@ -102,37 +105,51 @@ namespace ix
}
std::string req(ss.str());
std::string errMsg;
std::atomic<bool> requestInitCancellation(false);
// Make a cancellation object dealing with connection timeout
auto isCancellationRequested =
makeCancellationRequestWithTimeout(args.timeoutSecs, requestInitCancellation);
makeCancellationRequestWithTimeout(args.connectTimeout, requestInitCancellation);
bool success = _socket->connect(host, port, errMsg, isCancellationRequested);
if (!success)
{
std::stringstream ss;
ss << "Cannot connect to url: " << args.url;
return std::make_tuple(code, headers, payload, ss.str());
ss << "Cannot connect to url: " << url;
return std::make_tuple(code, HttpErrorCode_CannotConnect,
headers, payload, ss.str(),
uploadSize, downloadSize);
}
// Make a new cancellation object dealing with transfer timeout
isCancellationRequested =
makeCancellationRequestWithTimeout(args.transferTimeout, requestInitCancellation);
if (args.verbose)
{
std::cerr << "Sending " << verb << " request "
<< "to " << host << ":" << port << std::endl
<< "request size: " << req.size() << " bytes" << std::endl
<< "=============" << std::endl
<< req
<< "=============" << std::endl
<< std::endl;
std::stringstream ss;
ss << "Sending " << verb << " request "
<< "to " << host << ":" << port << std::endl
<< "request size: " << req.size() << " bytes" << std::endl
<< "=============" << std::endl
<< req
<< "=============" << std::endl
<< std::endl;
log(ss.str(), args);
}
if (!_socket->writeBytes(req, isCancellationRequested))
{
std::string errorMsg("Cannot send request");
return std::make_tuple(code, headers, payload, errorMsg);
return std::make_tuple(code, HttpErrorCode_SendError,
headers, payload, errorMsg,
uploadSize, downloadSize);
}
uploadSize = req.size();
auto lineResult = _socket->readLine(isCancellationRequested);
auto lineValid = lineResult.first;
auto line = lineResult.second;
@ -140,13 +157,24 @@ namespace ix
if (!lineValid)
{
std::string errorMsg("Cannot retrieve status line");
return std::make_tuple(code, headers, payload, errorMsg);
return std::make_tuple(code, HttpErrorCode_CannotReadStatusLine,
headers, payload, errorMsg,
uploadSize, downloadSize);
}
if (args.verbose)
{
std::stringstream ss;
ss << "Status line " << line;
log(ss.str(), args);
}
if (sscanf(line.c_str(), "HTTP/1.1 %d", &code) != 1)
{
std::string errorMsg("Cannot parse response code from status line");
return std::make_tuple(code, headers, payload, errorMsg);
return std::make_tuple(code, HttpErrorCode_MissingStatus,
headers, payload, errorMsg,
uploadSize, downloadSize);
}
auto result = parseHttpHeaders(_socket, isCancellationRequested);
@ -155,39 +183,50 @@ namespace ix
if (!headersValid)
{
code = 0; // 0 ?
std::string errorMsg("Cannot parse http headers");
return std::make_tuple(code, headers, payload, errorMsg);
return std::make_tuple(code, HttpErrorCode_HeaderParsingError,
headers, payload, errorMsg,
uploadSize, downloadSize);
}
// Redirect ?
// FIXME wrong conditional
if ((code == 301 || code == 308) && args.followRedirects)
if ((code >= 301 && code <= 308) && args.followRedirects)
{
if (headers.find("location") == headers.end())
if (headers.find("Location") == headers.end())
{
code = 0; // 0 ?
std::string errorMsg("Missing location header for redirect");
return std::make_tuple(code, headers, payload, errorMsg);
return std::make_tuple(code, HttpErrorCode_MissingLocation,
headers, payload, errorMsg,
uploadSize, downloadSize);
}
if (redirects >= args.maxRedirects)
{
std::stringstream ss;
ss << "Too many redirects: " << redirects;
return std::make_tuple(code, HttpErrorCode_TooManyRedirects,
headers, payload, ss.str(),
uploadSize, downloadSize);
}
// Recurse
std::string location = headers["location"];
return request(verb, body, args);
std::string location = headers["Location"];
return request(location, verb, body, args, redirects+1);
}
if (verb == "HEAD")
{
return std::make_tuple(code, headers, payload, std::string());
return std::make_tuple(code, HttpErrorCode_Ok,
headers, payload, std::string(),
uploadSize, downloadSize);
}
// Parse response:
// http://bryce-thomas.blogspot.com/2012/01/technical-parsing-http-to-extract.html
if (headers.find("content-length") != headers.end())
if (headers.find("Content-Length") != headers.end())
{
ssize_t contentLength = -1;
ss.str("");
ss << headers["content-length"];
ss << headers["Content-Length"];
ss >> contentLength;
payload.reserve(contentLength);
@ -198,18 +237,16 @@ namespace ix
char c;
if (!_socket->readByte(&c, isCancellationRequested))
{
ss.str("");
ss << "Cannot read byte";
return std::make_tuple(-1, headers, payload, "Cannot read byte");
return std::make_tuple(code, HttpErrorCode_ReadError,
headers, payload, "Cannot read byte",
uploadSize, downloadSize);
}
payload += c;
}
std::cout << "I WAS HERE" << std::endl;
}
else if (headers.find("transfer-encoding") != headers.end() &&
headers["transfer-encoding"] == "chunked")
else if (headers.find("Transfer-Encoding") != headers.end() &&
headers["Transfer-Encoding"] == "chunked")
{
std::stringstream ss;
@ -220,9 +257,9 @@ namespace ix
if (!lineResult.first)
{
code = 0; // 0 ?
std::string errorMsg("Cannot read http body");
return std::make_tuple(code, headers, payload, errorMsg);
return std::make_tuple(code, HttpErrorCode_ChunkReadError,
headers, payload, errorMsg,
uploadSize, downloadSize);
}
uint64_t chunkSize;
@ -232,8 +269,10 @@ namespace ix
if (args.verbose)
{
std::cerr << "Reading " << chunkSize << " bytes"
<< std::endl;
std::stringstream oss;
oss << "Reading " << chunkSize << " bytes"
<< std::endl;
log(oss.str(), args);
}
payload.reserve(payload.size() + chunkSize);
@ -245,9 +284,10 @@ namespace ix
char c;
if (!_socket->readByte(&c, isCancellationRequested))
{
ss.str("");
ss << "Cannot read byte";
return std::make_tuple(-1, headers, payload, ss.str());
errorMsg = "Cannot read byte";
return std::make_tuple(code, HttpErrorCode_ChunkReadError,
headers, payload, errorMsg,
uploadSize, downloadSize);
}
payload += c;
@ -257,9 +297,9 @@ namespace ix
if (!lineResult.first)
{
code = 0; // 0 ?
std::string errorMsg("Cannot read http body");
return std::make_tuple(code, headers, payload, errorMsg);
return std::make_tuple(code, HttpErrorCode_ChunkReadError,
headers, payload, errorMsg,
uploadSize, downloadSize);
}
if (chunkSize == 0) break;
@ -271,48 +311,57 @@ namespace ix
}
else
{
code = 0; // 0 ?
std::string errorMsg("Cannot read http body");
return std::make_tuple(-1, headers, payload, errorMsg);
return std::make_tuple(code, HttpErrorCode_CannotReadBody,
headers, payload, errorMsg,
uploadSize, downloadSize);
}
downloadSize = payload.size();
// If the content was compressed with gzip, decode it
if (headers["Content-Encoding"] == "gzip")
{
if (args.verbose) std::cout << "Decoding gzip..." << std::endl;
std::string decompressedPayload;
if (!gzipInflate(payload, decompressedPayload))
{
std::string errorMsg("Error decompressing payload");
return std::make_tuple(-1, headers, payload, errorMsg);
return std::make_tuple(code, HttpErrorCode_Gzip,
headers, payload, errorMsg,
uploadSize, downloadSize);
}
payload = decompressedPayload;
}
return std::make_tuple(code, headers, payload, "");
return std::make_tuple(code, HttpErrorCode_Ok,
headers, payload, std::string(),
uploadSize, downloadSize);
}
HttpResponse HttpClient::get(HttpRequestArgs args)
HttpResponse HttpClient::get(const std::string& url,
const HttpRequestArgs& args)
{
return request("GET", std::string(), args);
return request(url, kGet, std::string(), args);
}
HttpResponse HttpClient::head(HttpRequestArgs args)
HttpResponse HttpClient::head(const std::string& url,
const HttpRequestArgs& args)
{
return request("HEAD", std::string(), args);
return request(url, kHead, std::string(), args);
}
HttpResponse HttpClient::post(const HttpParameters& httpParameters,
HttpRequestArgs args)
HttpResponse HttpClient::post(const std::string& url,
const HttpParameters& httpParameters,
const HttpRequestArgs& args)
{
return request("POST", serializeHttpParameters(httpParameters), args);
return request(url, kPost, serializeHttpParameters(httpParameters), args);
}
HttpResponse HttpClient::post(const std::string& body,
HttpRequestArgs args)
HttpResponse HttpClient::post(const std::string& url,
const std::string& body,
const HttpRequestArgs& args)
{
return request("POST", body, args);
return request(url, kPost, body, args);
}
std::string HttpClient::urlEncode(const std::string& value)
@ -367,7 +416,7 @@ namespace ix
std::string& out)
{
z_stream inflateState;
memset(&inflateState, 0, sizeof(inflateState));
std::memset(&inflateState, 0, sizeof(inflateState));
inflateState.zalloc = Z_NULL;
inflateState.zfree = Z_NULL;
@ -385,7 +434,7 @@ namespace ix
const int kBufferSize = 1 << 14;
std::unique_ptr<unsigned char[]> compressBuffer =
std::unique_ptr<unsigned char[]> compressBuffer =
std::make_unique<unsigned char[]>(kBufferSize);
do
@ -410,4 +459,13 @@ namespace ix
inflateEnd(&inflateState);
return true;
}
void HttpClient::log(const std::string& msg,
const HttpRequestArgs& args)
{
if (args.logger)
{
args.logger(msg);
}
}
}

View File

@ -19,18 +19,48 @@
namespace ix
{
using HttpResponse = std::tuple<int, WebSocketHttpHeaders, std::string, std::string>;
enum HttpErrorCode
{
HttpErrorCode_Ok = 0,
HttpErrorCode_CannotConnect = 1,
HttpErrorCode_Timeout = 2,
HttpErrorCode_Gzip = 3,
HttpErrorCode_UrlMalformed = 4,
HttpErrorCode_CannotCreateSocket = 5,
HttpErrorCode_SendError = 6,
HttpErrorCode_ReadError = 7,
HttpErrorCode_CannotReadStatusLine = 8,
HttpErrorCode_MissingStatus = 9,
HttpErrorCode_HeaderParsingError = 10,
HttpErrorCode_MissingLocation = 11,
HttpErrorCode_TooManyRedirects = 12,
HttpErrorCode_ChunkReadError = 13,
HttpErrorCode_CannotReadBody = 14
};
using HttpResponse = std::tuple<int, // status
HttpErrorCode, // error code
WebSocketHttpHeaders,
std::string, // payload
std::string, // error msg
uint64_t, // upload size
uint64_t>; // download size
using HttpParameters = std::map<std::string, std::string>;
using Logger = std::function<void(const std::string&)>;
struct HttpRequestArgs
{
std::string url;
WebSocketHttpHeaders extraHeaders;
std::string body;
int timeoutSecs;
int connectTimeout;
int transferTimeout;
bool followRedirects;
int maxRedirects;
bool verbose;
bool compress;
Logger logger;
};
class HttpClient {
@ -38,27 +68,39 @@ namespace ix
HttpClient();
~HttpClient();
HttpResponse get(HttpRequestArgs args);
HttpResponse head(HttpRequestArgs args);
HttpResponse get(const std::string& url,
const HttpRequestArgs& args);
HttpResponse head(const std::string& url,
const HttpRequestArgs& args);
HttpResponse post(const HttpParameters& httpParameters,
HttpRequestArgs args);
HttpResponse post(const std::string& body,
HttpRequestArgs args);
HttpResponse post(const std::string& url,
const HttpParameters& httpParameters,
const HttpRequestArgs& args);
HttpResponse post(const std::string& url,
const std::string& body,
const HttpRequestArgs& args);
private:
HttpResponse request(const std::string& verb,
HttpResponse request(const std::string& url,
const std::string& verb,
const std::string& body,
HttpRequestArgs args);
const HttpRequestArgs& args,
int redirects = 0);
std::string serializeHttpParameters(const HttpParameters& httpParameters);
std::string urlEncode(const std::string& value);
void log(const std::string& msg, const HttpRequestArgs& args);
bool gzipInflate(
const std::string& in,
std::string& out);
std::shared_ptr<Socket> _socket;
const static std::string kPost;
const static std::string kGet;
const static std::string kHead;
};
}

View File

@ -6,23 +6,29 @@
#include "IXUrlParser.h"
#include <regex>
#include <iostream>
#include <sstream>
namespace ix
{
bool parseUrl(const std::string& url,
std::string& protocol,
std::string& host,
std::string& path,
std::string& query,
int& port)
//
// The only difference between those 2 regex is the protocol
//
std::regex UrlParser::_httpRegex("(http|https)://([^/ :]+):?([^/ ]*)(/?[^ #?]*)\\x3f?([^ #]*)#?([^ ]*)");
std::regex UrlParser::_webSocketRegex("(ws|wss)://([^/ :]+):?([^/ ]*)(/?[^ #?]*)\\x3f?([^ #]*)#?([^ ]*)");
bool UrlParser::parse(const std::string& url,
std::string& protocol,
std::string& host,
std::string& path,
std::string& query,
int& port,
bool websocket)
{
std::regex ex("(ws|wss|http|https)://([^/ :]+):?([^/ ]*)(/?[^ #?]*)\\x3f?([^ #]*)#?([^ ]*)");
std::cmatch what;
if (!regex_match(url.c_str(), what, ex))
if (!regex_match(url.c_str(), what,
websocket ? _webSocketRegex : _httpRegex))
{
return false;
}
@ -77,12 +83,12 @@ namespace ix
return true;
}
void printUrl(const std::string& url)
void UrlParser::printUrl(const std::string& url, bool websocket)
{
std::string protocol, host, path, query;
int port {0};
if (!parseUrl(url, protocol, host, path, query, port))
if (!parse(url, protocol, host, path, query, port, websocket))
{
return;
}

View File

@ -7,15 +7,25 @@
#pragma once
#include <string>
#include <regex>
namespace ix
{
bool parseUrl(const std::string& url,
std::string& protocol,
std::string& host,
std::string& path,
std::string& query,
int& port);
class UrlParser
{
public:
static bool parse(const std::string& url,
std::string& protocol,
std::string& host,
std::string& path,
std::string& query,
int& port,
bool websocket);
void printUrl(const std::string& url);
static void printUrl(const std::string& url, bool websocket);
private:
static std::regex _httpRegex;
static std::regex _webSocketRegex;
};
}

View File

@ -24,13 +24,13 @@ namespace ix
{
bool operator() (const unsigned char& c1, const unsigned char& c2) const
{
return std::tolower(c1) < std::tolower(c2);
return std::tolower(c1) < std::tolower(c2);
}
};
bool operator() (const std::string & s1, const std::string & s2) const
{
return std::lexicographical_compare
return std::lexicographical_compare
(s1.begin(), s1.end(), // source range
s2.begin(), s2.end(), // dest range
NocaseCompare()); // comparison

View File

@ -72,8 +72,9 @@ namespace ix
{
std::string protocol, host, path, query;
int port;
bool websocket = true;
if (!parseUrl(url, protocol, host, path, query, port))
if (!UrlParser::parse(url, protocol, host, path, query, port, websocket))
{
return WebSocketInitResult(false, 0,
std::string("Could not parse URL ") + url);

View File

@ -33,7 +33,9 @@ int main(int argc, char** argv)
bool save = false;
bool compress = false;
int port = 8080;
int connectTimeOutSeconds = 3;
int connectTimeOut = 60;
int transferTimeout = 1800;
int maxRedirects = 5;
CLI::App* sendApp = app.add_subcommand("send", "Send a file");
sendApp->add_option("url", url, "Connection url")->required();
@ -68,12 +70,14 @@ int main(int argc, char** argv)
httpClientApp->add_option("-F", data, "Form data")->join();
httpClientApp->add_option("-H", headers, "Header")->join();
httpClientApp->add_option("--output", output, "Output file");
httpClientApp->add_flag("-I", headersOnly, "Header");
httpClientApp->add_flag("-L", followRedirects, "Header");
httpClientApp->add_flag("-I", headersOnly, "Send a HEAD request");
httpClientApp->add_flag("-L", followRedirects, "Follow redirects");
httpClientApp->add_option("--max-redirects", maxRedirects, "Max Redirects");
httpClientApp->add_flag("-v", verbose, "Verbose");
httpClientApp->add_flag("-O", save, "Save to disk");
httpClientApp->add_flag("--compress", compress, "gzip compression");
httpClientApp->add_option("--connect-timeout", connectTimeOutSeconds, "Connection timeout");
httpClientApp->add_flag("-O", save, "Save output to disk");
httpClientApp->add_flag("--compress", compress, "Enable gzip compression");
httpClientApp->add_option("--connect-timeout", connectTimeOut, "Connection timeout");
httpClientApp->add_option("--transfer-timeout", transferTimeout, "Transfer timeout");
CLI11_PARSE(app, argc, argv);
@ -114,10 +118,10 @@ int main(int argc, char** argv)
}
else if (app.got_subcommand("curl"))
{
return ix::ws_http_client_main(url, headers, data,
headersOnly, connectTimeOutSeconds,
followRedirects, verbose, save, output,
compress);
return ix::ws_http_client_main(url, headers, data, headersOnly,
connectTimeOut, transferTimeout,
followRedirects, maxRedirects, verbose,
save, output, compress);
}
return 1;

View File

@ -13,8 +13,10 @@ namespace ix
const std::string& headers,
const std::string& data,
bool headersOnly,
int timeoutSecs,
int connectTimeout,
int transferTimeout,
bool followRedirects,
int maxRedirects,
bool verbose,
bool save,
const std::string& output,

View File

@ -86,20 +86,27 @@ namespace ix
const std::string& headersData,
const std::string& data,
bool headersOnly,
int timeoutSecs,
int connectTimeout,
int transferTimeout,
bool followRedirects,
int maxRedirects,
bool verbose,
bool save,
const std::string& output,
bool compress)
{
HttpRequestArgs args;
args.url = url;
args.extraHeaders = parseHeaders(headersData);
args.timeoutSecs = timeoutSecs;
args.connectTimeout = connectTimeout;
args.transferTimeout = transferTimeout;
args.followRedirects = followRedirects;
args.maxRedirects = maxRedirects;
args.verbose = verbose;
args.compress = compress;
args.logger = [](const std::string& msg)
{
std::cout << msg;
};
HttpParameters httpParameters = parsePostParameters(data);
@ -107,34 +114,40 @@ namespace ix
HttpResponse out;
if (headersOnly)
{
out = httpClient.head(args);
out = httpClient.head(url, args);
}
else if (data.empty())
{
out = httpClient.get(args);
out = httpClient.get(url, args);
}
else
{
out = httpClient.post(httpParameters, args);
out = httpClient.post(url, httpParameters, args);
}
auto errorCode = std::get<0>(out);
auto responseHeaders = std::get<1>(out);
auto payload = std::get<2>(out);
auto errorMsg = std::get<3>(out);
auto statusCode = std::get<0>(out);
auto errorCode = std::get<1>(out);
auto responseHeaders = std::get<2>(out);
auto payload = std::get<3>(out);
auto errorMsg = std::get<4>(out);
auto uploadSize = std::get<5>(out);
auto downloadSize = std::get<6>(out);
for (auto it : responseHeaders)
{
std::cerr << it.first << ": " << it.second << std::endl;
}
std::cerr << "error code: " << errorCode << std::endl;
if (errorCode != 200)
std::cerr << "Upload size: " << uploadSize << std::endl;
std::cerr << "Download size: " << downloadSize << std::endl;
std::cerr << "Status: " << statusCode << std::endl;
if (errorCode != HttpErrorCode_Ok)
{
std::cerr << "error message: " << errorMsg << std::endl;
}
if (!headersOnly && errorCode == 200)
if (!headersOnly && errorCode == HttpErrorCode_Ok)
{
if (save || !output.empty())
{