diff options
Diffstat (limited to 'src/http_protocol.cc')
| -rw-r--r-- | src/http_protocol.cc | 916 |
1 files changed, 916 insertions, 0 deletions
diff --git a/src/http_protocol.cc b/src/http_protocol.cc new file mode 100644 index 0000000..c0e40ad --- /dev/null +++ b/src/http_protocol.cc @@ -0,0 +1,916 @@ +#include "common.hh" + +#include "http_protocol.hh" + +#include <memory> +#include <string.h> +#include <time.h> +#include <vector> + +namespace { + +uint16_t number(std::string_view data, size_t start, size_t end) { + uint16_t ret = 0; + assert(start < end); + for (; start < end; ++start) { + ret *= 10; + ret += data[start] - '0'; + } + return ret; +} + +inline char lower_ascii(char c) { + return (c >= 'A' && c <= 'Z') ? (c - 'A' + 'a') : c; +} + +inline bool is_lws(char c) { + return c == ' ' || c == '\t'; +} + +inline bool is_char(char c) { + return !(c & 0x80); +} + +inline bool is_ctl(char c) { + return c < ' ' || c == 0x7f; +} + +inline bool is_separator(char c) { + return is_lws(c) || c == '(' || c == ')' || c == '<' || c == '>' || c == '@' + || c == ',' || c == ';' || c == ':' || c == '\\' || c == '\"' || c == '/' + || c == '[' || c == ']' || c == '?' || c == '=' || c == '{' || c == '}'; +} + +inline bool is_token(char c) { + return is_char(c) && !is_ctl(c) && !is_separator(c); +} + +void make_lowercase(std::string& data, size_t start, size_t end) { + for (size_t i = start; i <= end; ++i) { + char lower = lower_ascii(data[i]); + if (lower != data[i]) + data[i] = lower; + } +} + +bool allow_header_append(std::string_view name) { + // These headers doesn't handle being merged with ',' even if the standard + // say they must + return !(name == "set-cookie" || name == "set-cookie2"); +} + +enum ParseResult { + GOOD, + BAD, + INCOMPLETE, +}; + +class HeaderIteratorImpl : public HeaderIterator { +public: + HeaderIteratorImpl(std::string_view data, std::vector<size_t> const* headers) + : data_(data), headers_(headers), iter_(headers_->begin()) { + } + + bool valid() const override { + return iter_ != headers_->end(); + } + + std::string_view name() const override { + return data_.substr(iter_[0], iter_[1] - iter_[0]); + } + + std::string value() const override { + std::string ret(data_.substr(iter_[2], iter_[3] - iter_[2])); + if (allow_header_append(name())) { + auto i = iter_ + 4; + while (i != headers_->end()) { + if (i[0] != i[1]) break; + ret.push_back(','); + ret.append(data_.substr(i[2], i[3] - i[2])); + i += 4; + } + } + return ret; + } + + void next() override { + if (iter_ != headers_->end()) { + while (true) { + iter_ += 4; + if (iter_ == headers_->end() || iter_[0] != iter_[1]) + break; + } + } + } + +private: + std::string_view const data_; + std::vector<size_t> const* const headers_; + std::vector<size_t>::const_iterator iter_; +}; + +class FilterHeaderIteratorImpl : public HeaderIteratorImpl { +public: + FilterHeaderIteratorImpl(std::string_view data, + std::vector<size_t> const* headers, + std::string_view filter) + : HeaderIteratorImpl(data, headers), filter_(filter) { + check_filter(); + } + + void next() override { + HeaderIteratorImpl::next(); + check_filter(); + } + +private: + void check_filter() { + while (true) { + if (!valid() || name() == filter_) + return; + HeaderIteratorImpl::next(); + } + } + + std::string_view const filter_; +}; + +class HeaderTokenIteratorImpl : public HeaderTokenIterator { +public: + explicit HeaderTokenIteratorImpl(std::unique_ptr<HeaderIterator>&& header) + : header_(std::move(header)), start_(0), middle_(0), end_(0) { + check_token(); + } + + bool valid() const override { + return header_->valid(); + } + + std::string token() const override { + return header_->value().substr(start_, middle_ - start_); + } + + void next() override { + start_ = end_; + check_token(); + } + +private: + static size_t skip_lws(std::string const& str, size_t pos) { + while (pos < str.size() && is_lws(str[pos])) ++pos; + return pos; + } + + static size_t skip_token(std::string const& str, size_t pos) { + assert(is_token(str[pos])); + ++pos; + while (pos < str.size() && is_token(str[pos])) ++pos; + return pos; + } + + static size_t skip_quoted(std::string const& str, size_t pos) { + assert(str[pos] == '"'); + ++pos; + while (pos < str.size()) { + if (str[pos] == '\\') { + pos += 2; + } else if (str[pos] == '\"') { + ++pos; + break; + } else { + ++pos; + } + } + return pos; + } + + void check_token() { + while (true) { + if (!header_->valid()) return; + auto const& value = header_->value(); + start_ = skip_lws(value, start_); + if (start_ >= value.size()) { + header_->next(); + start_ = 0; + continue; + } + if (!is_token(value[start_])) { + if (value[start_] != ';') { + ++start_; + while (start_ < value.size() + && !(is_lws(value[start_]) || value[start_] == ',' + || value[start_] == ';')) { + ++start_; + } + if (start_ < value.size() && value[start_] != ';') { + continue; + } + } + // This will cause us to loop again after paramters + // are read + middle_ = start_; + } else { + middle_ = skip_token(value, start_); + } + end_ = middle_; + while (true) { + end_ = skip_lws(value, end_); + if (end_ == value.size() || value[end_] != ';') break; + end_ = skip_lws(value, end_ + 1); + if (!is_token(value[end_])) { + while (end_ < value.size() && !is_separator(value[end_])) ++end_; + continue; + } + end_ = skip_token(value, end_); + end_ = skip_lws(value, end_); + if (end_ == value.size() || value[end_] != '=') break; + end_ = skip_lws(value, end_ + 1); + if (end_ < value.size() && value[end_] == '"') { + end_ = skip_quoted(value, end_); + } else { + if (!is_token(value[end_])) { + while (end_ < value.size() && !is_separator(value[end_])) ++end_; + continue; + } + end_ = skip_token(value, end_); + } + } + if (end_ < value.size() && value[end_] == ',') ++end_; + if (start_ < middle_) return; + start_ = end_; + } + } + + std::unique_ptr<HeaderIterator> header_; + size_t start_; + size_t middle_; + size_t end_; +}; + +size_t find_newline(std::string_view data, size_t start, size_t* next) { + assert(start <= data.size()); + for (; start < data.size(); ++start) { + if (data[start] == '\r') { + if (start + 1 < data.size() && data[start + 1] == '\n') { + if (next) *next = start + 2; + } else { + if (next) *next = start + 1; + } + return start; + } else if (data[start] == '\n') { + if (next) *next = start + 1; + return start; + } + } + return std::string::npos; +} + +size_t find(std::string_view data, size_t start, char c, size_t end) { + assert(start <= end); + for (; start < end; ++start) { + if (data[start] == c) return start; + } + return std::string::npos; +} + +size_t skip_lws(std::string_view data, size_t start, size_t end) { + assert(start <= end); + while (start < end && is_lws(data[start])) ++start; + return start; +} + +size_t valid_number(std::string_view data, size_t start, size_t end) { + assert(start <= end); + if (start == end) + return std::string::npos; + if (data[start] == '0') { + return start + 1; + } + if (data[start] < '0' || data[start] > '9') + return std::string::npos; + for (++start; start < end; ++start) { + if (data[start] < '0' || data[start] > '9') + break; + } + return start; +} + +ParseResult parse_headers(std::string_view data, size_t* offset, + std::vector<size_t>* headers) { + assert(*offset <= data.size()); + assert(headers->empty()); + while (true) { + auto start = *offset; + auto end = find_newline(data, start, offset); + if (end == std::string::npos) + return INCOMPLETE; + if (end == start) { + // The final newline can only be a alone '\r' if the one in front of + // it is also '\r', otherwise we expect a missing '\n' + if (data[start - 1] == '\n' && data[*offset - 1] == '\r') { + return INCOMPLETE; + } + break; + } + if (is_lws(data[start])) { + if (headers->empty()) + return BAD; + headers->push_back(start); + headers->push_back(start); + headers->push_back(start + 1); + headers->push_back(end); + } else { + auto colon = find(data, start, ':', end); + if (colon == std::string::npos) return BAD; + auto value_start = skip_lws(data, colon + 1, end); + while (colon > start && is_lws(data[colon - 1])) --colon; + headers->push_back(start); + headers->push_back(colon); + headers->push_back(value_start); + headers->push_back(end); + } + } + return GOOD; +} + +std::string make_lowercase_header_names(std::string_view data, + std::vector<size_t> const& headers) { + std::string ret(data); + for (size_t i = 0; i < headers.size(); i += 4) { + make_lowercase(ret, headers[i], headers[i + 1]); + } + return ret; +} + +class AbstractHttp : public virtual HttpPackage { +public: + AbstractHttp(std::string data, bool good, size_t proto_start, + size_t proto_slash, size_t proto_dot, size_t proto_end, + std::vector<size_t> headers, size_t content_start) + : data_(std::move(data)), good_(good), proto_start_(proto_start), + proto_slash_(proto_slash), proto_dot_(proto_dot), proto_end_(proto_end), + headers_(std::move(headers)), content_start_(content_start) { + } + + bool good() const override { + return good_; + } + + std::string_view proto() const override { + return std::string_view(data_).substr( + proto_start_, proto_slash_ - proto_start_); + } + + Version proto_version() const override { + Version ret; + ret.major = number(data_, proto_slash_ + 1, proto_dot_); + ret.minor = number(data_, proto_dot_ + 1, proto_end_); + return ret; + } + + std::unique_ptr<HeaderIterator> header() const override { + return std::make_unique<HeaderIteratorImpl>(data_, &headers_); + } + std::unique_ptr<HeaderIterator> header( + std::string_view name) const override { + return std::make_unique<FilterHeaderIteratorImpl>(data_, &headers_, name); + } + + std::unique_ptr<HeaderTokenIterator> header_tokens(std::string_view name) + const override { + return std::make_unique<HeaderTokenIteratorImpl>(header(name)); + } + + size_t size() const override { + return content_start_; + } + +protected: + std::string const data_; + bool const good_; + size_t const proto_start_; + size_t const proto_slash_; + size_t const proto_dot_; + size_t const proto_end_; + std::vector<size_t> const headers_; + size_t const content_start_; +}; + +class HttpResponseImpl : public HttpResponse, protected AbstractHttp { +public: + HttpResponseImpl(std::string data, bool good, + size_t proto_start, size_t proto_slash, + size_t proto_dot, size_t proto_end, + size_t status_start, + size_t status_end, size_t status_msg_start, + size_t status_msg_end, + std::vector<size_t> headers, + size_t content_start) + : AbstractHttp(std::move(data), good, proto_start, proto_slash, proto_dot, + proto_end, std::move(headers), content_start), + status_start_(status_start), status_end_(status_end), + status_msg_start_(status_msg_start), status_msg_end_(status_msg_end) { + } + + uint16_t status_code() const override { + return number(data_, status_start_, status_end_); + } + + std::string_view status_message() const override { + return std::string_view(data_).substr(status_msg_start_, + status_msg_end_ - status_msg_start_); + } + + static std::unique_ptr<HttpResponse> parse(std::string_view data) { + size_t content_start = 0; + size_t status_msg_end = find_newline(data, 0, &content_start); + if (status_msg_end == std::string::npos) + return nullptr; + size_t proto_start = 0; + size_t proto_slash = find(data, 0, '/', status_msg_end); + if (proto_slash == std::string::npos) + return make_bad_http_response(); + size_t proto_dot = valid_number(data, proto_slash + 1, status_msg_end); + if (proto_dot == std::string::npos || data[proto_dot] != '.') + return make_bad_http_response(); + size_t proto_end = valid_number(data, proto_dot + 1, status_msg_end); + if (proto_end == std::string::npos || !is_lws(data[proto_end])) + return make_bad_http_response(); + size_t status_start = skip_lws(data, proto_end + 1, status_msg_end); + size_t status_end = valid_number(data, status_start, status_msg_end); + if (status_end == std::string::npos) + return make_bad_http_response(); + size_t status_msg_start; + if (is_lws(data[status_end])) { + status_msg_start = skip_lws(data, status_end + 1, status_msg_end); + } else { + status_msg_start = status_end; + if (status_msg_start != status_msg_end) + return make_bad_http_response(); + } + + std::vector<size_t> headers; + switch (parse_headers(data, &content_start, &headers)) { + case GOOD: + return std::make_unique<HttpResponseImpl>( + make_lowercase_header_names(data, headers), true, + proto_start, proto_slash, proto_dot, proto_end, + status_start, status_end, status_msg_start, status_msg_end, + std::move(headers), content_start); + case BAD: + return make_bad_http_response(); + case INCOMPLETE: + return nullptr; + } + assert(false); + return nullptr; + } + +private: + static std::unique_ptr<HttpResponse> make_bad_http_response() { + return std::make_unique<HttpResponseImpl>(std::string(), false, + 0, 0, 0, 0, 0, 0, 0, 0, + std::vector<size_t>(), 0); + } + + size_t const status_start_; + size_t const status_end_; + size_t const status_msg_start_; + size_t const status_msg_end_; +}; + +class HttpRequestImpl : public HttpRequest, protected AbstractHttp { +public: + HttpRequestImpl(std::string data, bool good, + size_t method_end, size_t url_start, size_t url_end, + size_t proto_start, size_t proto_slash, + size_t proto_dot, size_t proto_end, + std::vector<size_t> headers, + size_t content_start) + : AbstractHttp(std::move(data), good, proto_start, proto_slash, + proto_dot, proto_end, std::move(headers), content_start), + method_end_(method_end), url_start_(url_start), url_end_(url_end) { + } + + std::string_view method() const override { + return std::string_view(data_).substr(0, method_end_); + } + + std::string_view url() const override { + return std::string_view(data_).substr(url_start_, url_end_ - url_start_); + } + + static std::unique_ptr<HttpRequest> parse(std::string_view data) { + size_t content_start = 0; + size_t proto_end = find_newline(data, 0, &content_start); + if (proto_end == std::string::npos) + return nullptr; + size_t method_end = 0; + while (method_end < proto_end && !is_lws(data[method_end])) { + ++method_end; + } + if (method_end == 0 || method_end == proto_end) + return make_bad_request(); + size_t url_start = skip_lws(data, method_end + 1, proto_end); + size_t url_end = url_start; + while (url_end < proto_end && !is_lws(data[url_end])) + ++url_end; + if (url_end == url_start || url_end == proto_end) + return make_bad_request(); + size_t proto_start = skip_lws(data, url_end + 1, proto_end); + size_t proto_slash = find(data, proto_start, '/', proto_end); + if (proto_slash == std::string::npos) + return make_bad_request(); + size_t proto_dot = valid_number(data, proto_slash + 1, proto_end); + if (proto_dot == std::string::npos || data[proto_dot] != '.') + return make_bad_request(); + auto tmp = valid_number(data, proto_dot + 1, proto_end); + if (tmp != proto_end) + return make_bad_request(); + + std::vector<size_t> headers; + switch (parse_headers(data, &content_start, &headers)) { + case GOOD: + return std::make_unique<HttpRequestImpl>( + make_lowercase_header_names(data, headers), true, + method_end, url_start, url_end, + proto_start, proto_slash, + proto_dot, proto_end, + std::move(headers), content_start); + case BAD: + return make_bad_request(); + case INCOMPLETE: + return nullptr; + } + assert(false); + return nullptr; + } + +private: + static std::unique_ptr<HttpRequest> make_bad_request() { + return std::make_unique<HttpRequestImpl>("", false, 0, 0, 0, 0, 0, 0, 0, + std::vector<size_t>(), 0); + } + + size_t const method_end_; + size_t const url_start_; + size_t const url_end_; +}; + +class AbstractHttpBuilder { +public: + void add_header(std::string name, std::string value) { + headers_.emplace_back(std::move(name), std::move(value)); + } + + bool build(Buffer* dst) const { + for (auto const& pair : headers_) { + if (pair.first.empty()) { + if (!append(dst, " ")) + return false; + } else { + if (!append(dst, pair.first) || + !append(dst, ": ")) + return false; + } + if (!append(dst, pair.second) || + !append(dst, "\r\n")) + return false; + } + return append(dst, "\r\n"); + } + + size_t size() const { + size_t ret = 0; + for (auto const& pair : headers_) { + if (pair.first.empty()) { + ++ret; // ' ' + } else { + ret += pair.first.size() + 2; // ": " + } + ret += pair.second.size() + 2; // \r\” + } + return ret + 2; // \r\n + } + +protected: + bool append(Buffer* dst, std::string_view str) const { + return Buffer::write(dst, str.data(), str.size()) == str.size(); + } + + std::vector<std::pair<std::string, std::string>> headers_; +}; + +class HttpRequestBuilderImpl : public HttpRequestBuilder, AbstractHttpBuilder { +public: + HttpRequestBuilderImpl(std::string method, std::string url, std::string proto, + Version version) + : method_(std::move(method)), url_(std::move(url)), + proto_(std::move(proto)), version_(version) {} + + void add_header(std::string name, std::string value) override { + AbstractHttpBuilder::add_header(std::move(name), std::move(value)); + } + + bool build(Buffer* dst) const override { + if (!append(dst, method_) || + !append(dst, " ") || + !append(dst, url_) || + !append(dst, " ") || + !append(dst, proto_) || + !append(dst, "/")) + return false; + char tmp[10]; + auto len = snprintf(tmp, sizeof(tmp), "%u", + static_cast<unsigned int>(version_.major)); + if (!append(dst, std::string_view(tmp, len))) + return false; + if (!append(dst, ".")) + return false; + len = snprintf(tmp, sizeof(tmp), "%u", + static_cast<unsigned int>(version_.minor)); + if (!append(dst, std::string_view(tmp, len))) + return false; + if (!append(dst, "\r\n")) + return false; + return AbstractHttpBuilder::build(dst); + } + + size_t size() const override { + size_t ret = 0; + ret += method_.size() + 1 + url_.size() + 1 + proto_.size() + 1; + char tmp[10]; + auto len = snprintf(tmp, sizeof(tmp), "%u", + static_cast<unsigned int>(version_.major)); + ret += len; + ++ret; // '.' + len = snprintf(tmp, sizeof(tmp), "%u", + static_cast<unsigned int>(version_.minor)); + ret += len; + ret += 2; // \r\n + return ret + AbstractHttpBuilder::size(); + } + +private: + std::string const method_; + std::string const url_; + std::string const proto_; + Version const version_; +}; + +class HttpResponseBuilderImpl : public HttpResponseBuilder, + AbstractHttpBuilder { +public: + HttpResponseBuilderImpl(std::string proto, Version version, + uint16_t status_code, std::string status) + : proto_(std::move(proto)), version_(version), status_code_(status_code), + status_(std::move(status)) { + } + + void add_header(std::string name, std::string value) override { + AbstractHttpBuilder::add_header(std::move(name), std::move(value)); + } + + bool build(Buffer* dst) const override { + if (!append(dst, proto_) || + !append(dst, "/")) + return false; + char tmp[10]; + auto len = snprintf(tmp, sizeof(tmp), "%u", + static_cast<unsigned int>(version_.major)); + if (!append(dst, std::string_view(tmp, len)) || + !append(dst, ".")) + return false; + len = snprintf(tmp, sizeof(tmp), "%u", + static_cast<unsigned int>(version_.minor)); + if (!append(dst, std::string_view(tmp, len)) || + !append(dst, " ")) + return false; + len = snprintf(tmp, sizeof(tmp), "%u", + static_cast<unsigned int>(status_code_)); + if (!append(dst, std::string_view(tmp, len)) || + !append(dst, " ") || + !append(dst, status_) || + !append(dst, "\r\n")) + return false; + return AbstractHttpBuilder::build(dst); + } + + size_t size() const override { + size_t ret = 0; + ret += proto_.size(); + ++ret; // "/" + char tmp[10]; + auto len = snprintf(tmp, sizeof(tmp), "%u", + static_cast<unsigned int>(version_.major)); + ret += len; + ++ret; // "." + len = snprintf(tmp, sizeof(tmp), "%u", + static_cast<unsigned int>(version_.minor)); + ret += len; + ++ret; // " " + len = snprintf(tmp, sizeof(tmp), "%u", + static_cast<unsigned int>(status_code_)); + ret += len; + ++ret; // " " + ret += status_.length(); + ret += 2; // \r\n + return ret + AbstractHttpBuilder::size(); + } + +private: + std::string const proto_; + Version const version_; + uint16_t const status_code_; + std::string const status_; +}; + +class CgiResponseBuilderImpl : public CgiResponseBuilder, + AbstractHttpBuilder { +public: + explicit CgiResponseBuilderImpl(uint16_t status_code) { + AbstractHttpBuilder::add_header("Status", std::to_string(status_code)); + } + + void add_header(std::string name, std::string value) override { + AbstractHttpBuilder::add_header(std::move(name), std::move(value)); + } + + bool build(Buffer* dst) const override { + return AbstractHttpBuilder::build(dst); + } + + size_t size() const override { + return AbstractHttpBuilder::size(); + } +}; + +} // namespace + +// static +std::unique_ptr<HttpResponse> HttpResponse::parse(RoBuffer* buffer) { + size_t want = 1024; + size_t last_avail = 0; + while (true) { + size_t avail; + auto* rptr = buffer->rbuf(want, avail); + if (avail == last_avail) + return nullptr; + last_avail = avail; + auto resp = HttpResponseImpl::parse(std::string_view(rptr, avail)); + if (resp) { + buffer->rcommit(resp->size()); + return resp; + } + want = avail + 1024; + } +} + +std::string HttpPackage::first_header(std::string_view name) const { + static std::string empty_str; + auto iter = header(name); + if (iter->valid()) { + return iter->value(); + } + return empty_str; +} + +// static +std::unique_ptr<HttpRequest> HttpRequest::parse(RoBuffer* buffer) { + size_t want = 1024; + size_t last_avail = 0; + while (true) { + size_t avail; + auto* rptr = buffer->rbuf(want, avail); + if (avail == last_avail) + return nullptr; + last_avail = avail; + auto req = HttpRequestImpl::parse(std::string_view(rptr, avail)); + if (req) { + buffer->rcommit(req->size()); + return req; + } + want = avail + 1024; + } +} + +// static +std::unique_ptr<HttpRequestBuilder> HttpRequestBuilder::create( + std::string method, + std::string url, + std::string proto, + Version version) { + return std::make_unique<HttpRequestBuilderImpl>(std::move(method), + std::move(url), + std::move(proto), version); +} + +// static +std::unique_ptr<HttpResponseBuilder> HttpResponseBuilder::create( + std::string proto, + Version version, + uint16_t status_code, + std::string status) { + return std::make_unique<HttpResponseBuilderImpl>(std::move(proto), version, + status_code, + std::move(status)); +} + +// static +std::unique_ptr<CgiResponseBuilder> CgiResponseBuilder::create( + uint16_t status_code) { + return std::make_unique<CgiResponseBuilderImpl>(status_code); +} + +std::string_view http_standard_message(uint16_t code) { + switch (code) { + case 100: + return "Continue"; + case 101: + return "Switching Protocols"; + case 200: + return "OK"; + case 201: + return "Created"; + case 202: + return "Accepted"; + case 203: + return "Non-Authorative Information"; + case 204: + return "No Content"; + case 205: + return "Reset Content"; + case 206: + return "Partial Content"; + case 300: + return "Multiple Choices"; + case 301: + return "Moved Permanently"; + case 302: + return "Found"; + case 303: + return "See Other"; + case 304: + return "Not Modified"; + case 305: + return "Use Proxy"; + case 307: + return "Temporary Redirect"; + case 400: + return "Bad Request"; + case 401: + return "Unauthorized"; + case 402: + return "Payment Required"; + case 403: + return "Forbidden"; + case 404: + return "Not Found"; + case 405: + return "Method Not Allowed"; + case 406: + return "Not Acceptable"; + case 407: + return "Proxy Authentication Required"; + case 408: + return "Request Timeout"; + case 409: + return "Conflict"; + case 410: + return "Gone"; + case 411: + return "Length Required"; + case 412: + return "Precondition Failed"; + case 413: + return "Request Entity Too Large"; + case 414: + return "Request-URI Too Long"; + case 415: + return "Unsupported Media Type"; + case 416: + return "Requested Range Not Satisfiable"; + case 417: + return "Expectation Failed"; + case 500: + return "Internal Server Error"; + case 501: + return "Not Implemented"; + case 502: + return "Bad Gateway"; + case 503: + return "Service Unavailable"; + case 504: + return "Gateway Timeout"; + case 505: + return "HTTP Version Not Supported"; + } + return ""; +} + +std::string http_date(time_t in) { + char tmp[50]; + auto len = strftime(tmp, sizeof(tmp), "%a, %d %b %Y %H:%M:%S GMT", + gmtime(&in)); + return std::string(tmp, len); +} |
