Added post method support.

This commit is contained in:
yhirose 2012-10-03 23:47:59 -04:00
parent ffde8b7e4b
commit e0a25745cf
2 changed files with 198 additions and 67 deletions

165
httplib.h
View file

@ -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 <typename T>
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) ||

View file

@ -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<string, string> 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(); });
}
@ -88,6 +119,7 @@ protected:
const char* host_ = "localhost";
int port_ = 1914;
std::map<std::string, std::string> persons_;
Server svr_;
std::future<void> f_;
};
@ -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);
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&note=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