Skip to content

Commit 9b117c4

Browse files
author
Ruslan Nigmatullin
committed
Support full decoding of reserved expansions
1 parent b48d8aa commit 9b117c4

File tree

2 files changed

+59
-7
lines changed

2 files changed

+59
-7
lines changed

src/include/grpc_transcoding/path_matcher.h

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class PathMatcher {
8282
// The info associated with each method. The path matcher nodes
8383
// will hold pointers to MethodData objects in this vector.
8484
std::vector<std::unique_ptr<MethodData>> methods_;
85+
bool fully_decode_reserved_expansion_;
8586

8687
private:
8788
friend class PathMatcherBuilder<Method>;
@@ -113,6 +114,16 @@ class PathMatcherBuilder {
113114
bool Register(const std::string& http_method, const std::string& path,
114115
const std::string& body_field_path, Method method);
115116

117+
// When set to true, URL path parameters will be fully URI-decoded except in
118+
// cases of single segment matches in reserved expansion, where "%2F" will be
119+
// left encoded.
120+
//
121+
// The default behavior is to not decode RFC 6570 reserved characters in multi
122+
// segment matches.
123+
void SetFullyDecodeReservedExpansion(bool value) {
124+
fully_decode_reserved_expansion_ = value;
125+
}
126+
116127
// Returns a unique_ptr to a thread safe PathMatcher that contains all
117128
// registered path-WrapperGraph pairs. Note the PathMatchBuilder instance
118129
// will be moved so cannot use after invoking Build().
@@ -133,6 +144,7 @@ class PathMatcherBuilder {
133144
std::unordered_set<std::string> custom_verbs_;
134145
typedef typename PathMatcher<Method>::MethodData MethodData;
135146
std::vector<std::unique_ptr<MethodData>> methods_;
147+
bool fully_decode_reserved_expansion_;
136148

137149
friend class PathMatcher<Method>;
138150
};
@@ -260,7 +272,8 @@ std::string UrlUnescapeString(const std::string& part,
260272
template <class VariableBinding>
261273
void ExtractBindingsFromPath(const std::vector<HttpTemplate::Variable>& vars,
262274
const std::vector<std::string>& parts,
263-
std::vector<VariableBinding>* bindings) {
275+
std::vector<VariableBinding>* bindings,
276+
const bool fully_decode_reserved_expansion) {
264277
for (const auto& var : vars) {
265278
// Determine the subpath bound to the variable based on the
266279
// [start_segment, end_segment) segment range of the variable.
@@ -281,7 +294,8 @@ void ExtractBindingsFromPath(const std::vector<HttpTemplate::Variable>& vars,
281294
// Joins parts with "/" to form a path string.
282295
for (size_t i = var.start_segment; i < end_segment; ++i) {
283296
// For multipart matches only unescape non-reserved characters.
284-
binding.value += UrlUnescapeString(parts[i], !is_multipart);
297+
binding.value += UrlUnescapeString(
298+
parts[i], fully_decode_reserved_expansion || !is_multipart);
285299
if (i < end_segment - 1) {
286300
binding.value += "/";
287301
}
@@ -389,7 +403,9 @@ template <class Method>
389403
PathMatcher<Method>::PathMatcher(PathMatcherBuilder<Method>&& builder)
390404
: root_ptr_(std::move(builder.root_ptr_)),
391405
custom_verbs_(std::move(builder.custom_verbs_)),
392-
methods_(std::move(builder.methods_)) {}
406+
methods_(std::move(builder.methods_)),
407+
fully_decode_reserved_expansion_(
408+
builder.fully_decode_reserved_expansion_) {}
393409

394410
// Lookup is a wrapper method for the recursive node Lookup. First, the wrapper
395411
// splits the request path into slash-separated path parts. Next, the method
@@ -424,7 +440,8 @@ Method PathMatcher<Method>::Lookup(
424440
MethodData* method_data = reinterpret_cast<MethodData*>(lookup_result.data);
425441
if (variable_bindings != nullptr) {
426442
variable_bindings->clear();
427-
ExtractBindingsFromPath(method_data->variables, parts, variable_bindings);
443+
ExtractBindingsFromPath(method_data->variables, parts, variable_bindings,
444+
fully_decode_reserved_expansion_);
428445
ExtractBindingsFromQueryParameters(
429446
query_params, method_data->system_query_parameter_names,
430447
variable_bindings);
@@ -461,7 +478,8 @@ Method PathMatcher<Method>::Lookup(const std::string& http_method,
461478
// Initializes the builder with a root Path Segment
462479
template <class Method>
463480
PathMatcherBuilder<Method>::PathMatcherBuilder()
464-
: root_ptr_(new PathMatcherNode()) {}
481+
: root_ptr_(new PathMatcherNode()),
482+
fully_decode_reserved_expansion_(false) {}
465483

466484
template <class Method>
467485
PathMatcherPtr<Method> PathMatcherBuilder<Method>::Build() {

test/path_matcher_test.cc

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ std::string FieldPathToString(const FieldPath& fp) {
6464
return s;
6565
}
6666

67-
} // namespace
68-
6967
std::ostream& operator<<(std::ostream& os, const Binding& b) {
7068
return os << "{ " << FieldPathToString(b.field_path) << "=" << b.value << "}";
7169
}
@@ -77,6 +75,8 @@ std::ostream& operator<<(std::ostream& os, const Bindings& bindings) {
7775
return os;
7876
}
7977

78+
} // namespace
79+
8080
namespace {
8181

8282
class PathMatcherTest : public ::testing::Test {
@@ -116,6 +116,10 @@ class PathMatcherTest : public ::testing::Test {
116116

117117
MethodInfo* AddGetPath(std::string path) { return AddPath("GET", path); }
118118

119+
void SetFullyDecodeReservedExpansion(bool value) {
120+
builder_.SetFullyDecodeReservedExpansion(value);
121+
}
122+
119123
void Build() { matcher_ = builder_.Build(); }
120124

121125
MethodInfo* LookupWithBodyFieldPath(std::string method, std::string path,
@@ -374,6 +378,26 @@ TEST_F(PathMatcherTest, OnlyUnreservedCharsAreUnescapedForMultiSegmentMatch) {
374378
bindings);
375379
}
376380

381+
TEST_F(PathMatcherTest,
382+
OnlyUnreservedCharsAreUnescapedForMultiSegmentMatchFullyDecode) {
383+
MethodInfo* a__c = AddGetPath("/a/{x=**}/c");
384+
SetFullyDecodeReservedExpansion(true);
385+
Build();
386+
387+
EXPECT_NE(nullptr, a__c);
388+
389+
Bindings bindings;
390+
EXPECT_EQ(
391+
Lookup("GET",
392+
"/a/%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D/c",
393+
&bindings),
394+
a__c);
395+
396+
// All %XX are reserved characters, they should be decoded.
397+
EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "!#$&'()*+,/:;=?@[]"}}),
398+
bindings);
399+
}
400+
377401
TEST_F(PathMatcherTest, VariableBindingsWithCustomVerb) {
378402
MethodInfo* a_verb = AddGetPath("/a/{y=*}:verb");
379403
MethodInfo* ad__verb = AddGetPath("/a/{y=d/**}:verb");
@@ -760,6 +784,16 @@ TEST_F(PathMatcherTest, VariableBindingsWithQueryParamsAndSystemParams) {
760784
bindings);
761785
}
762786

787+
TEST(UrlUnescapeTest, SpecialCharacters) {
788+
EXPECT_EQ(UrlUnescapeString("%2523", true), "%23");
789+
EXPECT_EQ(UrlUnescapeString("%23", true), "#");
790+
EXPECT_EQ(UrlUnescapeString("%2523", false), "%23");
791+
EXPECT_EQ(UrlUnescapeString("%23", false), "%23");
792+
793+
EXPECT_EQ(UrlUnescapeString("%252525", true), "%2525");
794+
EXPECT_EQ(UrlUnescapeString("%252525", false), "%2525");
795+
}
796+
763797
} // namespace
764798

765799
} // namespace transcoding

0 commit comments

Comments
 (0)