From e0a25745cfbc3fc52f0d261a58cb6394586426c4 Mon Sep 17 00:00:00 2001 From: yhirose Date: Wed, 3 Oct 2012 23:47:59 -0400 Subject: [PATCH] Added post method support. --- httplib.h | 165 +++++++++++++++++++++++++++++++++++---------------- test/test.cc | 100 ++++++++++++++++++++++++++----- 2 files changed, 198 insertions(+), 67 deletions(-) diff --git a/httplib.h b/httplib.h index 0b6e9a9..e2443b3 100644 --- a/httplib.h +++ b/httplib.h @@ -53,11 +53,14 @@ struct Request { std::string url; MultiMap headers; std::string body; - Map query; - Match match; + Map params; + Match matches; bool has_header(const char* key) const; std::string get_header_value(const char* key) const; + void set_header(const char* key, const char* val); + + bool has_param(const char* key) const; }; struct Response { @@ -99,7 +102,6 @@ private: void process_request(FILE* fp_read, FILE* fp_write); bool read_request_line(FILE* fp, Request& req); - void write_response(FILE* fp, const Response& res); void dispatch_request(Connection& c, Handlers& handlers); const std::string host_; @@ -118,6 +120,8 @@ public: ~Client(); bool get(const char* url, Response& res); + bool post(const char* url, const std::string& body, const char* content_type, Response& res); + bool send(const Request& req, Response& res); private: bool read_response_line(FILE* fp, Response& res); @@ -319,6 +323,64 @@ bool read_content(T& x, FILE* fp) return true; } +template +inline void write_headers(FILE* fp, const T& x) +{ + fprintf(fp, "Connection: close\r\n"); + + for (auto it = x.headers.begin(); it != x.headers.end(); ++it) { + if (it->first != "Content-Type" && it->first != "Content-Length") { + fprintf(fp, "%s: %s\r\n", it->first.c_str(), it->second.c_str()); + } + } + + if (!x.body.empty()) { + auto content_type = get_header_value_text(x.headers, "Content-Type", "text/plain"); + fprintf(fp, "Content-Type: %s\r\n", content_type); + fprintf(fp, "Content-Length: %ld\r\n", x.body.size()); + } + + fprintf(fp, "\r\n"); +} + +inline void write_response(FILE* fp, const Response& res) +{ + fprintf(fp, "HTTP/1.0 %d %s\r\n", res.status, status_message(res.status)); + + write_headers(fp, res); + + if (!res.body.empty()) { + fprintf(fp, "%s", res.body.c_str()); + } +} + +inline void write_request(FILE* fp, const Request& req) +{ + fprintf(fp, "%s %s HTTP/1.0\r\n", req.method.c_str(), req.url.c_str()); + + write_headers(fp, req); + + if (!req.body.empty()) { + fprintf(fp, "%s", req.body.c_str()); + } +} + +inline void parse_query_text(const char* b, const char* e, Map& params) +{ + split(b, e, '&', [&](const char* b, const char* e) { + std::string key; + std::string val; + split(b, e, '=', [&](const char* b, const char* e) { + if (key.empty()) { + key.assign(b, e); + } else { + val.assign(b, e); + } + }); + params[key] = val; + }); +} + // HTTP server implementation inline bool Request::has_header(const char* key) const { @@ -330,6 +392,16 @@ inline std::string Request::get_header_value(const char* key) const return get_header_value_text(headers, key, ""); } +inline void Request::set_header(const char* key, const char* val) +{ + headers.insert(std::make_pair(key, val)); +} + +inline bool Request::has_param(const char* key) const +{ + return params.find(key) != params.end(); +} + inline bool Response::has_header(const char* key) const { return headers.find(key) != headers.end(); @@ -452,18 +524,7 @@ inline bool Server::read_request_line(FILE* fp, Request& req) auto len = std::distance(m[3].first, m[3].second); if (len > 0) { const auto& pos = m[3]; - split(pos.first, pos.second, '&', [&](const char* b, const char* e) { - std::string key; - std::string val; - split(b, e, '=', [&](const char* b, const char* e) { - if (key.empty()) { - key.assign(b, e); - } else { - val.assign(b, e); - } - }); - req.query[key] = val; - }); + parse_query_text(pos.first, pos.second, req.params); } return true; @@ -472,37 +533,13 @@ inline bool Server::read_request_line(FILE* fp, Request& req) return false; } -inline void Server::write_response(FILE* fp, const Response& res) -{ - fprintf(fp, "HTTP/1.0 %d %s\r\n", res.status, status_message(res.status)); - fprintf(fp, "Connection: close\r\n"); - - for (auto it = res.headers.begin(); it != res.headers.end(); ++it) { - if (it->first != "Content-Type" && it->second != "Content-Length") { - fprintf(fp, "%s: %s\r\n", it->first.c_str(), it->second.c_str()); - } - } - - if (!res.body.empty()) { - auto content_type = get_header_value_text(res.headers, "Content-Type", "text/plain"); - fprintf(fp, "Content-Type: %s\r\n", content_type); - fprintf(fp, "Content-Length: %ld\r\n", res.body.size()); - } - - fprintf(fp, "\r\n"); - - if (!res.body.empty()) { - fprintf(fp, "%s", res.body.c_str()); - } -} - inline void Server::dispatch_request(Connection& c, Handlers& handlers) { for (auto it = handlers.begin(); it != handlers.end(); ++it) { const auto& pattern = it->first; const auto& handler = it->second; - if (std::regex_match(c.request.url, c.request.match, pattern)) { + if (std::regex_match(c.request.url, c.request.matches, pattern)) { handler(c); if (!c.response.status) { @@ -516,33 +553,41 @@ inline void Server::dispatch_request(Connection& c, Handlers& handlers) inline void Server::process_request(FILE* fp_read, FILE* fp_write) { Connection c; + auto& req = c.request; + auto& res = c.response; - if (!read_request_line(fp_read, c.request) || - !read_headers(fp_read, c.request.headers)) { + if (!read_request_line(fp_read, req) || + !read_headers(fp_read, req.headers)) { return; } // Routing - c.response.status = 0; + res.status = 0; - if (c.request.method == "GET") { + if (req.method == "GET") { dispatch_request(c, get_handlers_); - } else if (c.request.method == "POST") { - if (!read_content(c.request, fp_read)) { + } else if (req.method == "POST") { + if (!read_content(req, fp_read)) { return; } + if (req.get_header_value("Content-Type") == "application/x-www-form-urlencoded") { + // Parse query text + const char* b = &req.body[0]; + const char* e = &req.body[req.body.size()]; + parse_query_text(b, e, req.params); + } dispatch_request(c, post_handlers_); } - if (!c.response.status) { - c.response.status = 404; + if (!res.status) { + res.status = 404; } - if (400 <= c.response.status && error_handler_) { + if (400 <= res.status && error_handler_) { error_handler_(c); } - write_response(fp_write, c.response); + write_response(fp_write, res); if (logger_) { logger_(c); @@ -586,6 +631,24 @@ inline bool Client::read_response_line(FILE* fp, Response& res) } inline bool Client::get(const char* url, Response& res) +{ + Request req; + req.method = "GET"; + req.url = url; + return send(req, res); +} + +inline bool Client::post(const char* url, const std::string& body, const char* content_type, Response& res) +{ + Request req; + req.method = "POST"; + req.url = url; + req.set_header("Content-Type", content_type); + req.body = body; + return send(req, res); +} + +inline bool Client::send(const Request& req, Response& res) { socket_t sock = create_client_socket(host_.c_str(), port_); if (sock == -1) { @@ -597,7 +660,7 @@ inline bool Client::get(const char* url, Response& res) get_flie_pointers(sock, fp_read, fp_write); // Send request - fprintf(fp_write, "GET %s HTTP/1.0\r\n\r\n", url); + write_request(fp_write, req); fflush(fp_write); if (!read_response_line(fp_read, res) || diff --git a/test/test.cc b/test/test.cc index bbc77ee..4dd2581 100644 --- a/test/test.cc +++ b/test/test.cc @@ -24,9 +24,21 @@ TEST(SplitTest, ParseQueryString) dic[key] = val; }); - ASSERT_EQ("val1", dic["key1"]); - ASSERT_EQ("val2", dic["key2"]); - ASSERT_EQ("val3", dic["key3"]); + EXPECT_EQ("val1", dic["key1"]); + EXPECT_EQ("val2", dic["key2"]); + EXPECT_EQ("val3", dic["key3"]); +} + +TEST(ParseQueryTest, ParseQueryString) +{ + string s = "key1=val1&key2=val2&key3=val3"; + map dic; + + parse_query_text(&s[0], &s[s.size()], dic); + + EXPECT_EQ("val1", dic["key1"]); + EXPECT_EQ("val2", dic["key2"]); + EXPECT_EQ("val3", dic["key3"]); } TEST(SocketTest, OpenClose) @@ -35,7 +47,7 @@ TEST(SocketTest, OpenClose) ASSERT_NE(-1, sock); auto ret = close_server_socket(sock); - ASSERT_EQ(0, ret); + EXPECT_EQ(0, ret); } TEST(GetHeaderValueTest, DefaultValue) @@ -49,7 +61,7 @@ TEST(GetHeaderValueTest, DefaultValueInt) { MultiMap map = {{"Dummy","Dummy"}}; auto val = get_header_value_int(map, "Content-Length", 100); - ASSERT_EQ(100, val); + EXPECT_EQ(100, val); } TEST(GetHeaderValueTest, RegularValue) @@ -63,12 +75,13 @@ TEST(GetHeaderValueTest, RegularValueInt) { MultiMap map = {{"Content-Length","100"}, {"Dummy", "Dummy"}}; auto val = get_header_value_int(map, "Content-Length", 0); - ASSERT_EQ(100, val); + EXPECT_EQ(100, val); } class ServerTest : public ::testing::Test { protected: ServerTest() : svr_(host_, port_) { + persons_["john"] = "programmer"; } virtual void SetUp() { @@ -78,6 +91,24 @@ protected: svr_.get("/", [&](httplib::Connection& c) { c.response.set_redirect("/hi"); }); + svr_.post("/person", [&](httplib::Connection& c) { + const auto& req = c.request; + if (req.has_param("name") && req.has_param("note")) { + persons_[req.params.at("name")] = req.params.at("note"); + } else { + c.response.status = 400; + } + }); + svr_.get("/person/(.*)", [&](httplib::Connection& c) { + const auto& req = c.request; + std::string name = req.matches[1]; + if (persons_.find(name) != persons_.end()) { + auto note = persons_[name]; + c.response.set_content(note, "text/plain"); + } else { + c.response.status = 404; + } + }); f_ = async([&](){ svr_.run(); }); } @@ -86,10 +117,11 @@ protected: f_.get(); } - const char* host_ = "localhost"; - int port_ = 1914; - Server svr_; - std::future f_; + const char* host_ = "localhost"; + int port_ = 1914; + std::map persons_; + Server svr_; + std::future f_; }; TEST_F(ServerTest, GetMethod200) @@ -97,9 +129,9 @@ TEST_F(ServerTest, GetMethod200) Response res; bool ret = Client(host_, port_).get("/hi", res); ASSERT_EQ(true, ret); - ASSERT_EQ(200, res.status); - ASSERT_EQ("text/plain", res.get_header_value("Content-Type")); - ASSERT_EQ("Hello World!", res.body); + EXPECT_EQ(200, res.status); + EXPECT_EQ("text/plain", res.get_header_value("Content-Type")); + EXPECT_EQ("Hello World!", res.body); } TEST_F(ServerTest, GetMethod302) @@ -107,8 +139,8 @@ TEST_F(ServerTest, GetMethod302) Response res; bool ret = Client(host_, port_).get("/", res); ASSERT_EQ(true, ret); - ASSERT_EQ(302, res.status); - ASSERT_EQ("/hi", res.get_header_value("Location")); + EXPECT_EQ(302, res.status); + EXPECT_EQ("/hi", res.get_header_value("Location")); } TEST_F(ServerTest, GetMethod404) @@ -116,7 +148,43 @@ TEST_F(ServerTest, GetMethod404) Response res; bool ret = Client(host_, port_).get("/invalid", res); ASSERT_EQ(true, ret); - ASSERT_EQ(404, res.status); + EXPECT_EQ(404, res.status); +} + +TEST_F(ServerTest, GetMethodPersonJohn) +{ + Response res; + bool ret = Client(host_, port_).get("/person/john", res); + ASSERT_EQ(true, ret); + EXPECT_EQ(200, res.status); + EXPECT_EQ("text/plain", res.get_header_value("Content-Type")); + EXPECT_EQ("programmer", res.body); +} + +TEST_F(ServerTest, PostMethod) +{ + { + Response res; + bool ret = Client(host_, port_).get("/person/john2", res); + ASSERT_EQ(true, ret); + ASSERT_EQ(404, res.status); + } + { + auto content = "name=john2¬e=coder"; + auto content_type = "application/x-www-form-urlencoded"; + Response res; + bool ret = Client(host_, port_).post("/person", content, content_type, res); + ASSERT_EQ(true, ret); + ASSERT_EQ(200, res.status); + } + { + Response res; + bool ret = Client(host_, port_).get("/person/john2", res); + ASSERT_EQ(true, ret); + ASSERT_EQ(200, res.status); + ASSERT_EQ("text/plain", res.get_header_value("Content-Type")); + ASSERT_EQ("coder", res.body); + } } // vim: et ts=4 sw=4 cin cino={1s ff=unix