#include "HttpMessage.h" #include #include "htime.h" #include "hlog.h" #include "hurl.h" #include "base64.h" using namespace hv; http_headers DefaultHeaders; http_body NoBody; HttpCookie NoCookie; char HttpMessage::s_date[32] = {0}; HttpCookie::HttpCookie() { init(); } void HttpCookie::init() { max_age = 0; secure = false; httponly = false; samesite = Default; priority = NotSet; } void HttpCookie::reset() { init(); name.clear(); value.clear(); domain.clear(); path.clear(); expires.clear(); kv.clear(); } bool HttpCookie::parse(const std::string& str) { std::stringstream ss; ss << str; std::string line; std::string::size_type pos; std::string key; std::string val; reset(); while (std::getline(ss, line, ';')) { pos = line.find_first_of('='); if (pos != std::string::npos) { key = trim(line.substr(0, pos)); val = trim(line.substr(pos+1)); const char* pkey = key.c_str(); if (stricmp(pkey, "Domain") == 0) { domain = val; } else if (stricmp(pkey, "Path") == 0) { path = val; } else if (stricmp(pkey, "Expires") == 0) { expires = val; } else if (stricmp(pkey, "Max-Age") == 0) { max_age = atoi(val.c_str()); } else if (stricmp(pkey, "SameSite") == 0) { samesite = stricmp(val.c_str(), "Strict") == 0 ? HttpCookie::SameSite::Strict : stricmp(val.c_str(), "Lax") == 0 ? HttpCookie::SameSite::Lax : stricmp(val.c_str(), "None") == 0 ? HttpCookie::SameSite::None : HttpCookie::SameSite::Default; } else if (stricmp(pkey, "Priority") == 0) { priority = stricmp(val.c_str(), "Low") == 0 ? HttpCookie::Priority::Low : stricmp(val.c_str(), "Medium") == 0 ? HttpCookie::Priority::Medium : stricmp(val.c_str(), "High") == 0 ? HttpCookie::Priority::High : HttpCookie::Priority::NotSet ; } else { if (name.empty()) { name = key; value = val; } kv[key] = val; } } else { key = trim(line); const char* pkey = key.c_str(); if (stricmp(pkey, "Secure") == 0) { secure = true; } else if (stricmp(pkey, "HttpOnly") == 0) { httponly = true; } else { hlogw("Unrecognized key '%s'", key.c_str()); } } } return !name.empty(); } std::string HttpCookie::dump() const { assert(!name.empty() || !kv.empty()); std::string res; if (!name.empty()) { res = name; res += "="; res += value; } for (auto& pair : kv) { if (pair.first == name) continue; if (!res.empty()) res += "; "; res += pair.first; res += "="; res += pair.second; } if (!domain.empty()) { res += "; Domain="; res += domain; } if (!path.empty()) { res += "; Path="; res += path; } if (max_age > 0) { res += "; Max-Age="; res += hv::to_string(max_age); } else if (!expires.empty()) { res += "; Expires="; res += expires; } if (samesite != HttpCookie::SameSite::Default) { res += "; SameSite="; res += samesite == HttpCookie::SameSite::Strict ? "Strict" : samesite == HttpCookie::SameSite::Lax ? "Lax" : "None" ; } if (priority != HttpCookie::Priority::NotSet) { res += "; Priority="; res += priority == HttpCookie::Priority::Low ? "Low" : priority == HttpCookie::Priority::Medium ? "Medium" : "High" ; } if (secure) { res += "; Secure"; } if (httponly) { res += "; HttpOnly"; } return res; } HttpMessage::HttpMessage() { type = HTTP_BOTH; Init(); } HttpMessage::~HttpMessage() { } void HttpMessage::Init() { http_major = 1; http_minor = 1; content = NULL; content_length = 0; content_type = CONTENT_TYPE_NONE; } void HttpMessage::Reset() { Init(); headers.clear(); cookies.clear(); body.clear(); #ifndef WITHOUT_HTTP_CONTENT json.clear(); form.clear(); kv.clear(); #endif } #ifndef WITHOUT_HTTP_CONTENT // NOTE: json ignore number/string, 123/"123" std::string HttpMessage::GetString(const char* key, const std::string& defvalue) { switch (ContentType()) { case APPLICATION_JSON: { if (json.empty()) { ParseBody(); } if (!json.is_object()) { return defvalue; } const auto& value = json[key]; if (value.is_string()) { return value; } else if (value.is_number()) { return hv::to_string(value); } else if (value.is_boolean()) { bool b = value; return b ? "true" : "false"; } else { return defvalue; } } break; case MULTIPART_FORM_DATA: { if (form.empty()) { ParseBody(); } auto iter = form.find(key); if (iter != form.end()) { return iter->second.content; } } break; case APPLICATION_URLENCODED: { if (kv.empty()) { ParseBody(); } auto iter = kv.find(key); if (iter != kv.end()) { return iter->second; } } break; default: break; } return defvalue; } template<> HV_EXPORT int64_t HttpMessage::Get(const char* key, int64_t defvalue) { if (ContentType() == APPLICATION_JSON) { if (json.empty()) { ParseBody(); } if (!json.is_object()) { return defvalue; } const auto& value = json[key]; if (value.is_number()) { return value; } else if (value.is_string()) { std::string str = value; return atoll(str.c_str()); } else if (value.is_boolean()) { bool b = value; return b ? 1 : 0; } else { return defvalue; } } else { std::string str = GetString(key); return str.empty() ? defvalue : atoll(str.c_str()); } } template<> HV_EXPORT int HttpMessage::Get(const char* key, int defvalue) { return (int)Get(key, defvalue); } template<> HV_EXPORT double HttpMessage::Get(const char* key, double defvalue) { if (ContentType() == APPLICATION_JSON) { if (json.empty()) { ParseBody(); } if (!json.is_object()) { return defvalue; } const auto& value = json[key]; if (value.is_number()) { return value; } else if (value.is_string()) { std::string str = value; return atof(str.c_str()); } else { return defvalue; } } else { std::string str = GetString(key); return str.empty() ? defvalue : atof(str.c_str()); } } template<> HV_EXPORT float HttpMessage::Get(const char* key, float defvalue) { return (float)Get(key, defvalue); } template<> HV_EXPORT bool HttpMessage::Get(const char* key, bool defvalue) { if (ContentType() == APPLICATION_JSON) { if (json.empty()) { ParseBody(); } if (!json.is_object()) { return defvalue; } const auto& value = json[key]; if (value.is_boolean()) { return value; } else if (value.is_string()) { std::string str = value; return hv_getboolean(str.c_str()); } else if (value.is_number()) { return value != 0; } else { return defvalue; } } else { std::string str = GetString(key); return str.empty() ? defvalue : hv_getboolean(str.c_str()); } } bool HttpMessage::GetBool(const char* key, bool defvalue) { return Get(key, defvalue); } int64_t HttpMessage::GetInt(const char* key, int64_t defvalue) { return Get(key, defvalue); } double HttpMessage::GetFloat(const char* key, double defvalue) { return Get(key, defvalue); } #endif void HttpMessage::FillContentType() { auto iter = headers.find("Content-Type"); if (iter != headers.end()) { content_type = http_content_type_enum(iter->second.c_str()); goto append; } #ifndef WITHOUT_HTTP_CONTENT if (content_type == CONTENT_TYPE_NONE) { if (json.size() != 0) { content_type = APPLICATION_JSON; } else if (form.size() != 0) { content_type = MULTIPART_FORM_DATA; } else if (kv.size() != 0) { content_type = X_WWW_FORM_URLENCODED; } else if (body.size() != 0) { content_type = TEXT_PLAIN; } } #endif if (content_type != CONTENT_TYPE_NONE) { headers["Content-Type"] = http_content_type_str(content_type); } append: #ifndef WITHOUT_HTTP_CONTENT if (content_type == MULTIPART_FORM_DATA) { auto iter = headers.find("Content-Type"); if (iter != headers.end()) { const char* boundary = strstr(iter->second.c_str(), "boundary="); if (boundary == NULL) { boundary = DEFAULT_MULTIPART_BOUNDARY; iter->second += "; boundary="; iter->second += boundary; } } } #endif return; } void HttpMessage::FillContentLength() { auto iter = headers.find("Content-Length"); if (iter != headers.end()) { content_length = atoll(iter->second.c_str()); } if (content_length == 0) { DumpBody(); content_length = body.size(); } if (iter == headers.end() && !IsChunked() && content_type != TEXT_EVENT_STREAM) { if (content_length != 0 || type == HTTP_RESPONSE) { headers["Content-Length"] = hv::to_string(content_length); } } } bool HttpMessage::IsChunked() { auto iter = headers.find("Transfer-Encoding"); return iter != headers.end() && stricmp(iter->second.c_str(), "chunked") == 0; } bool HttpMessage::IsKeepAlive() { bool keepalive = true; auto iter = headers.find("connection"); if (iter != headers.end()) { const char* keepalive_value = iter->second.c_str(); if (stricmp(keepalive_value, "keep-alive") == 0) { keepalive = true; } else if (stricmp(keepalive_value, "close") == 0) { keepalive = false; } else if (stricmp(keepalive_value, "upgrade") == 0) { keepalive = true; } } else if (http_major == 1 && http_minor == 0) { keepalive = false; } return keepalive; } bool HttpMessage::IsUpgrade() { auto iter = headers.find("upgrade"); return iter != headers.end(); } // headers void HttpMessage::SetHeader(const char* key, const std::string& value) { headers[key] = value; } std::string HttpMessage::GetHeader(const char* key, const std::string& defvalue) { auto iter = headers.find(key); return iter == headers.end() ? defvalue : iter->second; } // cookies void HttpMessage::AddCookie(const HttpCookie& cookie) { cookies.push_back(cookie); } const HttpCookie& HttpMessage::GetCookie(const std::string& name) { for (auto iter = cookies.begin(); iter != cookies.end(); ++iter) { if (iter->name == name) { return *iter; } auto kv_iter = iter->kv.find(name); if (kv_iter != iter->kv.end()) { iter->name = name; iter->value = kv_iter->second; return *iter; } } return NoCookie; } // body void HttpMessage::SetBody(const std::string& body) { this->body = body; } const std::string& HttpMessage::Body() { return this->body; } void HttpMessage::DumpHeaders(std::string& str) { FillContentType(); FillContentLength(); // headers for (auto& header: headers) { // http2 :method :path :scheme :authority :status if (*str.c_str() != ':') { // %s: %s\r\n str += header.first; str += ": "; // fix CVE-2023-26148 // if the value has \r\n, translate to \\r\\n if (header.second.find("\r") != std::string::npos || header.second.find("\n") != std::string::npos) { std::string newStr = ""; for (size_t i = 0; i < header.second.size(); ++i) { if (header.second[i] == '\r') { newStr += "\\r"; } else if (header.second[i] == '\n') { newStr += "\\n"; } else { newStr += header.second[i]; } } str += newStr; } else { str += header.second; } str += "\r\n"; } } // cookies const char* cookie_field = "Cookie"; if (type == HTTP_RESPONSE) { cookie_field = "Set-Cookie"; } for (auto& cookie : cookies) { str += cookie_field; str += ": "; str += cookie.dump(); str += "\r\n"; } } void HttpMessage::DumpBody() { if (body.size() != 0) { return; } FillContentType(); #ifndef WITHOUT_HTTP_CONTENT switch(content_type) { case APPLICATION_JSON: body = dump_json(json, 2); break; case MULTIPART_FORM_DATA: { auto iter = headers.find("Content-Type"); if (iter == headers.end()) { return; } const char* boundary = strstr(iter->second.c_str(), "boundary="); if (boundary == NULL) { return; } boundary += strlen("boundary="); body = dump_multipart(form, boundary); } break; case X_WWW_FORM_URLENCODED: body = dump_query_params(kv); break; default: // nothing to do break; } #endif } void HttpMessage::DumpBody(std::string& str) { DumpBody(); const char* content = (const char*)Content(); size_t content_length = ContentLength(); if (content && content_length) { str.append(content, content_length); } } int HttpMessage::ParseBody() { if (body.size() == 0) { return -1; } FillContentType(); #ifndef WITHOUT_HTTP_CONTENT switch(content_type) { case APPLICATION_JSON: { std::string errmsg; int ret = parse_json(body.c_str(), json, errmsg); if (ret != 0 && errmsg.size() != 0) { hloge("%s", errmsg.c_str()); } return ret; } case MULTIPART_FORM_DATA: { auto iter = headers.find("Content-Type"); if (iter == headers.end()) { return -1; } const char* boundary = strstr(iter->second.c_str(), "boundary="); if (boundary == NULL) { return -1; } boundary += strlen("boundary="); std::string strBoundary(boundary); strBoundary = trim_pairs(strBoundary, "\"\"\'\'"); return parse_multipart(body, form, strBoundary.c_str()); } case X_WWW_FORM_URLENCODED: return parse_query_params(body.c_str(), kv); default: // nothing to do return 0; } #endif return 0; } std::string HttpMessage::Dump(bool is_dump_headers, bool is_dump_body) { std::string str; if (is_dump_headers) { DumpHeaders(str); } str += "\r\n"; if (is_dump_body) { DumpBody(str); } return str; } HttpRequest::HttpRequest() : HttpMessage() { type = HTTP_REQUEST; Init(); } void HttpRequest::Init() { headers["User-Agent"] = DEFAULT_HTTP_USER_AGENT; headers["Accept"] = "*/*"; method = HTTP_GET; scheme = "http"; host = "127.0.0.1"; port = DEFAULT_HTTP_PORT; path = "/"; timeout = DEFAULT_HTTP_TIMEOUT; connect_timeout = DEFAULT_HTTP_CONNECT_TIMEOUT; retry_count = DEFAULT_HTTP_FAIL_RETRY_COUNT; retry_delay = DEFAULT_HTTP_FAIL_RETRY_DELAY; redirect = 1; proxy = 0; cancel = 0; } void HttpRequest::Reset() { HttpMessage::Reset(); Init(); url.clear(); query_params.clear(); } void HttpRequest::DumpUrl() { std::string str; if (url.size() != 0 && *url.c_str() != '/' && strstr(url.c_str(), "://") != NULL) { // have been complete url goto query; } // scheme:// str = scheme; str += "://"; // host:port if (url.size() != 0 && *url.c_str() != '/') { // url begin with host str += url; } else { if (port == 0 || port == DEFAULT_HTTP_PORT || port == DEFAULT_HTTPS_PORT) { str += Host(); } else { str += hv::asprintf("%s:%d", host.c_str(), port); } } // /path if (url.size() != 0 && *url.c_str() == '/') { // url begin with path str += url; } else if (path.size() > 1 && *path.c_str() == '/') { str += path; } else if (url.size() == 0) { str += '/'; } url = str; query: // ?query if (strchr(url.c_str(), '?') == NULL && query_params.size() != 0) { url += '?'; url += dump_query_params(query_params); } } void HttpRequest::ParseUrl() { DumpUrl(); hurl_t parser; hv_parse_url(&parser, url.c_str()); // scheme std::string scheme_ = url.substr(parser.fields[HV_URL_SCHEME].off, parser.fields[HV_URL_SCHEME].len); // host std::string host_(host); if (parser.fields[HV_URL_HOST].len > 0) { host_ = url.substr(parser.fields[HV_URL_HOST].off, parser.fields[HV_URL_HOST].len); } // port int port_ = parser.port ? parser.port : strcmp(scheme_.c_str(), "https") ? DEFAULT_HTTP_PORT : DEFAULT_HTTPS_PORT; if (!proxy) { scheme = scheme_; host = host_; port = port_; } FillHost(host_.c_str(), port_); // path if (parser.fields[HV_URL_PATH].len > 0) { path = url.substr(parser.fields[HV_URL_PATH].off); } // query if (parser.fields[HV_URL_QUERY].len > 0) { parse_query_params(url.c_str()+parser.fields[HV_URL_QUERY].off, query_params); } } std::string HttpRequest::Path() { const char* s = path.c_str(); const char* e = s; while (*e && *e != '?' && *e != '#') ++e; return HUrl::unescape(std::string(s, e)); } void HttpRequest::FillHost(const char* host, int port) { if (headers.find("Host") == headers.end()) { if (port == 0 || port == DEFAULT_HTTP_PORT || port == DEFAULT_HTTPS_PORT) { headers["Host"] = host; } else { headers["Host"] = asprintf("%s:%d", host, port); } } } void HttpRequest::SetHost(const char* host, int port) { this->host = host; this->port = port; FillHost(host, port); } void HttpRequest::SetProxy(const char* host, int port) { this->scheme = "http"; this->host = host; this->port = port; proxy = 1; } void HttpRequest::SetAuth(const std::string& auth) { SetHeader("Authorization", auth); } void HttpRequest::SetBasicAuth(const std::string& username, const std::string& password) { std::string strAuth = hv::asprintf("%s:%s", username.c_str(), password.c_str()); std::string base64Auth = hv::Base64Encode((const unsigned char*)strAuth.c_str(), strAuth.size()); SetAuth(std::string("Basic ") + base64Auth); } void HttpRequest::SetBearerTokenAuth(const std::string& token) { SetAuth(std::string("Bearer ") + token); } std::string HttpRequest::Dump(bool is_dump_headers, bool is_dump_body) { ParseUrl(); std::string str; str.reserve(MAX(512, path.size() + 128)); // GET / HTTP/1.1\r\n str = asprintf("%s %s HTTP/%d.%d\r\n", http_method_str(method), proxy ? url.c_str() : path.c_str(), (int)http_major, (int)http_minor); if (is_dump_headers) { DumpHeaders(str); } str += "\r\n"; if (is_dump_body) { DumpBody(str); } return str; } void HttpRequest::SetRange(long from, long to) { SetHeader("Range", hv::asprintf("bytes=%ld-%ld", from, to)); } bool HttpRequest::GetRange(long& from, long& to) { auto iter = headers.find("Range"); if (iter != headers.end()) { sscanf(iter->second.c_str(), "bytes=%ld-%ld", &from, &to); return true; } from = to = 0; return false; } HttpResponse::HttpResponse() : HttpMessage() { type = HTTP_RESPONSE; Init(); } void HttpResponse::Init() { status_code = HTTP_STATUS_OK; } void HttpResponse::Reset() { HttpMessage::Reset(); Init(); } std::string HttpResponse::Dump(bool is_dump_headers, bool is_dump_body) { char c_str[256] = {0}; std::string str; str.reserve(512); // HTTP/1.1 200 OK\r\n snprintf(c_str, sizeof(c_str), "HTTP/%d.%d %d %s\r\n", (int)http_major, (int)http_minor, (int)status_code, http_status_str(status_code)); str = c_str; if (is_dump_headers) { if (*s_date) { headers["Date"] = s_date; } else { headers["Date"] = gmtime_fmt(time(NULL), c_str); } DumpHeaders(str); } str += "\r\n"; if (is_dump_body) { DumpBody(str); } return str; } void HttpResponse::SetRange(long from, long to, long total) { SetHeader("Content-Range", hv::asprintf("bytes %ld-%ld/%ld", from, to, total)); } bool HttpResponse::GetRange(long& from, long& to, long& total) { auto iter = headers.find("Content-Range"); if (iter != headers.end()) { sscanf(iter->second.c_str(), "bytes %ld-%ld/%ld", &from, &to, &total); return true; } from = to = total = 0; return false; }