Skip to content

Commit 1ea0d23

Browse files
author
me
committed
- added query params
1 parent 400818f commit 1ea0d23

File tree

3 files changed

+170
-14
lines changed

3 files changed

+170
-14
lines changed

examples/unit_tests/message.cpp

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,89 @@
1+
#include <string_view>
2+
#include <vector>
13
#include <http.h>
24
#include "doctest.h"
35

6+
struct url_parsing_test_data
7+
{
8+
std::string_view url;
9+
std::string_view target_expected;
10+
std::vector<std::pair<std::string_view, std::string_view>> query_params_expected;
11+
};
12+
13+
static const url_parsing_test_data test_data[] = {
14+
{
15+
"/search?q=hello+world&lang=en",
16+
"/search",
17+
{{"q", "hello world"}, {"lang", "en"}}
18+
},
19+
{
20+
"/api/data?weird=%26%25%3F&empty=&plus=1%2B1%3D2",
21+
"/api/data",
22+
{{"weird", "&%?"}, {"empty", ""}, {"plus", "1+1=2"}}
23+
},
24+
{
25+
"/docs/space+test?file=name%20with%20spaces.txt&x=1",
26+
"/docs/space+test",
27+
{{"file", "name with spaces.txt"}, {"x", "1"}}
28+
},
29+
{
30+
"/multi?key=value1&key=value2&key=value3",
31+
"/multi",
32+
{{"key", "value1"}, {"key", "value2"}, {"key", "value3"}}
33+
},
34+
{
35+
"/equals?x=1%3D2%3D3",
36+
"/equals",
37+
{{"x", "1=2=3"}}
38+
},
39+
{
40+
"/onlypath",
41+
"/onlypath",
42+
{}
43+
},
44+
{
45+
"/weird?%3Fkey=%3Fvalue&key2=%26%3D",
46+
"/weird",
47+
{{"?key", "?value"}, {"key2", "&="}}
48+
},
49+
{
50+
"/complex+path/with%2Fslashes?q=%2Fthis%2Fis%2Fa%2Ftest",
51+
"/complex+path/with%2Fslashes",
52+
{{"q", "/this/is/a/test"}}
53+
},
54+
{
55+
"/emptykey?=novalue&foo=bar",
56+
"/emptykey",
57+
{{"", "novalue"}, {"foo", "bar"}}
58+
},
59+
{
60+
"/plus+in+path?plus=1+2",
61+
"/plus+in+path",
62+
{{"plus", "1 2"}}
63+
}
64+
};
65+
466
TEST_SUITE("[MESSAGE]")
567
{
68+
TEST_CASE("url parsing")
69+
{
70+
for (auto data : test_data)
71+
{
72+
std::error_code ec{};
73+
std::string target;
74+
std::vector<http::query_param> params;
75+
http::parse_url(data.url, target, params, ec);
76+
REQUIRE(!bool(ec));
77+
REQUIRE(target == data.target_expected);
78+
REQUIRE(params.size() == data.query_params_expected.size());
79+
for (size_t i = 0 ; i < params.size() ; ++i)
80+
{
81+
REQUIRE(params[i].key == data.query_params_expected[i].first);
82+
REQUIRE(params[i].val == data.query_params_expected[i].second);
83+
}
84+
}
85+
}
86+
687
TEST_CASE("serialise & parse bad requests")
788
{
889
http::request req;
@@ -47,7 +128,7 @@ TEST_SUITE("[MESSAGE]")
47128
{
48129
http::request req0;
49130
req0.verb = http::GET;
50-
req0.uri = "/path/to/resource/with%20spaces?q=search+term&empty=&weird=%26%25%3F";
131+
req0.uri = "/path/to/resource/with+spaces";
51132
req0.http_version_minor = 1;
52133
req0.add_header(http::host, "www.example.com:8080");
53134
req0.add_header(http::user_agent, "CustomTestAgent/7.4.2 (compatible; FancyBot/1.0; +https://example.com/bot)");
@@ -62,6 +143,9 @@ TEST_SUITE("[MESSAGE]")
62143
req0.add_header(http::pragma, "no-cache");
63144
req0.add_header(http::content_type, "application/json; charset=\"utf-8\"");
64145
req0.content = "{\"message\": \"This is a test body with some content.\"}";
146+
req0.params.push_back({"q", "search term"});
147+
req0.params.push_back({"empty", ""});
148+
req0.params.push_back({"weird", "&%?"});
65149
REQUIRE(req0.keep_alive());
66150
REQUIRE(req0.is_websocket_req());
67151

@@ -112,6 +196,12 @@ TEST_SUITE("[MESSAGE]")
112196
REQUIRE(req0.verb == req1.verb);
113197
REQUIRE(req0.http_version_minor == req1.http_version_minor);
114198
REQUIRE(req0.uri == req1.uri);
199+
REQUIRE(req0.params.size() == req1.params.size());
200+
for (size_t i = 0 ; i < req0.params.size() ; ++i)
201+
{
202+
REQUIRE(req0.params[i].key == req1.params[i].key);
203+
REQUIRE(req0.params[i].val == req1.params[i].val);
204+
}
115205
REQUIRE(req0.headers.size() == req1.headers.size());
116206
for (size_t i = 0 ; i < req0.headers.size() ; ++i)
117207
{

src/http.cpp

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,7 @@ namespace http
817817
static char from_hex(char ch) {return std::isdigit(ch) ? ch - '0' : std::tolower(ch) - 'a' + 10;}
818818
static char to_hex(char code) {constexpr char hex[] = "0123456789abcdef"; return hex[code & 15];}
819819

820-
std::string url_encode(std::string_view str)
820+
static std::string url_encode(std::string_view str)
821821
{
822822
std::string ret(str.size()*3+1, '\0');
823823
char* buf = &ret[0];
@@ -836,7 +836,7 @@ namespace http
836836
return ret;
837837
}
838838

839-
std::string url_decode(std::string_view str)
839+
static std::string url_decode(std::string_view str)
840840
{
841841
std::string ret(str.size() + 1, '\0');
842842
char* buf = &ret[0];
@@ -858,6 +858,45 @@ namespace http
858858
return ret;
859859
}
860860

861+
//----------------------------------------------------------------------------------------------------------------
862+
863+
void parse_url(std::string_view url, std::string& target, std::vector<query_param>& params, std::error_code& ec)
864+
{
865+
// Find target
866+
auto end = url.find_first_of('?');
867+
target = url.substr(0, end);
868+
auto pos = end + 1;
869+
if (end == std::string::npos || pos >= url.size())
870+
return;
871+
872+
const auto extract_kv = [&](std::string_view query)
873+
{
874+
const auto key_end = query.find_first_of('=');
875+
876+
if (key_end == std::string::npos || key_end+1 > query.size())
877+
{
878+
ec = make_error_code(http_read_bad_query_string);
879+
}
880+
else
881+
{
882+
const std::string_view key = query.substr(0, key_end);
883+
const std::string_view val = query.substr(key_end+1);
884+
params.push_back({url_decode(key), url_decode(val)});
885+
pos = end + 1;
886+
}
887+
};
888+
889+
// Find params
890+
while ((end = url.find_first_of('&', pos)) != std::string::npos && !ec)
891+
{
892+
extract_kv(url.substr(pos, end-pos));
893+
pos = end + 1;
894+
}
895+
896+
if (!ec)
897+
extract_kv(url.substr(pos));
898+
}
899+
861900
//----------------------------------------------------------------------------------------------------------------
862901

863902
bool header::contains_value(std::string_view v) const
@@ -1037,7 +1076,7 @@ namespace http
10371076
if (end != std::string_view::npos)
10381077
{
10391078
if constexpr (std::is_same_v<Message, request>)
1040-
msg.uri = buf.substr(0, end);
1079+
parse_url(buf.substr(0, end), msg.uri, msg.params, ec);
10411080

10421081
state = http_version;
10431082
buf.erase(begin(buf), begin(buf) + end + 1);
@@ -1296,8 +1335,25 @@ namespace http
12961335
return;
12971336
}
12981337

1338+
// Serialize URL
1339+
std::string uri_encoded = req.uri;
1340+
1341+
if (!req.params.empty())
1342+
{
1343+
uri_encoded += '?';
1344+
1345+
for (size_t i = 0 ; i < req.params.size() ; ++i)
1346+
{
1347+
const std::string key_encoded = url_encode(req.params[i].key);
1348+
const std::string val_encoded = url_encode(req.params[i].val);
1349+
uri_encoded += key_encoded + '=' + val_encoded;
1350+
if (i < (req.params.size() - 1))
1351+
uri_encoded += '&';
1352+
}
1353+
}
1354+
12991355
// Set request line
1300-
const std::string status_str = format("%s %s HTTP/1.%i\r\n", verb_label(req.verb).data(), req.uri.c_str(), req.http_version_minor);
1356+
const std::string status_str = format("%s %s HTTP/1.%i\r\n", verb_label(req.verb).data(), uri_encoded.c_str(), req.http_version_minor);
13011357

13021358
// Add default connection string if empty
13031359
if (!contains(req.headers, field::connection))
@@ -1361,8 +1417,9 @@ namespace http
13611417
case http_read_header_line_too_big: return "HTTP header line is too big";
13621418
case http_read_bad_method: return "HTTP request method bad";
13631419
case http_read_unsupported_http_version: return "HTTP version either bad or unsupported";
1364-
case http_read_header_kv_delimiter_not_found: return "Missing delimiter in HTTP header line";
13651420
case http_read_bad_status: return "HTTP status code bad";
1421+
case http_read_bad_query_string: return "Bad query string formatting";
1422+
case http_read_header_kv_delimiter_not_found: return "Missing delimiter in HTTP header line";
13661423
case http_read_header_unsupported_field: return "HTTP header field unsupported";
13671424
case http_write_unsupported_http_version: return "HTTP message contains bad or unsupported http minor version";
13681425
case http_write_request_bad_verb: return "HTTP request contains bad verb";

src/http.h

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -513,8 +513,11 @@ namespace http
513513

514514
//----------------------------------------------------------------------------------------------------------------
515515

516-
std::string url_encode(std::string_view str);
517-
std::string url_decode(std::string_view str);
516+
struct query_param
517+
{
518+
std::string key;
519+
std::string val;
520+
};
518521

519522
//----------------------------------------------------------------------------------------------------------------
520523

@@ -529,11 +532,12 @@ namespace http
529532

530533
struct request
531534
{
532-
verb_type verb{UNKNOWN_VERB};
533-
int http_version_minor{-1};
534-
std::string uri;
535-
std::vector<header> headers;
536-
std::string content;
535+
verb_type verb{UNKNOWN_VERB};
536+
int http_version_minor{-1};
537+
std::string uri;
538+
std::vector<query_param> params;
539+
std::vector<header> headers;
540+
std::string content;
537541

538542
void clear();
539543
void add_header(field f, std::string_view value);
@@ -560,7 +564,11 @@ namespace http
560564
};
561565

562566
//----------------------------------------------------------------------------------------------------------------
563-
567+
568+
void parse_url(std::string_view url, std::string& target, std::vector<query_param>& params, std::error_code& ec);
569+
570+
//----------------------------------------------------------------------------------------------------------------
571+
564572
template<class Message>
565573
class parser
566574
{
@@ -598,6 +606,7 @@ namespace http
598606
http_read_bad_method,
599607
http_read_unsupported_http_version,
600608
http_read_bad_status,
609+
http_read_bad_query_string,
601610
http_read_header_kv_delimiter_not_found,
602611
http_read_header_unsupported_field,
603612
http_write_unsupported_http_version,

0 commit comments

Comments
 (0)