/* * This file is a part of Winix * and is distributed under the 2-Clause BSD licence. * Author: Tomasz Sowa */ /* * Copyright (c) 2010-2022, Tomasz Sowa * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * */ #include "http.h" #include "core/lock.h" #include "core/log.h" #include "utf8/utf8.h" #include "core/header.h" namespace Winix { Http::Http() { begin(); } /* * prepare to start a new request */ Http & Http::begin() { curl = nullptr; synchro = nullptr; browser_name = "Winix"; conn_timeout = 30; http_headers = nullptr; read_function_input = nullptr; read_function_index = 0; additional_space_headers_to_send = nullptr; additional_string_headers_to_send = nullptr; additional_wstring_headers_to_send = nullptr; bearer_token = nullptr; output_headers_space = nullptr; change_header_names_to_lower = true; output_content_type = nullptr; debug_mode = 0; debug_info = nullptr; follow_location = true; verify_ssl_cert = true; forse_ssl_version = false; ssl_version = 0; ca_path = nullptr; ca_path_utf8.clear(); client_cert = nullptr; client_key = nullptr; client_cert_utf8.clear(); client_key_utf8.clear(); return *this; } Http::~Http() { uninitialize_curl(); } Http & Http::add_header(const pt::TextStream & header) { header.to_str(temp_header_ascii); add_header(temp_header_ascii); return *this; } Http & Http::add_header(const pt::WTextStream & header) { header.to_str(temp_header_ascii); add_header(temp_header_ascii); return *this; } Http & Http::add_header(const std::string & header) { if( !header.empty() ) { http_headers = curl_slist_append(http_headers, header.c_str()); } return *this; } Http & Http::add_header(const std::wstring & header) { if( !header.empty() ) { pt::wide_to_utf8(header, temp_header_ascii); http_headers = curl_slist_append(http_headers, temp_header_ascii.c_str()); } return *this; } Http & Http::add_header(const char * str, size_t len) { if( len > 0 ) { temp_header_ascii.assign(str, len); http_headers = curl_slist_append(http_headers, temp_header_ascii.c_str()); } return *this; } Http & Http::add_header(const wchar_t * str, size_t len) { if( len > 0 ) { pt::wide_to_utf8(str, len, temp_header_ascii); http_headers = curl_slist_append(http_headers, temp_header_ascii.c_str()); } return *this; } Http & Http::add_headers(pt::Space * headers) { additional_space_headers_to_send = headers; return *this; } Http & Http::add_headers(pt::Space & headers) { additional_space_headers_to_send = &headers; return *this; } Http & Http::add_headers(const char * headers) { additional_string_headers_to_send = headers; return *this; } Http & Http::add_headers(const wchar_t * headers) { additional_wstring_headers_to_send = headers; return *this; } Http & Http::add_headers(const std::string & headers) { additional_string_headers_to_send = headers.c_str(); return *this; } Http & Http::add_headers(const std::wstring & headers) { additional_wstring_headers_to_send = headers.c_str(); return *this; } Http & Http::get_output_headers_to(pt::Space * out_headers, bool change_names_to_lower) { this->output_headers_space = out_headers; this->change_header_names_to_lower = change_names_to_lower; return *this; } Http & Http::get_output_content_type_to(std::wstring * out_content_type) { this->output_content_type = out_content_type; return *this; } Http & Http::add_bearer_token(const wchar_t * token) { this->bearer_token = token; return *this; } Http & Http::add_bearer_token(const std::wstring & token) { this->bearer_token = token.c_str(); return *this; } bool Http::fetch(Method method, const wchar_t * url, const std::string * in, pt::WTextStream & out, bool clear_stream) { std::string url_ascii; pt::TextStream out_stream; if( clear_stream ) { out.clear(); } pt::wide_to_utf8(url, url_ascii); bool status = fetch_internal(method, url_ascii.c_str(), in, out_stream); out << out_stream; return status; } bool Http::fetch(Method method, const std::wstring & url, const std::string * in, pt::WTextStream & out, bool clear_stream) { return fetch(method, url.c_str(), in, out, clear_stream); } bool Http::fetch(Method method, const wchar_t * url, pt::WTextStream * in, pt::WTextStream & out, bool clear_stream) { if( in ) { std::string in_ascii; in->to_str(in_ascii); return fetch(method, url, &in_ascii, out, clear_stream); } else { const std::string * in = nullptr; return fetch(method, url, in, out, clear_stream); } } bool Http::get(const wchar_t * url, std::wstring & out, bool clear_str) { const std::string * in = nullptr; pt::WTextStream out_stream; bool status = fetch(Method::method_get, url, in, out_stream, false); out_stream.to_str(out, clear_str); return status; } bool Http::get(const std::wstring & url, std::wstring & out, bool clear_str) { return get(url.c_str(), out, clear_str); } bool Http::get(const pt::WTextStream & url, std::wstring & out, bool clear_str) { std::wstring url_str; url.to_str(url_str); return get(url_str, out, clear_str); } bool Http::get(const wchar_t * url, pt::WTextStream & out, bool clear_stream) { const std::string * in = nullptr; return fetch(Method::method_get, url, in, out, clear_stream); } bool Http::get(const std::wstring & url, pt::WTextStream & out, bool clear_stream) { return get(url.c_str(), out, clear_stream); } bool Http::get(const pt::WTextStream & url, pt::WTextStream & out, bool clear_stream) { std::wstring url_str; url.to_str(url_str); return get(url_str, out, clear_stream); } bool Http::post(const wchar_t * url, const std::string & in, pt::WTextStream & out, bool clear_stream) { return fetch(Method::method_post, url, &in, out, clear_stream); } bool Http::post(const std::wstring & url, const std::string & in, pt::WTextStream & out, bool clear_stream) { return fetch(Method::method_post, url, &in, out, clear_stream); } bool Http::post(const wchar_t * url, pt::WTextStream & in, pt::WTextStream & out, bool clear_stream) { return fetch(Method::method_post, url, &in, out, clear_stream); } bool Http::put(const wchar_t * url, const std::string & in, pt::WTextStream & out, bool clear_stream) { return fetch(Method::method_put, url, &in, out, clear_stream); } bool Http::put(const std::wstring & url, const std::string & in, pt::WTextStream & out, bool clear_stream) { return fetch(Method::method_put, url, &in, out, clear_stream); } bool Http::put(const wchar_t * url, pt::WTextStream & in, pt::WTextStream & out, bool clear_stream) { return fetch(Method::method_put, url, &in, out, clear_stream); } long Http::get_status() { long status = -1; if( curl ) { curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); } return status; } void Http::clear_tmp_objects() { out_headers_stream.clear(); temp_header.clear(); temp_header_ascii.clear(); temp_header_value.clear(); temp_header_value_ascii.clear(); } void Http::reset_headers() { if( http_headers ) { curl_slist_free_all(http_headers); http_headers = nullptr; } } void Http::add_additional_space_headers() { if( additional_space_headers_to_send && additional_space_headers_to_send->is_object() ) { pt::WTextStream header; pt::Space::ObjectType::iterator i = additional_space_headers_to_send->value.value_object.begin(); while( i != additional_space_headers_to_send->value.value_object.end() ) { header.clear(); header << i->first << ": "; pt::Space * value = i->second; if( value->is_wstr() ) { header << value->value.value_wstring; } else if( value->is_str() ) { header << value->value.value_string; } else { header << i->second->to_wstr(); } add_header(header); ++i; } } } void Http::add_additional_string_headers() { if( additional_string_headers_to_send ) { add_additional_string_headers(additional_string_headers_to_send); } if( additional_wstring_headers_to_send ) { add_additional_string_headers(additional_wstring_headers_to_send); } } void Http::add_bearer_token() { if( bearer_token && bearer_token[0] != 0 ) { pt::WTextStream header; header << L"Authorization: Bearer " << bearer_token; add_header(header); } } void Http::allow_redirects(bool allow_redirects) { this->follow_location = allow_redirects; } void Http::set_ssl_version(long ssl_version) { this->forse_ssl_version = true; this->ssl_version = ssl_version; } void Http::verify_ssl(bool verify) { this->verify_ssl_cert = verify; } void Http::set_ca_file(const wchar_t * path) { ca_path = path; } void Http::set_ca_file(const std::wstring & path) { ca_path = path.c_str(); } void Http::set_client_cert(const wchar_t * client_cert, const wchar_t * client_key) { this->client_cert = client_cert; this->client_key = client_key; } void Http::set_client_cert(const std::wstring & client_cert, const std::wstring & client_key) { this->client_cert = client_cert.c_str(); this->client_key = client_key.c_str(); } void Http::initialize_curl_if_needed() { if( !curl ) { /* * curl_easy_init() is not thread safe before curl 7.84.0 version * * from https://curl.se/libcurl/c/curl_easy_init.html * If you did not already call curl_global_init, curl_easy_init does it automatically. * This may be lethal in multi-threaded cases, since curl_global_init is not thread-safe, * and it may result in resource problems because there is no corresponding cleanup. * * but we have called curl_global_init() in main.cpp (InitCurlLibrary() method) * */ curl = curl_easy_init(); } } void Http::uninitialize_curl() { if( curl ) { curl_easy_cleanup(curl); curl = nullptr; } } void Http::use_debug_mode(bool debug, pt::Space * debug_info) { this->debug_mode = debug ? 1 : 0; this->debug_info = debug_info; } // in can be pointer to const char * bool Http::fetch_internal(Method method, const char * url, const std::string * in, pt::TextStream & out) { bool status = false; initialize_curl_if_needed(); if( output_headers_space ) output_headers_space->clear(); if( output_content_type ) output_content_type->clear(); if( !curl ) { log << log1 << "Http: I can't initialize curl easy session" << logend; return status; } error_buf[0] = 0; read_function_input = in; // can be null read_function_index = 0; put_method(method); if( read_function_input ) { curl_easy_setopt(curl, CURLOPT_READFUNCTION, fetch_read_function); curl_easy_setopt(curl, CURLOPT_READDATA, this); curl_easy_setopt(curl, CURLOPT_SEEKFUNCTION, fetch_seek_function); curl_easy_setopt(curl, CURLOPT_SEEKDATA, this); curl_off_t size = read_function_input->size(); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, size); } curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fetch_write_function); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &out); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_USERAGENT, browser_name.c_str()); curl_easy_setopt(curl, CURLOPT_TIMEOUT, conn_timeout); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, conn_timeout); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buf); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, (follow_location) ? 1 : 0); curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 20); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, (verify_ssl_cert) ? 1 : 0); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, (verify_ssl_cert) ? 2 : 0); // set to 2: https://curl.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html if( debug_mode == 1 ) { curl_easy_setopt(curl, CURLOPT_VERBOSE, debug_mode); curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, debug_function); curl_easy_setopt(curl, CURLOPT_DEBUGDATA, this); } if( output_headers_space || output_content_type ) { out_headers_stream.clear(); curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, fetch_header_function); curl_easy_setopt(curl, CURLOPT_HEADERDATA, &out_headers_stream); } if( forse_ssl_version ) { curl_easy_setopt(curl, CURLOPT_SSLVERSION, ssl_version); } if( ca_path ) { pt::wide_to_utf8(ca_path, ca_path_utf8); curl_easy_setopt(curl, CURLOPT_CAINFO, ca_path_utf8.c_str()); } if( client_cert ) { pt::wide_to_utf8(client_cert, client_cert_utf8); curl_easy_setopt(curl, CURLOPT_SSLCERT, client_cert_utf8.c_str()); } if( client_key ) { pt::wide_to_utf8(client_key, client_key_utf8); curl_easy_setopt(curl, CURLOPT_SSLKEY, client_key_utf8.c_str()); } // block the Expect: 100-continue header // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect // https://httpwg.org/specs/rfc7231.html#header.expect // https://curl.se/libcurl/c/post-callback.html /* Using POST with HTTP 1.1 implies the use of a "Expect: 100-continue" header. You can disable this header with CURLOPT_HTTPHEADER as usual. NOTE: if you want chunked transfer too, you need to combine these two since you can only set one list of headers with CURLOPT_HTTPHEADER. */ //add_header(L"Expect:"); add_additional_space_headers(); add_additional_string_headers(); add_bearer_token(); if( http_headers ) { curl_easy_setopt(curl, CURLOPT_HTTPHEADER, http_headers); } CURLcode res = curl_easy_perform(curl); reset_headers(); if( res == CURLE_OK ) { status = true; parse_headers(); } else { log << log1 << "Http: fetching failed: " << error_buf << ", code: " << static_cast(res) << logend; } clear_tmp_objects(); return status; } void Http::put_method(Method & method) { // we don't put 'get' here switch(method) { case Method::method_post: curl_easy_setopt(curl, CURLOPT_POST, 1); break; case Method::method_put: curl_easy_setopt(curl, CURLOPT_PUT, 1); break; case Method::method_patch: curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH"); break; case Method::method_delete: curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); break; case Method::method_options: curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "OPTIONS"); break; default: break; } } size_t Http::fetch_read_function(char * ptr, size_t size, size_t nmemb, void * userdata) { size_t len = 0; if( userdata ) { Http * http = reinterpret_cast(userdata); if( http->read_function_index < http->read_function_input->size() ) { len = size * nmemb; if( http->read_function_index + len > http->read_function_input->size() ) { len = http->read_function_input->size() - http->read_function_index; } for(size_t i=0 ; i < len ; ++i) { ptr[i] = (*http->read_function_input)[http->read_function_index + i]; } http->read_function_index += len; } } return len; } int Http::fetch_seek_set(Http * http, curl_off_t offset) { int status = CURL_SEEKFUNC_CANTSEEK; size_t offset_size = static_cast(offset); if( offset_size >=0 && offset_size < http->read_function_input->size() ) { http->read_function_index = offset_size; status = CURL_SEEKFUNC_OK; } return status; } int Http::fetch_seek_cur(Http * http, curl_off_t offset) { int status = CURL_SEEKFUNC_CANTSEEK; if( offset >= 0 ) { size_t offset_size = static_cast(offset); if( http->read_function_index + offset_size < http->read_function_input->size() ) { http->read_function_index += offset_size; status = CURL_SEEKFUNC_OK; } } else { size_t offset_size = static_cast(-offset); if( http->read_function_index >= offset_size ) { http->read_function_index -= offset_size; status = CURL_SEEKFUNC_OK; } } return status; } int Http::fetch_seek_end(Http * http, curl_off_t offset) { int status = CURL_SEEKFUNC_CANTSEEK; if( offset < 0 ) { size_t offset_size = static_cast(-offset); if( offset_size <= http->read_function_input->size() ) { http->read_function_index = http->read_function_input->size() - offset_size; status = CURL_SEEKFUNC_OK; } } return status; } /* * * https://curl.se/libcurl/c/CURLOPT_SEEKFUNCTION.html * * These are the return codes for the seek callbacks * * #define CURL_SEEKFUNC_OK 0 * #define CURL_SEEKFUNC_FAIL 1 // fail the entire transfer * #define CURL_SEEKFUNC_CANTSEEK 2 / /tell libcurl seeking cannot be done, so * libcurl might try other means instead * */ int Http::fetch_seek_function(void * userdata, curl_off_t offset, int origin) { Http * http = reinterpret_cast(userdata); int status = CURL_SEEKFUNC_FAIL; if( origin == SEEK_SET ) { status = fetch_seek_set(http, offset); } else if( origin == SEEK_CUR ) { status = fetch_seek_cur(http, offset); } else if( origin == SEEK_END ) { status = fetch_seek_end(http, offset); } return status; } size_t Http::fetch_write_function(char * ptr, size_t size, size_t nmemb, void * userdata) { size_t len = size * nmemb; if( userdata ) { pt::TextStream * out = reinterpret_cast(userdata); if( len > 0 ) out->write(ptr, len); } return len; } size_t Http::fetch_header_function(char * ptr, size_t size, size_t nmemb, void * userdata) { size_t len = size * nmemb; if( userdata ) { pt::TextStream * out = reinterpret_cast(userdata); if( len > 0 ) out->write(ptr, len); } return len; } void Http::skip_white(pt::TextStream::iterator & i) { for( ; i != out_headers_stream.end() ; ++i ) { if( !pt::is_white(*i, false, false) ) { break; } } } void Http::parse_header_name(pt::TextStream::iterator & i, std::string & name) { name.clear(); for( ; i != out_headers_stream.end() ; ++i ) { char c = *i; if( c == '\r' ) { // just skip } else if( c == '\n' ) { break; } else if( c == ':' ) { ++i; break; } else { if( change_header_names_to_lower ) { c = pt::to_lower(c); } name += c; } } } void Http::parse_header_value(pt::TextStream::iterator & i, std::string & value) { value.clear(); for( ; i != out_headers_stream.end() ; ++i ) { char c = *i; if( c == '\r' ) { // just skip } else if( c == '\n' ) { ++i; break; } else { value += c; } } } void Http::parse_headers() { if( output_headers_space || output_content_type ) { pt::TextStream::iterator i = out_headers_stream.begin(); while( i != out_headers_stream.end() ) { parse_header_name(i, temp_header_ascii); skip_white(i); parse_header_value(i, temp_header_value_ascii); pt::utf8_to_wide(temp_header_ascii, temp_header); pt::utf8_to_wide(temp_header_value_ascii, temp_header_value); // both temp_header and temp_header_value will be empty // after the first header 'HTTP/1.1 100 Continue' (if exists) // (there is an empty line in such a case) if( !temp_header.empty() || !temp_header_value.empty() ) { if( output_headers_space ) output_headers_space->add(temp_header, temp_header_value); if( output_content_type && pt::is_equal_nc(temp_header, Header::content_type) ) *output_content_type = temp_header_value; } } temp_header.clear(); temp_header_ascii.clear(); temp_header_value.clear(); temp_header_value_ascii.clear(); } } void Http::debug_function_append(pt::Space & debug, const char * prefix, const char * start, size_t part_size) { if( part_size > 0 ) { debug.add(prefix); std::string & last_table_str_item = debug.value.value_table.back()->value.value_string; last_table_str_item.append(start, part_size); } } void Http::debug_function_append(Http * http, const char * prefix, char * data, size_t size) { pt::Space & debug = *http->debug_info; const char * start = data; size_t part_size = 0; size_t i = 0; while( i < size ) { if( data[i]== '\r' && i+1 < size && data[i+1]=='\n' ) { debug_function_append(debug, prefix, start, part_size); i += 2; start = data + i; part_size = 0; } else if( data[i]== '\n' ) { debug_function_append(debug, prefix, start, part_size); i += 1; start = data + i; part_size = 0; } else { part_size += 1; i += 1; } } debug_function_append(debug, prefix, start, part_size); } int Http::debug_function(CURL * handle, curl_infotype type, char * data, size_t size, void * userdata) { Http * http = reinterpret_cast(userdata); if( http->debug_info ) { switch(type) { case CURLINFO_TEXT: debug_function_append(http, "", data, size); break; case CURLINFO_HEADER_IN: debug_function_append(http, "Header received: ", data, size); break; case CURLINFO_HEADER_OUT: debug_function_append(http, "Header sent: ", data, size); break; default: break; } } return 0; } const wchar_t * Http::method_to_str(Http::Method method) { switch(method) { case method_get: return L"GET"; case method_post: return L"POST"; case method_put: return L"PUT"; case method_patch: return L"PATCH"; case method_delete: return L"DELETE"; case method_options: return L"OPTIONS"; default: return L""; } } }