From bab67f70f939be5af9e2b060d77eeb4c1f0481e9 Mon Sep 17 00:00:00 2001 From: VaclavT Date: Fri, 14 Jan 2022 01:00:03 +0100 Subject: [PATCH] ssl client bit of work --- clib/sslclient.cpp | 212 +++++++++++++++++++++++++-------------------- clib/sslclient.h | 24 +++-- doc/Doc.md | 2 +- ml.cpp | 22 +++-- 4 files changed, 154 insertions(+), 106 deletions(-) diff --git a/clib/sslclient.cpp b/clib/sslclient.cpp index d4af933..c3a8b84 100644 --- a/clib/sslclient.cpp +++ b/clib/sslclient.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include #include @@ -16,13 +16,93 @@ #include #include +// https://stackoverflow.com/questions/25896916/parse-http-headers-in-c +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET -HttpClient::HttpClient() {}; +std::pair HttpClient::doRequest(const std::string &method, const std::string &url, const std::map &headers, const std::string &request_body) { -std::pair HttpClient::doGetRequest(const std::string &url, const std::unordered_map &headers) { - // https://stackoverflow.com/questions/25896916/parse-http-headers-in-c + // split url to parts + parseURL(url); + if (proto != "https") + throw std::runtime_error("Unsupported protocol"); - std::regex rgx{R"(^(?:((?:https?|s?ftp):)//)([^:/\s]+)(?::(\d*))?(?:/([^\s?#]+)?([?][^?#]*)?(#.*)?)?)"}; + // create headers + const std::string headers_string = createRequestHeaders(headers); + + // create request + std::string request = method + " " + full_url + " HTTP/1.0\r\nHost: " + server + headers_string + "\r\n\r\n"; + if (method == "POST") + request.append(request_body); + + // read response + int bytes_read = sslRequest(server, request); // TODO memory leaks ??? + if (bytes_read <= 0) { + std::cerr << "no data read" << std::endl; + return std::make_pair(403, ""); + } + + + // get headers + std::string::size_type position = ssl_read_packet.find("\r\n\r\n"); + if (position == std::string::npos) { + std::cerr << "end of headers not found" << std::endl; + // TODO invalid packet, throw exception + } + + const std::string hdr = ssl_read_packet.substr(0, position); + auto status_pos = hdr.find("\r\n"); + + const std::string status_str = hdr.substr(0, status_pos); + const std::string response_headers = hdr.substr(status_pos + 2, hdr.length() - 2 - status_pos); + + // parse status code + const int resp_status = responseStatusCode(status_str); + + // parse headers + responseHeaders(response_headers); + + // get body + const std::string body = ssl_read_packet.substr(position + 4, ssl_read_packet.length() - 4 - position); + + + return std::make_pair(resp_status, body); +} + +void HttpClient::responseHeaders(const std::string &hdr) { + std::istringstream resp(hdr); + std::string header; + std::string::size_type index; + while (std::getline(resp, header) && header != "\r") { + index = header.find(": ", 0); + if (index != std::string::npos) + headers_map.insert(std::make_pair(header.substr(0, index), header.substr(index + 1))); + } +} + +int HttpClient::responseStatusCode(const std::string &status_str) const { + int resp_status = 200; // default is OK + + std::regex status_rgx{"^HTTP/\\d\\.\\d (\\d{3}) .+$"}; + std::smatch status_matches; + if (std::regex_search(status_str, status_matches, status_rgx)) { + if (status_matches.size() > 1) + resp_status = std::stoi(status_matches[1].str()); + } + return resp_status; +} + +std::string HttpClient::createRequestHeaders(const std::map &headers) { + std::string headers_string; + for (const auto & header : headers) { + headers_string.append("\r\n" + header.first + ": " + header.second); +// std::cerr << "KEY: `" << it->first << "`, VALUE: `" << it->second << '`' << std::endl; + } + return headers_string; +} + +void HttpClient::parseURL(const std::string &url) { + std::regex rgx{R"(^(?:((?:https?|s?ftp))://)([^:/\s]+)(?::(\d*))?(?:/([^\s?#]+)?([?][^?#]*)?(#.*)?)?)"}; std::smatch matches; if (std::regex_search(url, matches, rgx)) { @@ -55,67 +135,14 @@ std::pair HttpClient::doGetRequest(const std::string &url, con } else { std::cerr << "Match not found" << std::endl; // TODO better message } +} - std::string headers_string = ""; - for (auto it = headers.begin(); it != headers.end(); ++it) { - headers_string.append("\r\n" + it->first + ": " + it->second); -// std::cerr << "KEY: `" << it->first << "`, VALUE: `" << it->second << '`' << std::endl; - } - - - std::string request = "GET " + full_url + " HTTP/1.0\r\nHost: " + server + headers_string + "\r\n\r\n"; - - // TODO memory leaks ??? - int bytes_read = sslRequest(server, request); - if (bytes_read <= 0) { - std::cerr << "no data read" << std::endl; - return std::make_pair(403, ""); - } - - std::string::size_type position = ssl_read_packet.find("\r\n\r\n"); - if (position == std::string::npos) { - std::cerr << "substring not found" << std::endl; // TODO invalid packet - } - - std::string hdr = ssl_read_packet.substr(0, position); - auto status_pos = hdr.find("\r\n"); - - std::string status_str = hdr.substr(0, status_pos); - hdr = hdr.substr(status_pos + 2, hdr.length() - 2 - status_pos); - - // TODO parse status code - std::regex status_rgx{"^HTTP/\\d\\.\\d (\\d{3}) .+$"}; - std::smatch status_matches; - if (std::regex_search(status_str, status_matches, status_rgx)) { - if (status_matches.size() > 1) { - auto sta = status_matches[1].str(); // string "200" - // std::cout << "status: " << sta << std::endl; - } - } - - std::string body = ssl_read_packet.substr(position + 4, ssl_read_packet.length() - 4 - position); - - - std::istringstream resp(hdr); - std::string header; - std::string::size_type index; - while (std::getline(resp, header) && header != "\r") { - index = header.find(": ", 0); - if (index != std::string::npos) { - headers_map.insert(std::make_pair(header.substr(0, index), header.substr(index + 1))); - } - } - - // TODO if error return error desc in string - return std::make_pair(200, body); -}; - -std::string HttpClient::inetAddress(std::string hostname) { +std::string HttpClient::inetAddress(const std::string &hostname) { hostent *record = gethostbyname(hostname.c_str()); if (record == NULL) { throw std::runtime_error(hostname + " network unavailable."); } - in_addr *address = (in_addr *) record->h_addr; + auto *address = (in_addr *) record->h_addr; std::string ip_address = inet_ntoa(*address); return ip_address; @@ -145,16 +172,15 @@ int HttpClient::sslRecvPacket() { return -1; } - return ssl_read_packet.length(); + return (int) ssl_read_packet.length(); } -int HttpClient::sslSendPacket(std::string buf) { - int len = SSL_write(ssl, buf.c_str(), strlen(buf.c_str())); +int HttpClient::sslSendPacket(const std::string &buf) { + int len = SSL_write(ssl, buf.c_str(), (int)strlen(buf.c_str())); if (len < 0) { int err = SSL_get_error(ssl, len); switch (err) { case SSL_ERROR_WANT_WRITE: - return 0; case SSL_ERROR_WANT_READ: return 0; case SSL_ERROR_ZERO_RETURN: @@ -164,8 +190,8 @@ int HttpClient::sslSendPacket(std::string buf) { return -1; } } - int errr = SSL_get_error(ssl, len); - return buf.length(); + + return len; } int HttpClient::sslRequest(const std::string &server_name, const std::string &request) { @@ -177,7 +203,6 @@ int HttpClient::sslRequest(const std::string &server_name, const std::string &re return -1; } - // socket address std::string server_ip = inetAddress(server_name); struct sockaddr_in sa; @@ -201,7 +226,7 @@ int HttpClient::sslRequest(const std::string &server_name, const std::string &re ssl = SSL_new(ctx); if (!ssl) { printf("sslRequest, error creating SSL.\n"); - log_ssl(); + logSSL(); return -1; } sock = SSL_get_fd(ssl); @@ -212,25 +237,31 @@ int HttpClient::sslRequest(const std::string &server_name, const std::string &re int err = SSL_connect(ssl); if (err <= 0) { printf("sslRequest, error creating SSL connection. err=%x\n", err); - log_ssl(); + logSSL(); fflush(stdout); return -1; } // log cipher - printf ("SSL connection using %s\n", SSL_get_cipher (ssl)); - ShowCerts(ssl); + // printf ("SSL connection using %s\n", SSL_get_cipher (ssl)); + // showCerts(ssl); + // send request - // std::out << request << std::endl; + // printf ("SSL sending request %s\n", request.c_str()); + int written_bytes = sslSendPacket(request); + if (written_bytes != request.length()) { + printf("sslRequest, error sending request\n"); + return -1; + } // read response and return its length return sslRecvPacket(); } -void HttpClient::log_ssl() { - int err; +void HttpClient::logSSL() { + unsigned long err; while ((err = ERR_get_error())) { char *str = ERR_error_string(err, 0); if (!str) @@ -239,24 +270,21 @@ void HttpClient::log_ssl() { } } - - -void HttpClient::ShowCerts(SSL* ssl) -{ X509 *cert; +void HttpClient::showCerts(SSL* ssl) { + X509 *cert; char *line; cert = SSL_get_peer_certificate(ssl); /* get the server's certificate */ - if ( cert != NULL ) - { - printf("Server certificates:\n"); - line = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0); - printf("Subject: %s\n", line); - free(line); /* free the malloc'ed string */ - line = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0); - printf("Issuer: %s\n", line); - free(line); /* free the malloc'ed string */ - X509_free(cert); /* free the malloc'ed certificate copy */ -} -else - printf("No certificates.\n"); + if ( cert != NULL ) { + printf("Server certificates:\n"); + line = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0); + printf("Subject: %s\n", line); + free(line); /* free the malloc'ed string */ + line = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0); + printf("Issuer: %s\n", line); + free(line); /* free the malloc'ed string */ + X509_free(cert); /* free the malloc'ed certificate copy */ + } else { + printf("No certificates.\n"); + } } \ No newline at end of file diff --git a/clib/sslclient.h b/clib/sslclient.h index f3a565e..f20100c 100644 --- a/clib/sslclient.h +++ b/clib/sslclient.h @@ -5,6 +5,7 @@ #include #include +#include class HttpClient { @@ -16,23 +17,30 @@ private: std::string full_url, proto, server, port, uri, params, href; std::basic_string ssl_read_packet; - std::unordered_map headers_map; + std::map headers_map; public: - HttpClient(); + HttpClient() = default; - std::pair - doGetRequest(const std::string &url, const std::unordered_map &headers); + std::pair doRequest(const std::string &method, const std::string &url, const std::map &headers, const std::string &request_body); private: - std::string inetAddress(std::string hostname); + static std::string inetAddress(const std::string &hostname); int sslRecvPacket(); - int sslSendPacket(std::string buf); + int sslSendPacket(const std::string &buf); int sslRequest(const std::string &server_name, const std::string &request); - void log_ssl(); - void ShowCerts(SSL* ssl); + static void logSSL(); + static void showCerts(SSL* ssl); + + void parseURL(const std::string &url); + + [[nodiscard]] static std::string createRequestHeaders(const std::map &headers) ; + + int responseStatusCode(const std::string &status_str) const; + + void responseHeaders(const std::string &hdr); }; diff --git a/doc/Doc.md b/doc/Doc.md index 1848413..55b0210 100644 --- a/doc/Doc.md +++ b/doc/Doc.md @@ -79,7 +79,7 @@ |`(read-file filename)`|Get the contents of a file|| |`(read-file-lines filename lambda)`|Reads file and for each line call lambda with passing the line as a parameter|`(read-file-lines "/tmp/f.txt" (lambda (ln) (print ln))`| |`(write-file filename content-str)`|Write a string to a file|| -|`(read-url url [headers])`|Reads URL|Returns list (status-code content)| +|`(read-url url [headers] [body] [method])`|Reads URL|Returns list (status-code content)| |`(system-cmd command_str)`|Execute system command|| |`(ls-dir dir)`|List a dir|List of directory entries| |`(is-file? filename)`|Returns true if passed filename is a file|| diff --git a/ml.cpp b/ml.cpp index 827f5b7..a913b88 100644 --- a/ml.cpp +++ b/ml.cpp @@ -1202,13 +1202,16 @@ MlValue read_url(std::vector args, MlEnvironment &env) { eval_args(args, env); // PERF optimize it for memory usage and performance - if (args.empty() || args.size() > 2) + if (args.empty() || args.size() > 4) throw MlError(MlValue("read_url", read_url), env, args.empty() ? TOO_FEW_ARGS : TOO_MANY_ARGS); - std::unordered_map headers = {}; - HttpClient client; + std::string url = args[0].as_string(); + std::map headers = {}; + std::string method = "GET"; + std::string body; - if (args.size() == 2) { + // headers + if (args.size() > 1) { for (const auto &hdr_val_pair: args[1].as_list()) { // TODO check its 2 string elements list const auto &pair = hdr_val_pair.as_list(); @@ -1216,7 +1219,16 @@ MlValue read_url(std::vector args, MlEnvironment &env) { } } - std::pair result = client.doGetRequest(args[0].as_string(), headers); + // body + if (args.size() > 2) + body = args[2].as_string(); + + // method + if (args.size() > 3) + method = args[3].as_string(); + + + std::pair result = HttpClient{}.doRequest(method, url, headers, body); return std::vector{MlValue(result.first), MlValue::string(result.second)}; }