Handle percent encoding
This commit is contained in:
@@ -1,18 +1,102 @@
|
||||
#include "api_url_parser.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <string_view>
|
||||
|
||||
namespace {
|
||||
|
||||
// A simplified helper to split a string_view, similar to std::string::find.
|
||||
// Returns a pair of views, the part before the delimiter and the part after.
|
||||
std::pair<std::string_view, std::string_view> split_view(std::string_view view,
|
||||
char delimiter) {
|
||||
size_t pos = view.find(delimiter);
|
||||
if (pos == std::string_view::npos) {
|
||||
return {view, ""};
|
||||
// RFC 3986 hex digit to value conversion
|
||||
// Returns -1 for invalid hex digits
|
||||
int hex_digit_to_value(char c) {
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0';
|
||||
if (c >= 'A' && c <= 'F')
|
||||
return c - 'A' + 10;
|
||||
if (c >= 'a' && c <= 'f')
|
||||
return c - 'a' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Decode percent-encoded sequence at src
|
||||
// Returns decoded byte value, or -1 for malformed encoding
|
||||
int decode_percent_sequence(const char *src) {
|
||||
if (src[0] != '%')
|
||||
return -1;
|
||||
|
||||
int high = hex_digit_to_value(src[1]);
|
||||
int low = hex_digit_to_value(src[2]);
|
||||
|
||||
if (high == -1 || low == -1)
|
||||
return -1;
|
||||
|
||||
return (high << 4) | low;
|
||||
}
|
||||
|
||||
// Decode RFC 3986 percent-encoding in place (for path segments)
|
||||
// Returns new length, or -1 for malformed encoding
|
||||
int decode_path_segment(char *data, int length) {
|
||||
char *write_pos = data;
|
||||
const char *read_pos = data;
|
||||
const char *end = data + length;
|
||||
|
||||
while (read_pos < end) {
|
||||
if (*read_pos == '%') {
|
||||
if (read_pos + 2 >= end)
|
||||
return -1; // Incomplete sequence
|
||||
|
||||
int decoded = decode_percent_sequence(read_pos);
|
||||
if (decoded == -1)
|
||||
return -1; // Malformed sequence
|
||||
|
||||
*write_pos++ = static_cast<char>(decoded);
|
||||
read_pos += 3;
|
||||
} else {
|
||||
*write_pos++ = *read_pos++;
|
||||
}
|
||||
}
|
||||
return {view.substr(0, pos), view.substr(pos + 1)};
|
||||
|
||||
return static_cast<int>(write_pos - data);
|
||||
}
|
||||
|
||||
// Decode application/x-www-form-urlencoded in place (for query parameters)
|
||||
// Handles + → space conversion, then percent-decoding
|
||||
// Returns new length, or -1 for malformed encoding
|
||||
int decode_query_value(char *data, int length) {
|
||||
char *write_pos = data;
|
||||
const char *read_pos = data;
|
||||
const char *end = data + length;
|
||||
|
||||
while (read_pos < end) {
|
||||
if (*read_pos == '+') {
|
||||
*write_pos++ = ' ';
|
||||
read_pos++;
|
||||
} else if (*read_pos == '%') {
|
||||
if (read_pos + 2 >= end)
|
||||
return -1; // Incomplete sequence
|
||||
|
||||
int decoded = decode_percent_sequence(read_pos);
|
||||
if (decoded == -1)
|
||||
return -1; // Malformed sequence
|
||||
|
||||
*write_pos++ = static_cast<char>(decoded);
|
||||
read_pos += 3;
|
||||
} else {
|
||||
*write_pos++ = *read_pos++;
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<int>(write_pos - data);
|
||||
}
|
||||
|
||||
// A simplified helper to find a delimiter in a buffer
|
||||
// Returns the position of the delimiter, or -1 if not found
|
||||
int find_delimiter(const char *data, int length, char delimiter) {
|
||||
for (int i = 0; i < length; ++i) {
|
||||
if (data[i] == delimiter) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Maps a string parameter key to its corresponding enum value.
|
||||
@@ -23,93 +107,144 @@ to_api_parameter_key(std::string_view key) {
|
||||
return ApiParameterKey::RequestId;
|
||||
if (key == "min_version")
|
||||
return ApiParameterKey::MinVersion;
|
||||
if (key == "wait")
|
||||
return ApiParameterKey::Wait;
|
||||
if (key == "timeout")
|
||||
return ApiParameterKey::Timeout;
|
||||
if (key == "verbose")
|
||||
return ApiParameterKey::Verbose;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Parses the query string part of a URL.
|
||||
void parse_query_string(std::string_view query_string, RouteMatch &match) {
|
||||
while (!query_string.empty()) {
|
||||
auto [key_value_pair, rest] = split_view(query_string, '&');
|
||||
auto [key, value] = split_view(key_value_pair, '=');
|
||||
// Parses the query string part of a URL (in-place decoding)
|
||||
// Returns ParseResult::Success or ParseResult::MalformedEncoding
|
||||
ParseResult parse_query_string(char *query_data, int query_length,
|
||||
RouteMatch &match) {
|
||||
int pos = 0;
|
||||
|
||||
if (auto key_enum = to_api_parameter_key(key)) {
|
||||
match.params[static_cast<size_t>(*key_enum)] = value;
|
||||
while (pos < query_length) {
|
||||
// Find end of current key=value pair
|
||||
int pair_end = find_delimiter(query_data + pos, query_length - pos, '&');
|
||||
if (pair_end == -1)
|
||||
pair_end = query_length - pos;
|
||||
|
||||
// Find = separator within the pair
|
||||
int eq_pos = find_delimiter(query_data + pos, pair_end, '=');
|
||||
if (eq_pos == -1) {
|
||||
// No value, skip this parameter
|
||||
pos += pair_end + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
query_string = rest;
|
||||
// Decode key and value in place
|
||||
char *key_start = query_data + pos;
|
||||
int key_length = eq_pos;
|
||||
|
||||
char *value_start = query_data + pos + eq_pos + 1;
|
||||
int value_length = pair_end - eq_pos - 1;
|
||||
|
||||
// Decode value (query parameters use form encoding)
|
||||
int decoded_value_length = decode_query_value(value_start, value_length);
|
||||
if (decoded_value_length == -1) {
|
||||
return ParseResult::MalformedEncoding;
|
||||
}
|
||||
|
||||
// Check if this is a parameter we care about
|
||||
std::string_view key_view(key_start, key_length);
|
||||
if (auto key_enum = to_api_parameter_key(key_view)) {
|
||||
match.params[static_cast<int>(*key_enum)] =
|
||||
std::string_view(value_start, decoded_value_length);
|
||||
}
|
||||
|
||||
pos += pair_end + 1;
|
||||
}
|
||||
|
||||
return ParseResult::Success;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RouteMatch ApiUrlParser::parse(std::string_view method, std::string_view url) {
|
||||
RouteMatch result;
|
||||
ParseResult ApiUrlParser::parse(std::string_view method, char *url_data,
|
||||
int url_length, RouteMatch &result) {
|
||||
assert(url_data != nullptr);
|
||||
assert(url_length >= 0);
|
||||
|
||||
auto [path, query] = split_view(url, '?');
|
||||
parse_query_string(query, result);
|
||||
// Find query string separator
|
||||
int query_start = find_delimiter(url_data, url_length, '?');
|
||||
|
||||
char *path_data = url_data;
|
||||
int path_length = (query_start == -1) ? url_length : query_start;
|
||||
|
||||
char *query_data = (query_start == -1) ? nullptr : url_data + query_start + 1;
|
||||
int query_length = (query_start == -1) ? 0 : url_length - query_start - 1;
|
||||
|
||||
// Parse and decode query string first
|
||||
if (query_data && query_length > 0) {
|
||||
ParseResult query_result =
|
||||
parse_query_string(query_data, query_length, result);
|
||||
if (query_result != ParseResult::Success) {
|
||||
return query_result;
|
||||
}
|
||||
}
|
||||
|
||||
// Decode path segment (RFC 3986 percent-decoding)
|
||||
int decoded_path_length = decode_path_segment(path_data, path_length);
|
||||
if (decoded_path_length == -1) {
|
||||
return ParseResult::MalformedEncoding;
|
||||
}
|
||||
|
||||
std::string_view path(path_data, decoded_path_length);
|
||||
|
||||
// Route matching with decoded path
|
||||
if (method == "GET") {
|
||||
if (path == "/v1/version") {
|
||||
result.route = HttpRoute::GetVersion;
|
||||
return result;
|
||||
return ParseResult::Success;
|
||||
}
|
||||
if (path == "/v1/subscribe") {
|
||||
result.route = HttpRoute::GetSubscribe;
|
||||
return result;
|
||||
return ParseResult::Success;
|
||||
}
|
||||
if (path == "/v1/status") {
|
||||
result.route = HttpRoute::GetStatus;
|
||||
return result;
|
||||
return ParseResult::Success;
|
||||
}
|
||||
if (path.starts_with("/v1/retention")) {
|
||||
result.route = HttpRoute::GetRetention;
|
||||
// Note: This matches both /v1/retention and /v1/retention/{id}
|
||||
// The handler will need to check for the presence of the PolicyId param.
|
||||
if (path.length() > 13) { // length of "/v1/retention"
|
||||
std::string_view policy_id =
|
||||
path.substr(14); // length of "/v1/retention/"
|
||||
if (!policy_id.empty()) {
|
||||
result.params[static_cast<size_t>(ApiParameterKey::PolicyId)] =
|
||||
result.params[static_cast<int>(ApiParameterKey::PolicyId)] =
|
||||
policy_id;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return ParseResult::Success;
|
||||
}
|
||||
if (path == "/metrics") {
|
||||
result.route = HttpRoute::GetMetrics;
|
||||
return result;
|
||||
return ParseResult::Success;
|
||||
}
|
||||
if (path == "/ok") {
|
||||
result.route = HttpRoute::GetOk;
|
||||
return result;
|
||||
return ParseResult::Success;
|
||||
}
|
||||
} else if (method == "POST") {
|
||||
if (path == "/v1/commit") {
|
||||
result.route = HttpRoute::PostCommit;
|
||||
return result;
|
||||
return ParseResult::Success;
|
||||
}
|
||||
} else if (method == "PUT") {
|
||||
if (path.starts_with("/v1/retention/")) {
|
||||
result.route = HttpRoute::PutRetention;
|
||||
std::string_view policy_id = path.substr(14);
|
||||
result.params[static_cast<size_t>(ApiParameterKey::PolicyId)] = policy_id;
|
||||
return result;
|
||||
result.params[static_cast<int>(ApiParameterKey::PolicyId)] = policy_id;
|
||||
return ParseResult::Success;
|
||||
}
|
||||
} else if (method == "DELETE") {
|
||||
if (path.starts_with("/v1/retention/")) {
|
||||
result.route = HttpRoute::DeleteRetention;
|
||||
std::string_view policy_id = path.substr(14);
|
||||
result.params[static_cast<size_t>(ApiParameterKey::PolicyId)] = policy_id;
|
||||
return result;
|
||||
result.params[static_cast<int>(ApiParameterKey::PolicyId)] = policy_id;
|
||||
return ParseResult::Success;
|
||||
}
|
||||
}
|
||||
|
||||
result.route = HttpRoute::NotFound;
|
||||
return result;
|
||||
}
|
||||
return ParseResult::Success;
|
||||
}
|
||||
Reference in New Issue
Block a user