Compare commits

..

14 Commits

Author SHA1 Message Date
yhirose
fe01fa760b Update README 2020-02-14 22:07:39 -05:00
yhirose
d61d63dd97 Added unit tests for SSLServer::bind_to_any_port 2020-02-14 21:59:37 -05:00
yhirose
3fe13ecc91 Updated README 2020-02-14 21:49:09 -05:00
yhirose
064cc6810e Fix #352 2020-02-13 17:40:06 -05:00
yhirose
464cc89b77 Fix #351 2020-02-12 22:35:52 -05:00
yhirose
ca5a50d2c9 Fix #349 2020-02-11 00:26:15 -05:00
yhirose
b251668522 Fixed build problem. 2020-02-03 22:45:50 -05:00
yhirose
9ee740fe8f Updated README 2020-02-02 20:57:29 -05:00
yhirose
851edaf77f Code format 2020-02-02 20:02:16 -05:00
yhirose
1a2a6e2b01 Updated README 2020-02-01 09:24:42 -05:00
yhirose
ac7742bb32 Changed the order of parameters of set_mounting_point. 2020-02-01 09:18:24 -05:00
yhirose
82c11168c1 Merge pull request #342 from yhirose/remove_mount_point
Fix #341
2020-01-31 20:41:39 -05:00
yhirose
7c5197c86c Updated README 2020-01-31 20:40:33 -05:00
yhirose
8801e51138 Fix #341 2020-01-31 20:35:20 -05:00
5 changed files with 252 additions and 120 deletions

221
README.md
View File

