From 6dc98a43e20b83df2e33bdab64829d6977ccedeb Mon Sep 17 00:00:00 2001 From: Alexander Polyakov Date: Fri, 13 Dec 2024 12:33:17 +0300 Subject: [PATCH] [k2] implement setcookie & setrawcookie (#1186) --- builtin-functions/kphp-light/server.txt | 4 ++ runtime-light/server/http/http-server-state.h | 13 ++++ runtime-light/server/http/init-functions.cpp | 67 +++++++++---------- .../stdlib/server/http-functions.cpp | 31 +++++++++ runtime-light/stdlib/server/http-functions.h | 9 +++ runtime-light/tl/tl-functions.h | 2 +- runtime-light/tl/tl-types.h | 8 ++- 7 files changed, 94 insertions(+), 40 deletions(-) diff --git a/builtin-functions/kphp-light/server.txt b/builtin-functions/kphp-light/server.txt index adc403562f..91fd29384f 100644 --- a/builtin-functions/kphp-light/server.txt +++ b/builtin-functions/kphp-light/server.txt @@ -36,3 +36,7 @@ function parse_url ($str ::: string, $component ::: int = -1): mixed; function header ($str ::: string, $replace ::: bool = true, $http_response_code ::: int = 0): void; function numa_get_bound_node(): int; + +function setcookie ($name ::: string, $value ::: string, $expire_or_options ::: int = 0, $path ::: string = '', $domain ::: string = '', $secure ::: bool = false, $http_only ::: bool = false): void; + +function setrawcookie ($name ::: string, $value ::: string, $expire_or_options ::: int = 0, $path ::: string = '', $domain ::: string = '', $secure ::: bool = false, $http_only ::: bool = false): void; diff --git a/runtime-light/server/http/http-server-state.h b/runtime-light/server/http/http-server-state.h index 5ff16e0671..17fe25a895 100644 --- a/runtime-light/server/http/http-server-state.h +++ b/runtime-light/server/http/http-server-state.h @@ -16,6 +16,19 @@ enum class HttpMethod : uint8_t { GET, POST, HEAD, OTHER }; +namespace HttpHeader { + +inline constexpr std::string_view HOST = "host"; +inline constexpr std::string_view COOKIE = "cookie"; +inline constexpr std::string_view SET_COOKIE = "set-cookie"; +inline constexpr std::string_view CONNECTION = "connection"; +inline constexpr std::string_view CONTENT_TYPE = "content-type"; +inline constexpr std::string_view CONTENT_LENGTH = "content-length"; +inline constexpr std::string_view AUTHORIZATION = "authorization"; +inline constexpr std::string_view ACCEPT_ENCODING = "accept-encoding"; + +} // namespace HttpHeader + enum class HttpConnectionKind : uint8_t { KEEP_ALIVE, CLOSE }; enum HttpStatus : uint16_t { diff --git a/runtime-light/server/http/init-functions.cpp b/runtime-light/server/http/init-functions.cpp index f0209da230..bbd264642d 100644 --- a/runtime-light/server/http/init-functions.cpp +++ b/runtime-light/server/http/init-functions.cpp @@ -44,14 +44,7 @@ constexpr std::string_view HTTP_X_REAL_REQUEST = "HTTP_X_REAL_REQUEST"; constexpr std::string_view SCHEME_SUFFIX = "://"; constexpr std::string_view GATEWAY_INTERFACE_VALUE = "CGI/1.1"; -constexpr std::string_view HEADER_HOST = "host"; -constexpr std::string_view HEADER_COOKIE = "cookie"; -constexpr std::string_view HEADER_CONNECTION = "connection"; -constexpr std::string_view HEADER_CONTENT_TYPE = "content-type"; -constexpr std::string_view HEADER_CONTENT_LENGTH = "content-length"; -constexpr std::string_view HEADER_AUTHORIZATION = "authorization"; -constexpr std::string_view HEADER_ACCEPT_ENCODING = "accept-encoding"; -constexpr std::string_view AUTHORIZATION_BASIC = "Basic"; +constexpr std::string_view AUTHORIZATION_BASIC = "basic"; constexpr std::string_view CONNECTION_CLOSE = "close"; constexpr std::string_view CONNECTION_KEEP_ALIVE = "keep-alive"; constexpr std::string_view ENCODING_GZIP = "gzip"; @@ -145,38 +138,38 @@ std::string_view process_headers(tl::K2InvokeHttp &invoke_http, PhpScriptBuiltIn std::string_view content_type{CONTENT_TYPE_APP_FORM_URLENCODED}; // platform provides headers that are already in lowercase - for (auto &[header_name, header] : invoke_http.headers) { - const std::string_view header_name_view{header_name.c_str(), header_name.size()}; - const std::string_view header_view{header.value.c_str(), header.value.size()}; + for (auto &[_, h_name, h_value] : invoke_http.headers) { + const std::string_view h_name_view{h_name.c_str(), h_name.size()}; + const std::string_view h_value_view{h_value.c_str(), h_value.size()}; using namespace PhpServerSuperGlobalIndices; - if (header_name_view == HEADER_ACCEPT_ENCODING) { - if (absl::StrContains(header_view, ENCODING_GZIP)) { + if (h_name_view == HttpHeader::ACCEPT_ENCODING) { + if (absl::StrContains(h_value_view, ENCODING_GZIP)) { http_server_instance_st.encoding |= HttpServerInstanceState::ENCODING_GZIP; } - if (absl::StrContains(header_view, ENCODING_DEFLATE)) { + if (absl::StrContains(h_value_view, ENCODING_DEFLATE)) { http_server_instance_st.encoding |= HttpServerInstanceState::ENCODING_DEFLATE; } - } else if (header_name_view == HEADER_CONNECTION) { - if (header_view == CONNECTION_KEEP_ALIVE) [[likely]] { + } else if (h_name_view == HttpHeader::CONNECTION) { + if (h_value_view == CONNECTION_KEEP_ALIVE) [[likely]] { http_server_instance_st.connection_kind = HttpConnectionKind::KEEP_ALIVE; - } else if (header_view == CONNECTION_CLOSE) [[likely]] { + } else if (h_value_view == CONNECTION_CLOSE) [[likely]] { http_server_instance_st.connection_kind = HttpConnectionKind::CLOSE; } else { - php_error("unexpected connection header: %s", header_view.data()); + php_error("unexpected connection header: %s", h_value_view.data()); } - } else if (header_name_view == HEADER_COOKIE) { - process_cookie_header(header.value, superglobals); - } else if (header_name_view == HEADER_HOST) { - server.set_value(string{SERVER_NAME.data(), SERVER_NAME.size()}, header.value); - } else if (header_name_view == HEADER_AUTHORIZATION) { - process_authorization_header(header.value, superglobals); - } else if (header_name_view == HEADER_CONTENT_TYPE) { - content_type = header_view; + } else if (h_name_view == HttpHeader::COOKIE) { + process_cookie_header(h_value, superglobals); + } else if (h_name_view == HttpHeader::HOST) { + server.set_value(string{SERVER_NAME.data(), SERVER_NAME.size()}, h_value); + } else if (h_name_view == HttpHeader::AUTHORIZATION) { + process_authorization_header(h_value, superglobals); + } else if (h_name_view == HttpHeader::CONTENT_TYPE) { + content_type = h_value_view; continue; - } else if (header_name_view == HEADER_CONTENT_LENGTH) { + } else if (h_name_view == HttpHeader::CONTENT_LENGTH) { int32_t content_length{}; - const auto [_, ec]{std::from_chars(header_view.data(), header_view.data() + header_view.size(), content_length)}; + const auto [_, ec]{std::from_chars(h_value_view.data(), h_value_view.data() + h_value_view.size(), content_length)}; if (ec != std::errc{} || content_length != invoke_http.body.size()) [[unlikely]] { php_error("content-length expected to be %d, but it's %u", content_length, invoke_http.body.size()); } @@ -185,14 +178,14 @@ std::string_view process_headers(tl::K2InvokeHttp &invoke_http, PhpScriptBuiltIn // add header entries string key{}; - key.reserve_at_least(HTTP_HEADER_PREFIX.size() + header_name.size()); + key.reserve_at_least(HTTP_HEADER_PREFIX.size() + h_name.size()); key.append(HTTP_HEADER_PREFIX.data()); - key.append(header_name_view.data(), header_name_view.size()); + key.append(h_name_view.data(), h_name_view.size()); // to uppercase inplace for (int64_t i = HTTP_HEADER_PREFIX.size(); i < key.size(); ++i) { key[i] = key[i] != '-' ? std::toupper(key[i]) : '_'; } - server.set_value(key, std::move(header.value)); + server.set_value(key, std::move(h_value)); } return content_type; @@ -300,11 +293,11 @@ void init_http_server(tl::K2InvokeHttp &&invoke_http) noexcept { // add content-type header auto &static_SB{RuntimeContext::get().static_SB}; - static_SB.clean() << HEADER_CONTENT_TYPE.data() << ": " << CONTENT_TYPE_TEXT_WIN1251.data(); + static_SB.clean() << HttpHeader::CONTENT_TYPE.data() << ": " << CONTENT_TYPE_TEXT_WIN1251.data(); header({static_SB.c_str(), static_SB.size()}, true, HttpStatus::NO_STATUS); // add connection kind header const auto connection_kind{http_server_instance_st.connection_kind == HttpConnectionKind::KEEP_ALIVE ? CONNECTION_KEEP_ALIVE : CONNECTION_CLOSE}; - static_SB.clean() << HEADER_CONNECTION.data() << ": " << connection_kind.data(); + static_SB.clean() << HttpHeader::CONNECTION.data() << ": " << connection_kind.data(); } task_t finalize_http_server(const string_buffer &output) noexcept { @@ -319,13 +312,13 @@ task_t finalize_http_server(const string_buffer &output) noexcept { .headers = {}, .body = std::move(body)}; // fill headers - http_response.headers.data.data.reserve(http_server_instance_st.headers().size()); - std::transform(http_server_instance_st.headers().cbegin(), http_server_instance_st.headers().cend(), std::back_inserter(http_response.headers.data.data), + http_response.headers.data.reserve(http_server_instance_st.headers().size()); + std::transform(http_server_instance_st.headers().cbegin(), http_server_instance_st.headers().cend(), std::back_inserter(http_response.headers.data), [](const auto &header_entry) noexcept { const auto &[name, value]{header_entry}; string header_name{name.data(), static_cast(name.size())}; - tl::httpHeaderValue header_value{.value = {value.data(), static_cast(value.size())}}; - return tl::dictionaryField{.key = std::move(header_name), .value = std::move(header_value)}; + string header_value{value.data(), static_cast(value.size())}; + return tl::httpHeaderEntry{.is_sensitive = {}, .name = std::move(header_name), .value = std::move(header_value)}; }); tl::TLBuffer tlb{}; diff --git a/runtime-light/stdlib/server/http-functions.cpp b/runtime-light/stdlib/server/http-functions.cpp index b5936bff82..b0b52f15ca 100644 --- a/runtime-light/stdlib/server/http-functions.cpp +++ b/runtime-light/stdlib/server/http-functions.cpp @@ -17,11 +17,14 @@ #include "absl/strings/ascii.h" #include "absl/strings/str_split.h" +#include "runtime-common/core/runtime-core.h" #include "runtime-common/core/utils/kphp-assert-core.h" #include "runtime-light/server/http/http-server-state.h" +#include "runtime-light/stdlib/time/time-functions.h" namespace { +constexpr std::string_view HTTP_DATE = R"(D, d M Y H:i:s \G\M\T)"; constexpr std::string_view HTTP_STATUS_PREFIX = "http/"; constexpr std::string_view HTTP_LOCATION_HEADER_PREFIX = "location:"; @@ -135,3 +138,31 @@ void header(std::string_view header_view, bool replace, int64_t response_code) n http_server_instance_st.status_code = response_code; } } + +void f$setrawcookie(const string &name, const string &value, int64_t expire_or_options, const string &path, const string &domain, bool secure, + bool http_only) noexcept { + auto &static_SB_spare{RuntimeContext::get().static_SB_spare}; + + static_SB_spare.clean() << HttpHeader::SET_COOKIE.data() << ": " << name << '='; + if (value.empty()) { + static_SB_spare << "DELETED; expires=Thu, 01 Jan 1970 00:00:01 GMT"; + } else { + static_SB_spare << value; + if (expire_or_options != 0) { + static_SB_spare << "; expires=" << f$gmdate({HTTP_DATE.data(), HTTP_DATE.size()}, expire_or_options); + } + } + if (!path.empty()) { + static_SB_spare << "; path=" << path; + } + if (!domain.empty()) { + static_SB_spare << "; domain=" << domain; + } + if (secure) { + static_SB_spare << "; secure"; + } + if (http_only) { + static_SB_spare << "; HttpOnly"; + } + header({static_SB_spare.c_str(), static_SB_spare.size()}, false, HttpStatus::NO_STATUS); +} diff --git a/runtime-light/stdlib/server/http-functions.h b/runtime-light/stdlib/server/http-functions.h index 8258268ee2..f3dfb3256a 100644 --- a/runtime-light/stdlib/server/http-functions.h +++ b/runtime-light/stdlib/server/http-functions.h @@ -8,6 +8,7 @@ #include #include "runtime-common/core/runtime-core.h" +#include "runtime-common/stdlib/server/url-functions.h" #include "runtime-light/server/http/http-server-state.h" void header(std::string_view header, bool replace, int64_t response_code) noexcept; @@ -15,3 +16,11 @@ void header(std::string_view header, bool replace, int64_t response_code) noexce inline void f$header(const string &str, bool replace = true, int64_t response_code = HttpStatus::NO_STATUS) noexcept { header({str.c_str(), str.size()}, replace, response_code); } + +void f$setrawcookie(const string &name, const string &value, int64_t expire_or_options = 0, const string &path = {}, const string &domain = {}, + bool secure = false, bool http_only = false) noexcept; + +inline void f$setcookie(const string &name, const string &value = {}, int64_t expire_or_options = 0, const string &path = {}, const string &domain = {}, + bool secure = false, bool http_only = false) noexcept { + f$setrawcookie(name, f$urlencode(value), expire_or_options, path, domain, secure, http_only); +} diff --git a/runtime-light/tl/tl-functions.h b/runtime-light/tl/tl-functions.h index a6ffe9c3b0..2dcba508a7 100644 --- a/runtime-light/tl/tl-functions.h +++ b/runtime-light/tl/tl-functions.h @@ -121,7 +121,7 @@ class K2InvokeHttp final { HttpVersion version{}; string method; httpUri uri{}; - dictionary headers{}; + vector headers{}; string body; bool fetch(TLBuffer &tlb) noexcept; diff --git a/runtime-light/tl/tl-types.h b/runtime-light/tl/tl-types.h index 611f86f3eb..e2731446ee 100644 --- a/runtime-light/tl/tl-types.h +++ b/runtime-light/tl/tl-types.h @@ -454,19 +454,23 @@ class httpUri final { } }; -struct httpHeaderValue final { +struct httpHeaderEntry final { Bool is_sensitive{}; + string name; string value; bool fetch(TLBuffer &tlb) noexcept { const auto ok{is_sensitive.fetch(tlb)}; + const auto name_view{tlb.fetch_string()}; const auto value_view{tlb.fetch_string()}; + name = {name_view.data(), static_cast(name_view.size())}; value = {value_view.data(), static_cast(value_view.size())}; return ok; } void store(TLBuffer &tlb) const noexcept { is_sensitive.store(tlb); + tlb.store_string({name.c_str(), static_cast(name.size())}); tlb.store_string({value.c_str(), static_cast(value.size())}); } }; @@ -498,7 +502,7 @@ struct httpConnection final { struct httpResponse final { HttpVersion version{}; int32_t status_code{}; - dictionary headers{}; + vector headers{}; string body; void store(TLBuffer &tlb) const noexcept {