nghttpd: Use StringRef
This commit is contained in:
parent
a1e0bd134e
commit
db1ee3aa88
|
@ -71,8 +71,9 @@ namespace nghttp2 {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// TODO could be constexpr
|
// TODO could be constexpr
|
||||||
constexpr char DEFAULT_HTML[] = "index.html";
|
constexpr auto DEFAULT_HTML = StringRef::from_lit("index.html");
|
||||||
constexpr char NGHTTPD_SERVER[] = "nghttpd nghttp2/" NGHTTP2_VERSION;
|
constexpr auto NGHTTPD_SERVER =
|
||||||
|
StringRef::from_lit("nghttpd nghttp2/" NGHTTP2_VERSION);
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
@ -86,20 +87,6 @@ namespace {
|
||||||
void print_session_id(int64_t id) { std::cout << "[id=" << id << "] "; }
|
void print_session_id(int64_t id) { std::cout << "[id=" << id << "] "; }
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace {
|
|
||||||
template <typename Array> void append_nv(Stream *stream, const Array &nva) {
|
|
||||||
for (size_t i = 0; i < nva.size(); ++i) {
|
|
||||||
auto &nv = nva[i];
|
|
||||||
auto token = http2::lookup_token(nv.name, nv.namelen);
|
|
||||||
if (token != -1) {
|
|
||||||
http2::index_header(stream->hdidx, token, i);
|
|
||||||
}
|
|
||||||
http2::add_header(stream->headers, nv.name, nv.namelen, nv.value,
|
|
||||||
nv.valuelen, nv.flags & NGHTTP2_NV_FLAG_NO_INDEX, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
Config::Config()
|
Config::Config()
|
||||||
: mime_types_file("/etc/mime.types"),
|
: mime_types_file("/etc/mime.types"),
|
||||||
stream_read_timeout(1_min),
|
stream_read_timeout(1_min),
|
||||||
|
@ -442,7 +429,9 @@ void release_fd_cb(struct ev_loop *loop, ev_timer *w, int revents) {
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Stream::Stream(Http2Handler *handler, int32_t stream_id)
|
Stream::Stream(Http2Handler *handler, int32_t stream_id)
|
||||||
: handler(handler),
|
: balloc(1024, 1024),
|
||||||
|
header{},
|
||||||
|
handler(handler),
|
||||||
file_ent(nullptr),
|
file_ent(nullptr),
|
||||||
body_length(0),
|
body_length(0),
|
||||||
body_offset(0),
|
body_offset(0),
|
||||||
|
@ -454,10 +443,6 @@ Stream::Stream(Http2Handler *handler, int32_t stream_id)
|
||||||
ev_timer_init(&wtimer, stream_timeout_cb, 0., config->stream_write_timeout);
|
ev_timer_init(&wtimer, stream_timeout_cb, 0., config->stream_write_timeout);
|
||||||
rtimer.data = this;
|
rtimer.data = this;
|
||||||
wtimer.data = this;
|
wtimer.data = this;
|
||||||
|
|
||||||
headers.reserve(10);
|
|
||||||
|
|
||||||
http2::init_hdidx(hdidx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream::~Stream() {
|
Stream::~Stream() {
|
||||||
|
@ -466,6 +451,15 @@ Stream::~Stream() {
|
||||||
sessions->release_fd(file_ent);
|
sessions->release_fd(file_ent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto &rcbuf = header.rcbuf;
|
||||||
|
nghttp2_rcbuf_decref(rcbuf.method);
|
||||||
|
nghttp2_rcbuf_decref(rcbuf.scheme);
|
||||||
|
nghttp2_rcbuf_decref(rcbuf.authority);
|
||||||
|
nghttp2_rcbuf_decref(rcbuf.host);
|
||||||
|
nghttp2_rcbuf_decref(rcbuf.path);
|
||||||
|
nghttp2_rcbuf_decref(rcbuf.ims);
|
||||||
|
nghttp2_rcbuf_decref(rcbuf.expect);
|
||||||
|
|
||||||
auto loop = handler->get_loop();
|
auto loop = handler->get_loop();
|
||||||
ev_timer_stop(loop, &rtimer);
|
ev_timer_stop(loop, &rtimer);
|
||||||
ev_timer_stop(loop, &wtimer);
|
ev_timer_stop(loop, &wtimer);
|
||||||
|
@ -909,37 +903,22 @@ int Http2Handler::verify_npn_result() {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
int Http2Handler::submit_file_response(const StringRef &status, Stream *stream,
|
||||||
std::string make_trailer_header_value(const Headers &trailer) {
|
time_t last_modified, off_t file_length,
|
||||||
if (trailer.empty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
auto trailer_names = trailer[0].name;
|
|
||||||
for (size_t i = 1; i < trailer.size(); ++i) {
|
|
||||||
trailer_names += ", ";
|
|
||||||
trailer_names += trailer[i].name;
|
|
||||||
}
|
|
||||||
return trailer_names;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int Http2Handler::submit_file_response(const std::string &status,
|
|
||||||
Stream *stream, time_t last_modified,
|
|
||||||
off_t file_length,
|
|
||||||
const std::string *content_type,
|
const std::string *content_type,
|
||||||
nghttp2_data_provider *data_prd) {
|
nghttp2_data_provider *data_prd) {
|
||||||
std::string content_length = util::utos(file_length);
|
|
||||||
std::string last_modified_str;
|
std::string last_modified_str;
|
||||||
auto nva = make_array(http2::make_nv_ls(":status", status),
|
auto nva = make_array(http2::make_nv_ls_nocopy(":status", status),
|
||||||
http2::make_nv_ll("server", NGHTTPD_SERVER),
|
http2::make_nv_ls_nocopy("server", NGHTTPD_SERVER),
|
||||||
http2::make_nv_ll("cache-control", "max-age=3600"),
|
http2::make_nv_ll("cache-control", "max-age=3600"),
|
||||||
http2::make_nv_ls("date", sessions_->get_cached_date()),
|
http2::make_nv_ls("date", sessions_->get_cached_date()),
|
||||||
http2::make_nv_ll("", ""), http2::make_nv_ll("", ""),
|
http2::make_nv_ll("", ""), http2::make_nv_ll("", ""),
|
||||||
http2::make_nv_ll("", ""), http2::make_nv_ll("", ""));
|
http2::make_nv_ll("", ""), http2::make_nv_ll("", ""));
|
||||||
size_t nvlen = 4;
|
size_t nvlen = 4;
|
||||||
if (!get_config()->no_content_length) {
|
if (!get_config()->no_content_length) {
|
||||||
nva[nvlen++] = http2::make_nv_ls("content-length", content_length);
|
nva[nvlen++] = http2::make_nv_ls_nocopy(
|
||||||
|
"content-length",
|
||||||
|
util::make_string_ref_uint(stream->balloc, file_length));
|
||||||
}
|
}
|
||||||
if (last_modified != 0) {
|
if (last_modified != 0) {
|
||||||
last_modified_str = util::http_date(last_modified);
|
last_modified_str = util::http_date(last_modified);
|
||||||
|
@ -948,52 +927,50 @@ int Http2Handler::submit_file_response(const std::string &status,
|
||||||
if (content_type) {
|
if (content_type) {
|
||||||
nva[nvlen++] = http2::make_nv_ls("content-type", *content_type);
|
nva[nvlen++] = http2::make_nv_ls("content-type", *content_type);
|
||||||
}
|
}
|
||||||
auto trailer_names = make_trailer_header_value(get_config()->trailer);
|
auto &trailer_names = get_config()->trailer_names;
|
||||||
if (!trailer_names.empty()) {
|
if (!trailer_names.empty()) {
|
||||||
nva[nvlen++] = http2::make_nv_ls("trailer", trailer_names);
|
nva[nvlen++] = http2::make_nv_ls_nocopy("trailer", trailer_names);
|
||||||
}
|
}
|
||||||
return nghttp2_submit_response(session_, stream->stream_id, nva.data(), nvlen,
|
return nghttp2_submit_response(session_, stream->stream_id, nva.data(), nvlen,
|
||||||
data_prd);
|
data_prd);
|
||||||
}
|
}
|
||||||
|
|
||||||
int Http2Handler::submit_response(const std::string &status, int32_t stream_id,
|
int Http2Handler::submit_response(const StringRef &status, int32_t stream_id,
|
||||||
const Headers &headers,
|
const HeaderRefs &headers,
|
||||||
nghttp2_data_provider *data_prd) {
|
nghttp2_data_provider *data_prd) {
|
||||||
auto nva = std::vector<nghttp2_nv>();
|
auto nva = std::vector<nghttp2_nv>();
|
||||||
nva.reserve(4 + headers.size());
|
nva.reserve(4 + headers.size());
|
||||||
nva.push_back(http2::make_nv_ls(":status", status));
|
nva.push_back(http2::make_nv_ls_nocopy(":status", status));
|
||||||
nva.push_back(http2::make_nv_ll("server", NGHTTPD_SERVER));
|
nva.push_back(http2::make_nv_ls_nocopy("server", NGHTTPD_SERVER));
|
||||||
nva.push_back(http2::make_nv_ls("date", sessions_->get_cached_date()));
|
nva.push_back(http2::make_nv_ls("date", sessions_->get_cached_date()));
|
||||||
|
|
||||||
std::string trailer_names;
|
|
||||||
if (data_prd) {
|
if (data_prd) {
|
||||||
trailer_names = make_trailer_header_value(get_config()->trailer);
|
auto &trailer_names = get_config()->trailer_names;
|
||||||
if (!trailer_names.empty()) {
|
if (!trailer_names.empty()) {
|
||||||
nva.push_back(http2::make_nv_ls("trailer", trailer_names));
|
nva.push_back(http2::make_nv_ls_nocopy("trailer", trailer_names));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto &nv : headers) {
|
for (auto &nv : headers) {
|
||||||
nva.push_back(http2::make_nv(nv.name, nv.value, nv.no_index));
|
nva.push_back(http2::make_nv_nocopy(nv.name, nv.value, nv.no_index));
|
||||||
}
|
}
|
||||||
int r = nghttp2_submit_response(session_, stream_id, nva.data(), nva.size(),
|
int r = nghttp2_submit_response(session_, stream_id, nva.data(), nva.size(),
|
||||||
data_prd);
|
data_prd);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Http2Handler::submit_response(const std::string &status, int32_t stream_id,
|
int Http2Handler::submit_response(const StringRef &status, int32_t stream_id,
|
||||||
nghttp2_data_provider *data_prd) {
|
nghttp2_data_provider *data_prd) {
|
||||||
auto nva = make_array(http2::make_nv_ls(":status", status),
|
auto nva = make_array(http2::make_nv_ls_nocopy(":status", status),
|
||||||
http2::make_nv_ll("server", NGHTTPD_SERVER),
|
http2::make_nv_ls_nocopy("server", NGHTTPD_SERVER),
|
||||||
http2::make_nv_ls("date", sessions_->get_cached_date()),
|
http2::make_nv_ls("date", sessions_->get_cached_date()),
|
||||||
http2::make_nv_ll("", ""));
|
http2::make_nv_ll("", ""));
|
||||||
size_t nvlen = 3;
|
size_t nvlen = 3;
|
||||||
|
|
||||||
std::string trailer_names;
|
|
||||||
if (data_prd) {
|
if (data_prd) {
|
||||||
trailer_names = make_trailer_header_value(get_config()->trailer);
|
auto &trailer_names = get_config()->trailer_names;
|
||||||
if (!trailer_names.empty()) {
|
if (!trailer_names.empty()) {
|
||||||
nva[nvlen++] = http2::make_nv_ls("trailer", trailer_names);
|
nva[nvlen++] = http2::make_nv_ls_nocopy("trailer", trailer_names);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1009,21 +986,20 @@ int Http2Handler::submit_non_final_response(const std::string &status,
|
||||||
}
|
}
|
||||||
|
|
||||||
int Http2Handler::submit_push_promise(Stream *stream,
|
int Http2Handler::submit_push_promise(Stream *stream,
|
||||||
const std::string &push_path) {
|
const StringRef &push_path) {
|
||||||
auto authority =
|
auto authority = stream->header.authority;
|
||||||
http2::get_header(stream->hdidx, http2::HD__AUTHORITY, stream->headers);
|
|
||||||
|
|
||||||
if (!authority) {
|
if (authority.empty()) {
|
||||||
authority =
|
authority = stream->header.host;
|
||||||
http2::get_header(stream->hdidx, http2::HD_HOST, stream->headers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto nva =
|
auto scheme = get_config()->no_tls ? StringRef::from_lit("http")
|
||||||
make_array(http2::make_nv_ll(":method", "GET"),
|
: StringRef::from_lit("https");
|
||||||
http2::make_nv_ls(":path", push_path),
|
|
||||||
get_config()->no_tls ? http2::make_nv_ll(":scheme", "http")
|
auto nva = make_array(http2::make_nv_ll(":method", "GET"),
|
||||||
: http2::make_nv_ll(":scheme", "https"),
|
http2::make_nv_ls_nocopy(":path", push_path),
|
||||||
http2::make_nv_ls(":authority", authority->value));
|
http2::make_nv_ls_nocopy(":scheme", scheme),
|
||||||
|
http2::make_nv_ls_nocopy(":authority", authority));
|
||||||
|
|
||||||
auto promised_stream_id = nghttp2_submit_push_promise(
|
auto promised_stream_id = nghttp2_submit_push_promise(
|
||||||
session_, NGHTTP2_FLAG_END_HEADERS, stream->stream_id, nva.data(),
|
session_, NGHTTP2_FLAG_END_HEADERS, stream->stream_id, nva.data(),
|
||||||
|
@ -1035,7 +1011,13 @@ int Http2Handler::submit_push_promise(Stream *stream,
|
||||||
|
|
||||||
auto promised_stream = make_unique<Stream>(this, promised_stream_id);
|
auto promised_stream = make_unique<Stream>(this, promised_stream_id);
|
||||||
|
|
||||||
append_nv(promised_stream.get(), nva);
|
auto &promised_header = promised_stream->header;
|
||||||
|
promised_header.method = StringRef::from_lit("GET");
|
||||||
|
promised_header.path = push_path;
|
||||||
|
promised_header.scheme = scheme;
|
||||||
|
promised_header.authority =
|
||||||
|
make_string_ref(promised_stream->balloc, authority);
|
||||||
|
|
||||||
add_stream(promised_stream_id, std::move(promised_stream));
|
add_stream(promised_stream_id, std::move(promised_stream));
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -1138,10 +1120,11 @@ void prepare_status_response(Stream *stream, Http2Handler *hd, int status) {
|
||||||
data_prd.source.fd = file_ent->fd;
|
data_prd.source.fd = file_ent->fd;
|
||||||
data_prd.read_callback = file_read_callback;
|
data_prd.read_callback = file_read_callback;
|
||||||
|
|
||||||
Headers headers;
|
HeaderRefs headers;
|
||||||
headers.emplace_back("content-type", "text/html; charset=UTF-8");
|
headers.emplace_back(StringRef::from_lit("content-type"),
|
||||||
hd->submit_response(status_page->status, stream->stream_id, headers,
|
StringRef::from_lit("text/html; charset=UTF-8"));
|
||||||
&data_prd);
|
hd->submit_response(StringRef{status_page->status}, stream->stream_id,
|
||||||
|
headers, &data_prd);
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
@ -1161,13 +1144,16 @@ void prepare_echo_response(Stream *stream, Http2Handler *hd) {
|
||||||
data_prd.source.fd = stream->file_ent->fd;
|
data_prd.source.fd = stream->file_ent->fd;
|
||||||
data_prd.read_callback = file_read_callback;
|
data_prd.read_callback = file_read_callback;
|
||||||
|
|
||||||
Headers headers;
|
HeaderRefs headers;
|
||||||
headers.emplace_back("nghttpd-response", "echo");
|
headers.emplace_back(StringRef::from_lit("nghttpd-response"),
|
||||||
|
StringRef::from_lit("echo"));
|
||||||
if (!hd->get_config()->no_content_length) {
|
if (!hd->get_config()->no_content_length) {
|
||||||
headers.emplace_back("content-length", util::utos(length));
|
headers.emplace_back(StringRef::from_lit("content-length"),
|
||||||
|
util::make_string_ref_uint(stream->balloc, length));
|
||||||
}
|
}
|
||||||
|
|
||||||
hd->submit_response("200", stream->stream_id, headers, &data_prd);
|
hd->submit_response(StringRef::from_lit("200"), stream->stream_id, headers,
|
||||||
|
&data_prd);
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
@ -1193,27 +1179,31 @@ bool prepare_upload_temp_store(Stream *stream, Http2Handler *hd) {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
void prepare_redirect_response(Stream *stream, Http2Handler *hd,
|
void prepare_redirect_response(Stream *stream, Http2Handler *hd,
|
||||||
const std::string &path, int status) {
|
const StringRef &path, int status) {
|
||||||
auto scheme =
|
auto scheme = stream->header.scheme;
|
||||||
http2::get_header(stream->hdidx, http2::HD__SCHEME, stream->headers);
|
|
||||||
auto authority =
|
auto authority = stream->header.authority;
|
||||||
http2::get_header(stream->hdidx, http2::HD__AUTHORITY, stream->headers);
|
if (authority.empty()) {
|
||||||
if (!authority) {
|
authority = stream->header.host;
|
||||||
authority =
|
|
||||||
http2::get_header(stream->hdidx, http2::HD_HOST, stream->headers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto redirect_url = scheme->value;
|
size_t len = scheme.size() + str_size("://") + authority.size() + path.size();
|
||||||
redirect_url += "://";
|
auto iov = make_byte_ref(stream->balloc, len + 1);
|
||||||
redirect_url += authority->value;
|
auto p = iov.base;
|
||||||
redirect_url += path;
|
p = std::copy(std::begin(scheme), std::end(scheme), p);
|
||||||
|
p = util::copy_lit(p, "://");
|
||||||
|
p = std::copy(std::begin(authority), std::end(authority), p);
|
||||||
|
p = std::copy(std::begin(path), std::end(path), p);
|
||||||
|
*p = '\0';
|
||||||
|
|
||||||
auto headers = Headers{{"location", redirect_url}};
|
auto headers =
|
||||||
|
HeaderRefs{{StringRef::from_lit("location"), StringRef{iov.base, p}}};
|
||||||
|
|
||||||
auto sessions = hd->get_sessions();
|
auto sessions = hd->get_sessions();
|
||||||
auto status_page = sessions->get_server()->get_status_page(status);
|
auto status_page = sessions->get_server()->get_status_page(status);
|
||||||
|
|
||||||
hd->submit_response(status_page->status, stream->stream_id, headers, nullptr);
|
hd->submit_response(StringRef{status_page->status}, stream->stream_id,
|
||||||
|
headers, nullptr);
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
@ -1221,39 +1211,49 @@ namespace {
|
||||||
void prepare_response(Stream *stream, Http2Handler *hd,
|
void prepare_response(Stream *stream, Http2Handler *hd,
|
||||||
bool allow_push = true) {
|
bool allow_push = true) {
|
||||||
int rv;
|
int rv;
|
||||||
auto pathhdr =
|
auto reqpath = stream->header.path;
|
||||||
http2::get_header(stream->hdidx, http2::HD__PATH, stream->headers);
|
if (reqpath.empty()) {
|
||||||
if (!pathhdr) {
|
|
||||||
prepare_status_response(stream, hd, 405);
|
prepare_status_response(stream, hd, 405);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto reqpath = pathhdr->value;
|
|
||||||
auto ims =
|
auto ims = stream->header.ims;
|
||||||
get_header(stream->hdidx, http2::HD_IF_MODIFIED_SINCE, stream->headers);
|
|
||||||
|
|
||||||
time_t last_mod = 0;
|
time_t last_mod = 0;
|
||||||
bool last_mod_found = false;
|
bool last_mod_found = false;
|
||||||
if (ims) {
|
if (!ims.empty()) {
|
||||||
last_mod_found = true;
|
last_mod_found = true;
|
||||||
last_mod = util::parse_http_date(StringRef{ims->value});
|
last_mod = util::parse_http_date(ims);
|
||||||
}
|
}
|
||||||
auto query_pos = reqpath.find("?");
|
|
||||||
std::string url;
|
StringRef raw_path, raw_query;
|
||||||
if (query_pos != std::string::npos) {
|
auto query_pos = std::find(std::begin(reqpath), std::end(reqpath), '?');
|
||||||
|
if (query_pos != std::end(reqpath)) {
|
||||||
// Do not response to this request to allow clients to test timeouts.
|
// Do not response to this request to allow clients to test timeouts.
|
||||||
if (reqpath.find("nghttpd_do_not_respond_to_req=yes", query_pos) !=
|
if (util::streq_l("nghttpd_do_not_respond_to_req=yes",
|
||||||
std::string::npos) {
|
StringRef{query_pos, std::end(reqpath)})) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
url = reqpath.substr(0, query_pos);
|
raw_path = StringRef{std::begin(reqpath), query_pos};
|
||||||
|
raw_query = StringRef{query_pos, std::end(reqpath)};
|
||||||
} else {
|
} else {
|
||||||
url = reqpath;
|
raw_path = reqpath;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto sessions = hd->get_sessions();
|
auto sessions = hd->get_sessions();
|
||||||
|
|
||||||
url = util::percent_decode(std::begin(url), std::end(url));
|
StringRef path;
|
||||||
if (!util::check_path(url)) {
|
if (std::find(std::begin(raw_path), std::end(raw_path), '%') ==
|
||||||
|
std::end(raw_path)) {
|
||||||
|
path = raw_path;
|
||||||
|
} else {
|
||||||
|
path = util::percent_decode(stream->balloc, raw_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
path = http2::path_join(stream->balloc, StringRef{}, StringRef{}, path,
|
||||||
|
StringRef{});
|
||||||
|
|
||||||
|
if (std::find(std::begin(path), std::end(path), '\\') != std::end(path)) {
|
||||||
if (stream->file_ent) {
|
if (stream->file_ent) {
|
||||||
sessions->release_fd(stream->file_ent);
|
sessions->release_fd(stream->file_ent);
|
||||||
stream->file_ent = nullptr;
|
stream->file_ent = nullptr;
|
||||||
|
@ -1261,20 +1261,39 @@ void prepare_response(Stream *stream, Http2Handler *hd,
|
||||||
prepare_status_response(stream, hd, 404);
|
prepare_status_response(stream, hd, 404);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto push_itr = hd->get_config()->push.find(url);
|
|
||||||
|
if (!hd->get_config()->push.empty()) {
|
||||||
|
auto push_itr = hd->get_config()->push.find(path.str());
|
||||||
if (allow_push && push_itr != std::end(hd->get_config()->push)) {
|
if (allow_push && push_itr != std::end(hd->get_config()->push)) {
|
||||||
for (auto &push_path : (*push_itr).second) {
|
for (auto &push_path : (*push_itr).second) {
|
||||||
rv = hd->submit_push_promise(stream, push_path);
|
rv = hd->submit_push_promise(stream, StringRef{push_path});
|
||||||
if (rv != 0) {
|
if (rv != 0) {
|
||||||
std::cerr << "nghttp2_submit_push_promise() returned error: "
|
std::cerr << "nghttp2_submit_push_promise() returned error: "
|
||||||
<< nghttp2_strerror(rv) << std::endl;
|
<< nghttp2_strerror(rv) << std::endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::string path = hd->get_config()->htdocs + url;
|
std::string file_path;
|
||||||
if (path[path.size() - 1] == '/') {
|
{
|
||||||
path += DEFAULT_HTML;
|
auto len = hd->get_config()->htdocs.size() + path.size();
|
||||||
|
|
||||||
|
auto trailing_slash = path[path.size() - 1] == '/';
|
||||||
|
if (trailing_slash) {
|
||||||
|
len += DEFAULT_HTML.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
file_path.resize(len);
|
||||||
|
|
||||||
|
auto p = &file_path[0];
|
||||||
|
|
||||||
|
auto &htdocs = hd->get_config()->htdocs;
|
||||||
|
p = std::copy(std::begin(htdocs), std::end(htdocs), p);
|
||||||
|
p = std::copy(std::begin(path), std::end(path), p);
|
||||||
|
if (trailing_slash) {
|
||||||
|
p = std::copy(std::begin(DEFAULT_HTML), std::end(DEFAULT_HTML), p);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stream->echo_upload) {
|
if (stream->echo_upload) {
|
||||||
|
@ -1283,10 +1302,10 @@ void prepare_response(Stream *stream, Http2Handler *hd,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto file_ent = sessions->get_cached_fd(path);
|
auto file_ent = sessions->get_cached_fd(file_path);
|
||||||
|
|
||||||
if (file_ent == nullptr) {
|
if (file_ent == nullptr) {
|
||||||
int file = open(path.c_str(), O_RDONLY | O_BINARY);
|
int file = open(file_path.c_str(), O_RDONLY | O_BINARY);
|
||||||
if (file == -1) {
|
if (file == -1) {
|
||||||
prepare_status_response(stream, hd, 404);
|
prepare_status_response(stream, hd, 404);
|
||||||
|
|
||||||
|
@ -1305,11 +1324,8 @@ void prepare_response(Stream *stream, Http2Handler *hd,
|
||||||
if (buf.st_mode & S_IFDIR) {
|
if (buf.st_mode & S_IFDIR) {
|
||||||
close(file);
|
close(file);
|
||||||
|
|
||||||
if (query_pos == std::string::npos) {
|
auto reqpath = concat_string_ref(stream->balloc, raw_path,
|
||||||
reqpath += '/';
|
StringRef::from_lit("/"), raw_query);
|
||||||
} else {
|
|
||||||
reqpath.insert(query_pos, "/");
|
|
||||||
}
|
|
||||||
|
|
||||||
prepare_redirect_response(stream, hd, reqpath, 301);
|
prepare_redirect_response(stream, hd, reqpath, 301);
|
||||||
|
|
||||||
|
@ -1318,12 +1334,8 @@ void prepare_response(Stream *stream, Http2Handler *hd,
|
||||||
|
|
||||||
const std::string *content_type = nullptr;
|
const std::string *content_type = nullptr;
|
||||||
|
|
||||||
if (path[path.size() - 1] == '/') {
|
auto ext = file_path.c_str() + file_path.size() - 1;
|
||||||
static const std::string TEXT_HTML = "text/html";
|
for (; file_path.c_str() < ext && *ext != '.' && *ext != '/'; --ext)
|
||||||
content_type = &TEXT_HTML;
|
|
||||||
} else {
|
|
||||||
auto ext = path.c_str() + path.size() - 1;
|
|
||||||
for (; path.c_str() < ext && *ext != '.' && *ext != '/'; --ext)
|
|
||||||
;
|
;
|
||||||
if (*ext == '.') {
|
if (*ext == '.') {
|
||||||
++ext;
|
++ext;
|
||||||
|
@ -1334,25 +1346,24 @@ void prepare_response(Stream *stream, Http2Handler *hd,
|
||||||
content_type = &(*content_type_itr).second;
|
content_type = &(*content_type_itr).second;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
file_ent = sessions->cache_fd(
|
file_ent = sessions->cache_fd(
|
||||||
path, FileEntry(path, buf.st_size, buf.st_mtime, file, content_type,
|
file_path, FileEntry(file_path, buf.st_size, buf.st_mtime, file,
|
||||||
ev_now(sessions->get_loop())));
|
content_type, ev_now(sessions->get_loop())));
|
||||||
}
|
}
|
||||||
|
|
||||||
stream->file_ent = file_ent;
|
stream->file_ent = file_ent;
|
||||||
|
|
||||||
if (last_mod_found && file_ent->mtime <= last_mod) {
|
if (last_mod_found && file_ent->mtime <= last_mod) {
|
||||||
hd->submit_response("304", stream->stream_id, nullptr);
|
hd->submit_response(StringRef::from_lit("304"), stream->stream_id, nullptr);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &method = http2::get_header(stream->hdidx, http2::HD__METHOD,
|
auto method = stream->header.method;
|
||||||
stream->headers)->value;
|
|
||||||
if (method == "HEAD") {
|
if (method == "HEAD") {
|
||||||
hd->submit_file_response("200", stream, file_ent->mtime, file_ent->length,
|
hd->submit_file_response(StringRef::from_lit("200"), stream,
|
||||||
|
file_ent->mtime, file_ent->length,
|
||||||
file_ent->content_type, nullptr);
|
file_ent->content_type, nullptr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1364,21 +1375,24 @@ void prepare_response(Stream *stream, Http2Handler *hd,
|
||||||
data_prd.source.fd = file_ent->fd;
|
data_prd.source.fd = file_ent->fd;
|
||||||
data_prd.read_callback = file_read_callback;
|
data_prd.read_callback = file_read_callback;
|
||||||
|
|
||||||
hd->submit_file_response("200", stream, file_ent->mtime, file_ent->length,
|
hd->submit_file_response(StringRef::from_lit("200"), stream, file_ent->mtime,
|
||||||
file_ent->content_type, &data_prd);
|
file_ent->length, file_ent->content_type, &data_prd);
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
int on_header_callback(nghttp2_session *session, const nghttp2_frame *frame,
|
int on_header_callback2(nghttp2_session *session, const nghttp2_frame *frame,
|
||||||
const uint8_t *name, size_t namelen,
|
nghttp2_rcbuf *name, nghttp2_rcbuf *value,
|
||||||
const uint8_t *value, size_t valuelen, uint8_t flags,
|
uint8_t flags, void *user_data) {
|
||||||
void *user_data) {
|
|
||||||
auto hd = static_cast<Http2Handler *>(user_data);
|
auto hd = static_cast<Http2Handler *>(user_data);
|
||||||
|
|
||||||
|
auto namebuf = nghttp2_rcbuf_get_buf(name);
|
||||||
|
auto valuebuf = nghttp2_rcbuf_get_buf(value);
|
||||||
|
|
||||||
if (hd->get_config()->verbose) {
|
if (hd->get_config()->verbose) {
|
||||||
print_session_id(hd->session_id());
|
print_session_id(hd->session_id());
|
||||||
verbose_on_header_callback(session, frame, name, namelen, value, valuelen,
|
verbose_on_header_callback(session, frame, namebuf.base, namebuf.len,
|
||||||
flags, user_data);
|
valuebuf.base, valuebuf.len, flags, user_data);
|
||||||
}
|
}
|
||||||
if (frame->hd.type != NGHTTP2_HEADERS ||
|
if (frame->hd.type != NGHTTP2_HEADERS ||
|
||||||
frame->headers.cat != NGHTTP2_HCAT_REQUEST) {
|
frame->headers.cat != NGHTTP2_HCAT_REQUEST) {
|
||||||
|
@ -1389,18 +1403,55 @@ int on_header_callback(nghttp2_session *session, const nghttp2_frame *frame,
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stream->header_buffer_size + namelen + valuelen > 64_k) {
|
if (stream->header_buffer_size + namebuf.len + valuebuf.len > 64_k) {
|
||||||
hd->submit_rst_stream(stream, NGHTTP2_INTERNAL_ERROR);
|
hd->submit_rst_stream(stream, NGHTTP2_INTERNAL_ERROR);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
stream->header_buffer_size += namelen + valuelen;
|
stream->header_buffer_size += namebuf.len + valuebuf.len;
|
||||||
|
|
||||||
auto token = http2::lookup_token(name, namelen);
|
auto token = http2::lookup_token(namebuf.base, namebuf.len);
|
||||||
|
|
||||||
|
auto &header = stream->header;
|
||||||
|
|
||||||
|
switch (token) {
|
||||||
|
case http2::HD__METHOD:
|
||||||
|
header.method = StringRef{valuebuf.base, valuebuf.len};
|
||||||
|
header.rcbuf.method = value;
|
||||||
|
nghttp2_rcbuf_incref(value);
|
||||||
|
break;
|
||||||
|
case http2::HD__SCHEME:
|
||||||
|
header.scheme = StringRef{valuebuf.base, valuebuf.len};
|
||||||
|
header.rcbuf.scheme = value;
|
||||||
|
nghttp2_rcbuf_incref(value);
|
||||||
|
break;
|
||||||
|
case http2::HD__AUTHORITY:
|
||||||
|
header.authority = StringRef{valuebuf.base, valuebuf.len};
|
||||||
|
header.rcbuf.authority = value;
|
||||||
|
nghttp2_rcbuf_incref(value);
|
||||||
|
break;
|
||||||
|
case http2::HD_HOST:
|
||||||
|
header.host = StringRef{valuebuf.base, valuebuf.len};
|
||||||
|
header.rcbuf.host = value;
|
||||||
|
nghttp2_rcbuf_incref(value);
|
||||||
|
break;
|
||||||
|
case http2::HD__PATH:
|
||||||
|
header.path = StringRef{valuebuf.base, valuebuf.len};
|
||||||
|
header.rcbuf.path = value;
|
||||||
|
nghttp2_rcbuf_incref(value);
|
||||||
|
break;
|
||||||
|
case http2::HD_IF_MODIFIED_SINCE:
|
||||||
|
header.ims = StringRef{valuebuf.base, valuebuf.len};
|
||||||
|
header.rcbuf.ims = value;
|
||||||
|
nghttp2_rcbuf_incref(value);
|
||||||
|
break;
|
||||||
|
case http2::HD_EXPECT:
|
||||||
|
header.expect = StringRef{valuebuf.base, valuebuf.len};
|
||||||
|
header.rcbuf.expect = value;
|
||||||
|
nghttp2_rcbuf_incref(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
http2::index_header(stream->hdidx, token, stream->headers.size());
|
|
||||||
http2::add_header(stream->headers, name, namelen, value, valuelen,
|
|
||||||
flags & NGHTTP2_NV_FLAG_NO_INDEX, token);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
@ -1460,15 +1511,13 @@ int hd_on_frame_recv_callback(nghttp2_session *session,
|
||||||
|
|
||||||
if (frame->headers.cat == NGHTTP2_HCAT_REQUEST) {
|
if (frame->headers.cat == NGHTTP2_HCAT_REQUEST) {
|
||||||
|
|
||||||
auto expect100 =
|
auto expect100 = stream->header.expect;
|
||||||
http2::get_header(stream->hdidx, http2::HD_EXPECT, stream->headers);
|
|
||||||
|
|
||||||
if (expect100 && util::strieq_l("100-continue", expect100->value)) {
|
if (util::strieq_l("100-continue", expect100)) {
|
||||||
hd->submit_non_final_response("100", frame->hd.stream_id);
|
hd->submit_non_final_response("100", frame->hd.stream_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &method = http2::get_header(stream->hdidx, http2::HD__METHOD,
|
auto method = stream->header.method;
|
||||||
stream->headers)->value;
|
|
||||||
if (hd->get_config()->echo_upload &&
|
if (hd->get_config()->echo_upload &&
|
||||||
(method == "POST" || method == "PUT")) {
|
(method == "POST" || method == "PUT")) {
|
||||||
if (!prepare_upload_temp_store(stream, hd)) {
|
if (!prepare_upload_temp_store(stream, hd)) {
|
||||||
|
@ -1697,8 +1746,8 @@ void fill_callback(nghttp2_session_callbacks *callbacks, const Config *config) {
|
||||||
nghttp2_session_callbacks_set_on_data_chunk_recv_callback(
|
nghttp2_session_callbacks_set_on_data_chunk_recv_callback(
|
||||||
callbacks, on_data_chunk_recv_callback);
|
callbacks, on_data_chunk_recv_callback);
|
||||||
|
|
||||||
nghttp2_session_callbacks_set_on_header_callback(callbacks,
|
nghttp2_session_callbacks_set_on_header_callback2(callbacks,
|
||||||
on_header_callback);
|
on_header_callback2);
|
||||||
|
|
||||||
nghttp2_session_callbacks_set_on_begin_headers_callback(
|
nghttp2_session_callbacks_set_on_begin_headers_callback(
|
||||||
callbacks, on_begin_headers_callback);
|
callbacks, on_begin_headers_callback);
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
#include "http2.h"
|
#include "http2.h"
|
||||||
#include "buffer.h"
|
#include "buffer.h"
|
||||||
#include "template.h"
|
#include "template.h"
|
||||||
|
#include "allocator.h"
|
||||||
|
|
||||||
namespace nghttp2 {
|
namespace nghttp2 {
|
||||||
|
|
||||||
|
@ -53,6 +54,7 @@ struct Config {
|
||||||
std::map<std::string, std::vector<std::string>> push;
|
std::map<std::string, std::vector<std::string>> push;
|
||||||
std::map<std::string, std::string> mime_types;
|
std::map<std::string, std::string> mime_types;
|
||||||
Headers trailer;
|
Headers trailer;
|
||||||
|
std::string trailer_names;
|
||||||
std::string htdocs;
|
std::string htdocs;
|
||||||
std::string host;
|
std::string host;
|
||||||
std::string private_key_file;
|
std::string private_key_file;
|
||||||
|
@ -111,8 +113,29 @@ struct FileEntry {
|
||||||
bool stale;
|
bool stale;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct RequestHeader {
|
||||||
|
StringRef method;
|
||||||
|
StringRef scheme;
|
||||||
|
StringRef authority;
|
||||||
|
StringRef host;
|
||||||
|
StringRef path;
|
||||||
|
StringRef ims;
|
||||||
|
StringRef expect;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
nghttp2_rcbuf *method;
|
||||||
|
nghttp2_rcbuf *scheme;
|
||||||
|
nghttp2_rcbuf *authority;
|
||||||
|
nghttp2_rcbuf *host;
|
||||||
|
nghttp2_rcbuf *path;
|
||||||
|
nghttp2_rcbuf *ims;
|
||||||
|
nghttp2_rcbuf *expect;
|
||||||
|
} rcbuf;
|
||||||
|
};
|
||||||
|
|
||||||
struct Stream {
|
struct Stream {
|
||||||
Headers headers;
|
BlockAllocator balloc;
|
||||||
|
RequestHeader header;
|
||||||
Http2Handler *handler;
|
Http2Handler *handler;
|
||||||
FileEntry *file_ent;
|
FileEntry *file_ent;
|
||||||
ev_timer rtimer;
|
ev_timer rtimer;
|
||||||
|
@ -123,7 +146,6 @@ struct Stream {
|
||||||
// headers.
|
// headers.
|
||||||
size_t header_buffer_size;
|
size_t header_buffer_size;
|
||||||
int32_t stream_id;
|
int32_t stream_id;
|
||||||
http2::HeaderIndex hdidx;
|
|
||||||
bool echo_upload;
|
bool echo_upload;
|
||||||
Stream(Http2Handler *handler, int32_t stream_id);
|
Stream(Http2Handler *handler, int32_t stream_id);
|
||||||
~Stream();
|
~Stream();
|
||||||
|
@ -143,20 +165,21 @@ public:
|
||||||
int connection_made();
|
int connection_made();
|
||||||
int verify_npn_result();
|
int verify_npn_result();
|
||||||
|
|
||||||
int submit_file_response(const std::string &status, Stream *stream,
|
int submit_file_response(const StringRef &status, Stream *stream,
|
||||||
time_t last_modified, off_t file_length,
|
time_t last_modified, off_t file_length,
|
||||||
const std::string *content_type,
|
const std::string *content_type,
|
||||||
nghttp2_data_provider *data_prd);
|
nghttp2_data_provider *data_prd);
|
||||||
|
|
||||||
int submit_response(const std::string &status, int32_t stream_id,
|
int submit_response(const StringRef &status, int32_t stream_id,
|
||||||
nghttp2_data_provider *data_prd);
|
nghttp2_data_provider *data_prd);
|
||||||
|
|
||||||
int submit_response(const std::string &status, int32_t stream_id,
|
int submit_response(const StringRef &status, int32_t stream_id,
|
||||||
const Headers &headers, nghttp2_data_provider *data_prd);
|
const HeaderRefs &headers,
|
||||||
|
nghttp2_data_provider *data_prd);
|
||||||
|
|
||||||
int submit_non_final_response(const std::string &status, int32_t stream_id);
|
int submit_non_final_response(const std::string &status, int32_t stream_id);
|
||||||
|
|
||||||
int submit_push_promise(Stream *stream, const std::string &push_path);
|
int submit_push_promise(Stream *stream, const StringRef &push_path);
|
||||||
|
|
||||||
int submit_rst_stream(Stream *stream, uint32_t error_code);
|
int submit_rst_stream(Stream *stream, uint32_t error_code);
|
||||||
|
|
||||||
|
|
|
@ -420,6 +420,15 @@ int main(int argc, char **argv) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto &trailer_names = config.trailer_names;
|
||||||
|
for (auto &h : config.trailer) {
|
||||||
|
trailer_names += h.name;
|
||||||
|
trailer_names += ", ";
|
||||||
|
}
|
||||||
|
if (trailer_names.size() >= 2) {
|
||||||
|
trailer_names.resize(trailer_names.size() - 2);
|
||||||
|
}
|
||||||
|
|
||||||
set_color_output(color || isatty(fileno(stdout)));
|
set_color_output(color || isatty(fileno(stdout)));
|
||||||
|
|
||||||
struct sigaction act {};
|
struct sigaction act {};
|
||||||
|
|
22
src/util.cc
22
src/util.cc
|
@ -1363,6 +1363,28 @@ int read_mime_types(std::map<std::string, std::string> &res,
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StringRef percent_decode(BlockAllocator &balloc, const StringRef &src) {
|
||||||
|
auto iov = make_byte_ref(balloc, src.size() * 3 + 1);
|
||||||
|
auto p = iov.base;
|
||||||
|
for (auto first = std::begin(src); first != std::end(src); ++first) {
|
||||||
|
if (*first != '%') {
|
||||||
|
*p++ = *first;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first + 1 != std::end(src) && first + 2 != std::end(src) &&
|
||||||
|
is_hex_digit(*(first + 1)) && is_hex_digit(*(first + 2))) {
|
||||||
|
*p++ = (hex_to_uint(*(first + 1)) << 4) + hex_to_uint(*(first + 2));
|
||||||
|
first += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
*p++ = *first;
|
||||||
|
}
|
||||||
|
*p = '\0';
|
||||||
|
return StringRef{iov.base, p};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace util
|
} // namespace util
|
||||||
|
|
||||||
} // namespace nghttp2
|
} // namespace nghttp2
|
||||||
|
|
12
src/util.h
12
src/util.h
|
@ -123,6 +123,8 @@ std::string percent_decode(InputIt first, InputIt last) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StringRef percent_decode(BlockAllocator &balloc, const StringRef &src);
|
||||||
|
|
||||||
// Percent encode |target| if character is not in token or '%'.
|
// Percent encode |target| if character is not in token or '%'.
|
||||||
std::string percent_encode_token(const std::string &target);
|
std::string percent_encode_token(const std::string &target);
|
||||||
|
|
||||||
|
@ -244,6 +246,11 @@ inline bool ends_with(const std::string &a, const std::string &b) {
|
||||||
return ends_with(std::begin(a), std::end(a), std::begin(b), std::end(b));
|
return ends_with(std::begin(a), std::end(a), std::begin(b), std::end(b));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename CharT, size_t N>
|
||||||
|
bool ends_with_l(const StringRef &a, const CharT(&b)[N]) {
|
||||||
|
return ends_with(std::begin(a), std::end(a), b, b + N - 1);
|
||||||
|
}
|
||||||
|
|
||||||
template <typename InputIterator1, typename InputIterator2>
|
template <typename InputIterator1, typename InputIterator2>
|
||||||
bool iends_with(InputIterator1 first1, InputIterator1 last1,
|
bool iends_with(InputIterator1 first1, InputIterator1 last1,
|
||||||
InputIterator2 first2, InputIterator2 last2) {
|
InputIterator2 first2, InputIterator2 last2) {
|
||||||
|
@ -356,6 +363,11 @@ bool streq_l(const CharT(&a)[N], const std::string &b) {
|
||||||
return streq(a, N - 1, std::begin(b), b.size());
|
return streq(a, N - 1, std::begin(b), b.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename CharT, size_t N>
|
||||||
|
bool streq_l(const CharT(&a)[N], const StringRef &b) {
|
||||||
|
return streq(a, N - 1, std::begin(b), b.size());
|
||||||
|
}
|
||||||
|
|
||||||
bool strifind(const char *a, const char *b);
|
bool strifind(const char *a, const char *b);
|
||||||
|
|
||||||
template <typename InputIt> void inp_strlower(InputIt first, InputIt last) {
|
template <typename InputIt> void inp_strlower(InputIt first, InputIt last) {
|
||||||
|
|
|
@ -157,6 +157,15 @@ void test_util_percent_decode(void) {
|
||||||
std::string s = "%66%";
|
std::string s = "%66%";
|
||||||
CU_ASSERT("f%" == util::percent_decode(std::begin(s), std::end(s)));
|
CU_ASSERT("f%" == util::percent_decode(std::begin(s), std::end(s)));
|
||||||
}
|
}
|
||||||
|
BlockAllocator balloc(1024, 1024);
|
||||||
|
|
||||||
|
CU_ASSERT("foobar" == util::percent_decode(
|
||||||
|
balloc, StringRef::from_lit("%66%6F%6f%62%61%72")));
|
||||||
|
|
||||||
|
CU_ASSERT("f%6" ==
|
||||||
|
util::percent_decode(balloc, StringRef::from_lit("%66%6")));
|
||||||
|
|
||||||
|
CU_ASSERT("f%" == util::percent_decode(balloc, StringRef::from_lit("%66%")));
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_util_quote_string(void) {
|
void test_util_quote_string(void) {
|
||||||
|
|
Loading…
Reference in New Issue