@@ -17,24 +17,24 @@ Server Example
int main(void)
{
using namespace httplib;
using namespace httplib;
Server svr;
Server svr;
svr.Get("/hi", [](const Request& req, Response& res) {
res.set_content("Hello World!", "text/plain");
});
svr.Get("/hi", [](const Request& req, Response& res) {
res.set_content("Hello World!", "text/plain");
});
svr.Get(R"(/numbers/(\d+))", [&](const Request& req, Response& res) {
auto numbers = req.matches[1];
res.set_content(numbers, "text/plain");
});
svr.Get(R"(/numbers/(\d+))", [&](const Request& req, Response& res) {
auto numbers = req.matches[1];
res.set_content(numbers, "text/plain");
});
svr.Get("/stop", [&](const Request& req, Response& res) {
svr.stop();
});
svr.Get("/stop", [&](const Request& req, Response& res) {
svr.stop();
});
svr.listen("localhost", 1234);
svr.listen("localhost", 1234);
}
```
@@ -50,17 +50,24 @@ svr.listen_after_bind();
### Static File Server
```cpp
auto ret = svr.set_base_dir("./www"); // This is same as `svr.set_base_dir("./www", "/")`;
// Mount / to ./www directory
auto ret = svr.set_mount_point("/", "./www");
if (!ret) {
// The specified base directory doesn't exist...
}
// Mount /public to ./www directory
ret = svr.set_base_dir("./www", "/public");
ret = svr.set_mount_point("/public", "./www");
// Mount /public to ./www1 and ./www2 directories
ret = svr.set_base_dir("./www1", "/public"); // 1st order to search
ret = svr.set_base_dir("./www2", "/public"); // 2nd order to search
ret = svr.set_mount_point("/public", "./www1"); // 1st order to search
ret = svr.set_mount_point("/public", "./www2"); // 2nd order to search
// Remove mount /
ret = svr.remove_mount_point("/");
// Remove mount /public
ret = svr.remove_mount_point("/public");
```
```cpp
@@ -72,39 +79,41 @@ svr.set_file_extension_and_mimetype_mapping("hh", "text/x-h");
The followings are built-in mappings:
| Extension | MIME Type |
| :--------- | :--------------------- |
| .txt | text/plain |
| .html .htm | text/html |
| .css | text/css |
| .jpeg .jpg | image/jpg |
| .png | image/png |
| .gif | image/gif |
| .svg | image/svg+xml |
| .ico | image/x-icon |
| .json | application/json |
| .pdf | application/pdf |
| .js | application/javascript |
| .wasm | application/wasm |
| .xml | application/xml |
| .xhtml | application/xhtml+xml |
| Extension | MIME Type |
| :-------- | :--------------------- |
| txt | text/plain |
| html, htm | text/html |
| css | text/css |
| jpeg, jpg | image/jpg |
| png | image/png |
| gif | image/gif |
| svg | image/svg+xml |
| ico | image/x-icon |
| json | application/json |
| pdf | application/pdf |
| js | application/javascript |
| wasm | application/wasm |
| xml | application/xml |
| xhtml | application/xhtml+xml |
NOTE: These the static file server methods are not thread safe.
### Logging
```cpp
svr.set_logger([](const auto& req, const auto& res) {
your_logger(req, res);
your_logger(req, res);
});
```
### Error Handler
### Error handler
```cpp
svr.set_error_handler([](const auto& req, auto& res) {
auto fmt = "<p>Error Status: <span style='color:red;'>%d</span></p>";
char buf[BUFSIZ];
snprintf(buf, sizeof(buf), fmt, res.status);
res.set_content(buf, "text/html");
auto fmt = "<p>Error Status: <span style='color:red;'>%d</span></p>";
char buf[BUFSIZ];
snprintf(buf, sizeof(buf), fmt, res.status);
res.set_content(buf, "text/html");
});
```
@@ -112,31 +121,12 @@ svr.set_error_handler([](const auto& req, auto& res) {
```cpp
svr.Post("/multipart", [&](const auto& req, auto& res) {
auto size = req.files.size();
auto ret = req.has_file("name1");
const auto& file = req.get_file_value("name1");
// file.filename;
// file.content_type;
// file.content;
});
```
### Send content with Content provider
```cpp
const uint64_t DATA_CHUNK_SIZE = 4;
svr.Get("/stream", [&](const Request &req, Response &res) {
auto data = new std::string("abcdefg");
res.set_content_provider(
data->size(), // Content length
[data](uint64_t offset, uint64_t length, DataSink &sink) {
const auto &d = *data;
sink.write(&d[offset], std::min(length, DATA_CHUNK_SIZE));
},
[data] { delete data; });
auto size = req.files.size();
auto ret = req.has_file("name1");
const auto& file = req.get_file_value("name1");
// file.filename;
// file.content_type;
// file.content;
});
```
@@ -167,6 +157,24 @@ svr.Post("/content_receiver",
});
```
### Send content with Content provider
```cpp
const uint64_t DATA_CHUNK_SIZE = 4;
svr.Get("/stream", [&](const Request &req, Response &res) {
auto data = new std::string("abcdefg");
res.set_content_provider(
data->size(), // Content length
[data](uint64_t offset, uint64_t length, DataSink &sink) {
const auto &d = *data;
sink.write(&d[offset], std::min(length, DATA_CHUNK_SIZE));
},
[data] { delete data; });
});
```
### Chunked transfer encoding
```cpp
@@ -191,11 +199,7 @@ Please check [here](https://github.com/yhirose/cpp-httplib/blob/master/example/s
`ThreadPool` is used as a default task queue, and the default thread count is set to value from `std::thread::hardware_concurrency()`.
Set thread count to 8:
```cpp
#define CPPHTTPLIB_THREAD_POOL_COUNT 8
```
You can change the thread count by setting `CPPHTTPLIB_THREAD_POOL_COUNT`.
### Override the default thread pool with yours
@@ -223,6 +227,24 @@ svr.new_task_queue = [] {
};
```
### 'Expect: 100-continue' handler
As default, the server sends `100 Continue` response for `Expect: 100-continue` header.
```cpp
// Send a '417 Expectation Failed' response.
svr.set_expect_100_continue_handler([](const Request &req, Response &res) {
return 417;
});
```
```cpp
// Send a final status without reading the message body.
svr.set_expect_100_continue_handler([](const Request &req, Response &res) {
return res.status = 401;
});
```
Client Example
--------------
@@ -234,36 +256,36 @@ Client Example
int main(void)
{
httplib::Client cli("localhost", 1234);
httplib::Client cli("localhost", 1234);
auto res = cli.Get("/hi");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
auto res = cli.Get("/hi");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
}
```
### GET with HTTP headers
```c++
httplib::Headers headers = {
{ "Accept-Encoding", "gzip, deflate" }
};
auto res = cli.Get("/hi", headers);
httplib::Headers headers = {
{ "Accept-Encoding", "gzip, deflate" }
};
auto res = cli.Get("/hi", headers);
```
### GET with Content Receiver
```c++
std::string body;
std::string body;
auto res = cli.Get("/large-data",
[&](const char *data, uint64_t data_length) {
body.append(data, data_length);
return true;
});
auto res = cli.Get("/large-data",
[&](const char *data, uint64_t data_length) {
body.append(data, data_length);
return true;
});
assert(res->body.empty());
assert(res->body.empty());
```
### POST
@@ -296,15 +318,15 @@ auto res = cli.Post("/post", params);
### POST with Multipart Form Data
```c++
httplib::MultipartFormDataItems items = {
{ "text1", "text default", "", "" },
{ "text2", "aωb", "", "" },
{ "file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain" },
{ "file2", "{\n \"world\", true\n}\n", "world.json", "application/json" },
{ "file3", "", "", "application/octet-stream" },
};
httplib::MultipartFormDataItems items = {
{ "text1", "text default", "", "" },
{ "text2", "aωb", "", "" },
{ "file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain" },
{ "file2", "{\n \"world\", true\n}\n", "world.json", "application/json" },
{ "file3", "", "", "application/octet-stream" },
};
auto res = cli.Post("/multipart", items);
auto res = cli.Post("/multipart", items);
```
### PUT
@@ -338,12 +360,12 @@ httplib::Client client(url, port);
// prints: 0 / 000 bytes => 50% complete
std::shared_ptr<httplib::Response> res =
cli.Get("/", [](uint64_t len, uint64_t total) {
printf("%lld / %lld bytes => %d%% complete\n",
len, total,
(int)((len/total)*100));
return true; // return 'false' if you want to cancel the request.
}
cli.Get("/", [](uint64_t len, uint64_t total) {
printf("%lld / %lld bytes => %d%% complete\n",
len, total,
(int)((len/total)*100));
return true; // return 'false' if you want to cancel the request.
}
);
```
@@ -532,3 +554,4 @@ The following folks made great contributions to polish this library to totally a
* [DraTeots](https://github.com/DraTeots)
* [BastienDurel](https://github.com/BastienDurel)
* [vitalyster](https://github.com/vitalyster)
* [trollixx](https://github.com/trollixx)

View File

@@ -122,7 +122,7 @@ int main(int argc, const char **argv) {
auto base_dir = "./";
if (argc > 2) { base_dir = argv[2]; }
if (!svr.set_base_dir(base_dir)) {
if (!svr.set_mount_point("/", base_dir)) {
cout << "The specified base directory doesn't exist...";
return 1;
}

View File

@@ -11,6 +11,7 @@
#include <httplib.h>
#include <iostream>
#include <mutex>
#include <sstream>
#include <thread>
using namespace httplib;

View File

@@ -41,7 +41,7 @@
#endif
#ifndef CPPHTTPLIB_PAYLOAD_MAX_LENGTH
#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH (std::numeric_limits<size_t>::max)()
#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH (std::numeric_limits<size_t>::max())
#endif
#ifndef CPPHTTPLIB_RECV_BUFSIZ
@@ -49,12 +49,8 @@
#endif
#ifndef CPPHTTPLIB_THREAD_POOL_COUNT
// if hardware_concurrency() outputs 0 we still wants to use threads for this.
// -1 because we have one thread already in the main function.
#define CPPHTTPLIB_THREAD_POOL_COUNT \
(std::thread::hardware_concurrency() \
? std::thread::hardware_concurrency() - 1 \
: 2)
(std::max(1u, std::thread::hardware_concurrency() - 1))
#endif
/*
@@ -448,6 +444,8 @@ public:
using Handler = std::function<void(const Request &, Response &)>;
using HandlerWithContentReader = std::function<void(
const Request &, Response &, const ContentReader &content_reader)>;
using Expect100ContinueHandler =
std::function<int(const Request &, Response &)>;
Server();
@@ -465,7 +463,10 @@ public:
Server &Delete(const char *pattern, Handler handler);
Server &Options(const char *pattern, Handler handler);
bool set_base_dir(const char *dir, const char *mount_point = nullptr);
[[deprecated]] bool set_base_dir(const char *dir,
const char *mount_point = nullptr);
bool set_mount_point(const char *mount_point, const char *dir);
bool remove_mount_point(const char *mount_point);
void set_file_extension_and_mimetype_mapping(const char *ext,
const char *mime);
void set_file_request_handler(Handler handler);
@@ -473,6 +474,8 @@ public:
void set_error_handler(Handler handler);
void set_logger(Logger logger);
void set_expect_100_continue_handler(Expect100ContinueHandler handler);
void set_keep_alive_max_count(size_t count);
void set_read_timeout(time_t sec, time_t usec);
void set_payload_max_length(size_t length);
@@ -550,6 +553,7 @@ private:
Handlers options_handlers_;
Handler error_handler_;
Logger logger_;
Expect100ContinueHandler expect_100_continue_handler_;
};
class Client {
@@ -1591,6 +1595,7 @@ find_content_type(const std::string &path,
inline const char *status_message(int status) {
switch (status) {
case 100: return "Continue";
case 200: return "OK";
case 202: return "Accepted";
case 204: return "No Content";
@@ -1607,6 +1612,7 @@ inline const char *status_message(int status) {
case 414: return "Request-URI Too Long";
case 415: return "Unsupported Media Type";
case 416: return "Range Not Satisfiable";
case 417: return "Expectation Failed";
case 503: return "Service Unavailable";
default:
@@ -2889,6 +2895,10 @@ inline Server &Server::Options(const char *pattern, Handler handler) {
}
inline bool Server::set_base_dir(const char *dir, const char *mount_point) {
return set_mount_point(mount_point, dir);
}
inline bool Server::set_mount_point(const char *mount_point, const char *dir) {
if (detail::is_dir(dir)) {
std::string mnt = mount_point ? mount_point : "/";
if (!mnt.empty() && mnt[0] == '/') {
@@ -2899,6 +2909,16 @@ inline bool Server::set_base_dir(const char *dir, const char *mount_point) {
return false;
}
inline bool Server::remove_mount_point(const char *mount_point) {
for (auto it = base_dirs_.begin(); it != base_dirs_.end(); ++it) {
if (it->first == mount_point) {
base_dirs_.erase(it);
return true;
}
}
return false;
}
inline void Server::set_file_extension_and_mimetype_mapping(const char *ext,
const char *mime) {
file_extension_and_mimetype_map_[ext] = mime;
@@ -2914,6 +2934,11 @@ inline void Server::set_error_handler(Handler handler) {
inline void Server::set_logger(Logger logger) { logger_ = std::move(logger); }
inline void
Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) {
expect_100_continue_handler_ = std::move(handler);
}
inline void Server::set_keep_alive_max_count(size_t count) {
keep_alive_max_count_ = count;
}
@@ -2980,9 +3005,11 @@ inline bool Server::write_response(Stream &strm, bool last_connection,
if (400 <= res.status && error_handler_) { error_handler_(req, res); }
detail::BufferStream bstrm;
// Response line
if (!strm.write_format("HTTP/1.1 %d %s\r\n", res.status,
detail::status_message(res.status))) {
if (!bstrm.write_format("HTTP/1.1 %d %s\r\n", res.status,
detail::status_message(res.status))) {
return false;
}
@@ -2995,11 +3022,12 @@ inline bool Server::write_response(Stream &strm, bool last_connection,
res.set_header("Connection", "Keep-Alive");
}
if (!res.has_header("Content-Type")) {
if (!res.has_header("Content-Type") &&
(!res.body.empty() || res.content_length > 0)) {
res.set_header("Content-Type", "text/plain");
}
if (!res.has_header("Accept-Ranges")) {
if (!res.has_header("Accept-Ranges") && req.method == "HEAD") {
res.set_header("Accept-Ranges", "bytes");
}
@@ -3076,7 +3104,11 @@ inline bool Server::write_response(Stream &strm, bool last_connection,
res.set_header("Content-Length", length);
}
if (!detail::write_headers(strm, res, Headers())) { return false; }
if (!detail::write_headers(bstrm, res, Headers())) { return false; }
// Flush buffer
auto &data = bstrm.get_buffer();
strm.write(data.data(), data.size());
// Body
if (req.method != "HEAD") {
@@ -3212,7 +3244,8 @@ inline bool Server::read_content_core(Stream &strm, bool last_connection,
return true;
}
inline bool Server::handle_file_request(Request &req, Response &res, bool head) {
inline bool Server::handle_file_request(Request &req, Response &res,
bool head) {
for (const auto &kv : base_dirs_) {
const auto &mount_point = kv.first;
const auto &base_dir = kv.second;
@@ -3230,7 +3263,9 @@ inline bool Server::handle_file_request(Request &req, Response &res, bool head)
detail::find_content_type(path, file_extension_and_mimetype_map_);
if (type) { res.set_header("Content-Type", type); }
res.status = 200;
if (!head && file_request_handler_) { file_request_handler_(req, res); }
if (!head && file_request_handler_) {
file_request_handler_(req, res);
}
return true;
}
}
@@ -3332,7 +3367,10 @@ inline bool Server::routing(Request &req, Response &res, Stream &strm,
bool last_connection) {
// File handler
bool is_head_request = req.method == "HEAD";
if ((req.method == "GET" || is_head_request) && handle_file_request(req, res, is_head_request)) { return true; }
if ((req.method == "GET" || is_head_request) &&
handle_file_request(req, res, is_head_request)) {
return true;
}
if (detail::expect_content(req)) {
// Content reader handler
@@ -3468,6 +3506,21 @@ Server::process_request(Stream &strm, bool last_connection,
if (setup_request) { setup_request(req); }
if (req.get_header_value("Expect") == "100-continue") {
auto status = 100;
if (expect_100_continue_handler_) {
status = expect_100_continue_handler_(req, res);
}
switch (status) {
case 100:
case 417:
strm.write_format("HTTP/1.1 %d %s\r\n\r\n", status,
detail::status_message(status));
break;
default: return write_response(strm, last_connection, req, res);
}
}
// Rounting
if (routing(req, res, strm, last_connection)) {
if (res.status == -1) { res.status = req.ranges.empty() ? 200 : 206; }

View File

@@ -201,7 +201,7 @@ TEST(ParseHeaderValueTest, Range) {
TEST(BufferStreamTest, read) {
detail::BufferStream strm1;
Stream& strm = strm1;
Stream &strm = strm1;
EXPECT_EQ(5, strm.write("hello"));
@@ -645,11 +645,22 @@ TEST(HttpsToHttpRedirectTest, Redirect) {
TEST(Server, BindAndListenSeparately) {
Server svr;
int port = svr.bind_to_any_port("localhost");
int port = svr.bind_to_any_port("0.0.0.0");
ASSERT_TRUE(svr.is_valid());
ASSERT_TRUE(port > 0);
svr.stop();
}
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
TEST(SSLServer, BindAndListenSeparately) {
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE, CLIENT_CA_CERT_DIR);
int port = svr.bind_to_any_port("0.0.0.0");
ASSERT_TRUE(svr.is_valid());
ASSERT_TRUE(port > 0);
svr.stop();
}
#endif
class ServerTest : public ::testing::Test {
protected:
ServerTest()
@@ -662,8 +673,8 @@ protected:
}
virtual void SetUp() {
svr_.set_base_dir("./www");
svr_.set_base_dir("./www2", "/mount");
svr_.set_mount_point("/", "./www");
svr_.set_mount_point("/mount", "./www2");
svr_.set_file_extension_and_mimetype_mapping("abcde", "text/abcde");
svr_.Get("/hi",
@@ -1245,7 +1256,7 @@ TEST_F(ServerTest, UserDefinedMIMETypeMapping) {
}
TEST_F(ServerTest, InvalidBaseDirMount) {
EXPECT_EQ(false, svr_.set_base_dir("./www3", "invalid_mount_point"));
EXPECT_EQ(false, svr_.set_mount_point("invalid_mount_point", "./www3"));
}
TEST_F(ServerTest, EmptyRequest) {
@@ -2069,6 +2080,50 @@ TEST(ServerStopTest, StopServerWithChunkedTransmission) {
ASSERT_FALSE(svr.is_running());
}
TEST(MountTest, Unmount) {
Server svr;
auto listen_thread = std::thread([&svr]() { svr.listen("localhost", PORT); });
while (!svr.is_running()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
// Give GET time to get a few messages.
std::this_thread::sleep_for(std::chrono::seconds(1));
Client cli("localhost", PORT);
svr.set_mount_point("/mount2", "./www2");
auto res = cli.Get("/");
ASSERT_TRUE(res != nullptr);
EXPECT_EQ(404, res->status);
res = cli.Get("/mount2/dir/test.html");
ASSERT_TRUE(res != nullptr);
EXPECT_EQ(200, res->status);
svr.set_mount_point("/", "./www");
res = cli.Get("/dir/");
ASSERT_TRUE(res != nullptr);
EXPECT_EQ(200, res->status);
svr.remove_mount_point("/");
res = cli.Get("/dir/");
ASSERT_TRUE(res != nullptr);
EXPECT_EQ(404, res->status);
svr.remove_mount_point("/mount2");
res = cli.Get("/mount2/dir/test.html");
ASSERT_TRUE(res != nullptr);
EXPECT_EQ(404, res->status);
svr.stop();
listen_thread.join();
ASSERT_FALSE(svr.is_running());
}
class ServerTestWithAI_PASSIVE : public ::testing::Test {
protected:
ServerTestWithAI_PASSIVE()