mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 40db42108f | |||
| 24bb1387d6 | |||
| d0bd4afb0b | |||
| 78ea786abd | |||
| 9cac2c9ceb | |||
| 0cff3245df | |||
| 0e3925db3f | |||
| c9a13d214b | |||
| 0954af2d4c | |||
| 7c1c952f5a | |||
| a6edfc730a | |||
| c1264bfedc | |||
| 90a5b6ceb0 | |||
| eb240ad2e5 |
@@ -5,7 +5,7 @@ cpp-httplib
|
||||
|
||||
A C++11 single-file header-only cross platform HTTP/HTTPS library.
|
||||
|
||||
It's extremely easy to setup. Just include **httplib.h** file in your code!
|
||||
It's extremely easy to setup. Just include the **httplib.h** file in your code!
|
||||
|
||||
NOTE: This is a 'blocking' HTTP library. If you are looking for a 'non-blocking' library, this is not the one that you want.
|
||||
|
||||
@@ -120,24 +120,50 @@ 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 |
|
||||
| :--------- | :-------------------------- |
|
||||
| css | text/css |
|
||||
| csv | text/csv |
|
||||
| txt | text/plain |
|
||||
| vtt | text/vtt |
|
||||
| html, htm | text/html |
|
||||
| apng | image/apng |
|
||||
| avif | image/avif |
|
||||
| bmp | image/bmp |
|
||||
| gif | image/gif |
|
||||
| png | image/png |
|
||||
| svg | image/svg+xml |
|
||||
| webp | image/webp |
|
||||
| ico | image/x-icon |
|
||||
| tif | image/tiff |
|
||||
| tiff | image/tiff |
|
||||
| jpeg, jpg | image/jpeg |
|
||||
| mp4 | video/mp4 |
|
||||
| mpeg | video/mpeg |
|
||||
| webm | video/webm |
|
||||
| mp3 | audio/mp3 |
|
||||
| mpga | audio/mpeg |
|
||||
| weba | audio/webm |
|
||||
| wav | audio/wave |
|
||||
| otf | font/otf |
|
||||
| ttf | font/ttf |
|
||||
| woff | font/woff |
|
||||
| woff2 | font/woff2 |
|
||||
| 7z | application/x-7z-compressed |
|
||||
| atom | application/atom+xml |
|
||||
| pdf | application/pdf |
|
||||
| mjs, js | application/javascript |
|
||||
| json | application/json |
|
||||
| rss | application/rss+xml |
|
||||
| tar | application/x-tar |
|
||||
| xhtml, xht | application/xhtml+xml |
|
||||
| xslt | application/xslt+xml |
|
||||
| xml | application/xml |
|
||||
| gz | application/gzip |
|
||||
| zip | application/zip |
|
||||
| wasm | application/wasm |
|
||||
|
||||
NOTE: These the static file server methods are not thread safe.
|
||||
NOTE: These static file server methods are not thread-safe.
|
||||
|
||||
### Logging
|
||||
|
||||
@@ -171,7 +197,7 @@ svr.Post("/multipart", [&](const auto& req, auto& res) {
|
||||
});
|
||||
```
|
||||
|
||||
### Receive content with Content receiver
|
||||
### Receive content with a content receiver
|
||||
|
||||
```cpp
|
||||
svr.Post("/content_receiver",
|
||||
@@ -198,7 +224,7 @@ svr.Post("/content_receiver",
|
||||
});
|
||||
```
|
||||
|
||||
### Send content with Content provider
|
||||
### Send content with the content provider
|
||||
|
||||
```cpp
|
||||
const size_t DATA_CHUNK_SIZE = 4;
|
||||
@@ -255,7 +281,7 @@ svr.Get("/chunked", [&](const Request& req, Response& res) {
|
||||
|
||||
### 'Expect: 100-continue' handler
|
||||
|
||||
As default, the server sends `100 Continue` response for `Expect: 100-continue` header.
|
||||
By default, the server sends a `100 Continue` response for an `Expect: 100-continue` header.
|
||||
|
||||
```cpp
|
||||
// Send a '417 Expectation Failed' response.
|
||||
@@ -286,7 +312,7 @@ svr.set_write_timeout(5, 0); // 5 seconds
|
||||
svr.set_idle_interval(0, 100000); // 100 milliseconds
|
||||
```
|
||||
|
||||
### Set maximum payload length for reading request body
|
||||
### Set maximum payload length for reading a request body
|
||||
|
||||
```c++
|
||||
svr.set_payload_max_length(1024 * 1024 * 512); // 512MB
|
||||
@@ -471,7 +497,7 @@ cli.set_read_timeout(5, 0); // 5 seconds
|
||||
cli.set_write_timeout(5, 0); // 5 seconds
|
||||
```
|
||||
|
||||
### Receive content with Content receiver
|
||||
### Receive content with a content receiver
|
||||
|
||||
```c++
|
||||
std::string body;
|
||||
@@ -498,7 +524,7 @@ auto res = cli.Get(
|
||||
});
|
||||
```
|
||||
|
||||
### Send content with Content provider
|
||||
### Send content with a content provider
|
||||
|
||||
```cpp
|
||||
std::string body = ...;
|
||||
@@ -621,7 +647,7 @@ res = cli.Get("/");
|
||||
res->status; // 200
|
||||
```
|
||||
|
||||
### Use a specitic network interface
|
||||
### Use a specific network interface
|
||||
|
||||
NOTE: This feature is not available on Windows, yet.
|
||||
|
||||
@@ -655,7 +681,7 @@ be to set up a signal handler for SIGPIPE to handle or ignore it yourself.
|
||||
Compression
|
||||
-----------
|
||||
|
||||
The server can applie compression to the following MIME type contents:
|
||||
The server can apply compression to the following MIME type contents:
|
||||
|
||||
* all text types except text/event-stream
|
||||
* image/svg+xml
|
||||
@@ -719,7 +745,7 @@ Include `httplib.h` before `Windows.h` or include `Windows.h` by defining `WIN32
|
||||
#include <httplib.h>
|
||||
```
|
||||
|
||||
Note: Cygwin on Windows is not supported.
|
||||
Note: Windows 8 or lower and Cygwin on Windows are not supported.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
@@ -203,6 +203,7 @@ using socket_t = int;
|
||||
#include <string>
|
||||
#include <sys/stat.h>
|
||||
#include <thread>
|
||||
#include <iomanip>
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#include <openssl/err.h>
|
||||
@@ -214,7 +215,6 @@ using socket_t = int;
|
||||
#include <openssl/applink.c>
|
||||
#endif
|
||||
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
@@ -1025,7 +1025,7 @@ protected:
|
||||
|
||||
private:
|
||||
socket_t create_client_socket(Error &error) const;
|
||||
bool read_response_line(Stream &strm, Response &res);
|
||||
bool read_response_line(Stream &strm, const Request &req, Response &res);
|
||||
bool write_request(Stream &strm, const Request &req, bool close_connection,
|
||||
Error &error);
|
||||
bool redirect(const Request &req, Response &res, Error &error);
|
||||
@@ -1457,6 +1457,33 @@ inline bool is_valid_path(const std::string &path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::string encode_query_param(const std::string &value){
|
||||
std::ostringstream escaped;
|
||||
escaped.fill('0');
|
||||
escaped << std::hex;
|
||||
|
||||
for (char const &c: value) {
|
||||
if (std::isalnum(c) ||
|
||||
c == '-' ||
|
||||
c == '_' ||
|
||||
c == '.' ||
|
||||
c == '!' ||
|
||||
c == '~' ||
|
||||
c == '*' ||
|
||||
c == '\'' ||
|
||||
c == '(' ||
|
||||
c == ')') {
|
||||
escaped << c;
|
||||
} else {
|
||||
escaped << std::uppercase;
|
||||
escaped << '%' << std::setw(2) << static_cast<int>(static_cast<unsigned char>(c));
|
||||
escaped << std::nouppercase;
|
||||
}
|
||||
}
|
||||
|
||||
return escaped.str();
|
||||
}
|
||||
|
||||
inline std::string encode_url(const std::string &s) {
|
||||
std::string result;
|
||||
|
||||
@@ -2135,6 +2162,25 @@ inline void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) {
|
||||
}
|
||||
}
|
||||
|
||||
inline constexpr unsigned int str2tag_core(const char *s, size_t l,
|
||||
unsigned int h) {
|
||||
return (l == 0) ? h
|
||||
: str2tag_core(s + 1, l - 1,
|
||||
(h * 33) ^ static_cast<unsigned char>(*s));
|
||||
}
|
||||
|
||||
inline unsigned int str2tag(const std::string &s) {
|
||||
return str2tag_core(s.data(), s.size(), 0);
|
||||
}
|
||||
|
||||
namespace udl {
|
||||
|
||||
inline constexpr unsigned int operator"" _(const char *s, size_t l) {
|
||||
return str2tag_core(s, l, 0);
|
||||
}
|
||||
|
||||
} // namespace udl
|
||||
|
||||
inline const char *
|
||||
find_content_type(const std::string &path,
|
||||
const std::map<std::string, std::string> &user_data) {
|
||||
@@ -2143,36 +2189,60 @@ find_content_type(const std::string &path,
|
||||
auto it = user_data.find(ext);
|
||||
if (it != user_data.end()) { return it->second.c_str(); }
|
||||
|
||||
if (ext == "txt") {
|
||||
return "text/plain";
|
||||
} else if (ext == "html" || ext == "htm") {
|
||||
return "text/html";
|
||||
} else if (ext == "css") {
|
||||
return "text/css";
|
||||
} else if (ext == "jpeg" || ext == "jpg") {
|
||||
return "image/jpg";
|
||||
} else if (ext == "png") {
|
||||
return "image/png";
|
||||
} else if (ext == "gif") {
|
||||
return "image/gif";
|
||||
} else if (ext == "svg") {
|
||||
return "image/svg+xml";
|
||||
} else if (ext == "ico") {
|
||||
return "image/x-icon";
|
||||
} else if (ext == "json") {
|
||||
return "application/json";
|
||||
} else if (ext == "pdf") {
|
||||
return "application/pdf";
|
||||
} else if (ext == "js") {
|
||||
return "application/javascript";
|
||||
} else if (ext == "wasm") {
|
||||
return "application/wasm";
|
||||
} else if (ext == "xml") {
|
||||
return "application/xml";
|
||||
} else if (ext == "xhtml") {
|
||||
return "application/xhtml+xml";
|
||||
using udl::operator""_;
|
||||
|
||||
switch (str2tag(ext)) {
|
||||
default: return nullptr;
|
||||
case "css"_: return "text/css";
|
||||
case "csv"_: return "text/csv";
|
||||
case "txt"_: return "text/plain";
|
||||
case "vtt"_: return "text/vtt";
|
||||
case "htm"_:
|
||||
case "html"_: return "text/html";
|
||||
|
||||
case "apng"_: return "image/apng";
|
||||
case "avif"_: return "image/avif";
|
||||
case "bmp"_: return "image/bmp";
|
||||
case "gif"_: return "image/gif";
|
||||
case "png"_: return "image/png";
|
||||
case "svg"_: return "image/svg+xml";
|
||||
case "webp"_: return "image/webp";
|
||||
case "ico"_: return "image/x-icon";
|
||||
case "tif"_: return "image/tiff";
|
||||
case "tiff"_: return "image/tiff";
|
||||
case "jpg"_:
|
||||
case "jpeg"_: return "image/jpeg";
|
||||
|
||||
case "mp4"_: return "video/mp4";
|
||||
case "mpeg"_: return "video/mpeg";
|
||||
case "webm"_: return "video/webm";
|
||||
|
||||
case "mp3"_: return "audio/mp3";
|
||||
case "mpga"_: return "audio/mpeg";
|
||||
case "weba"_: return "audio/webm";
|
||||
case "wav"_: return "audio/wave";
|
||||
|
||||
case "otf"_: return "font/otf";
|
||||
case "ttf"_: return "font/ttf";
|
||||
case "woff"_: return "font/woff";
|
||||
case "woff2"_: return "font/woff2";
|
||||
|
||||
case "7z"_: return "application/x-7z-compressed";
|
||||
case "atom"_: return "application/atom+xml";
|
||||
case "pdf"_: return "application/pdf";
|
||||
case "js"_:
|
||||
case "mjs"_: return "application/javascript";
|
||||
case "json"_: return "application/json";
|
||||
case "rss"_: return "application/rss+xml";
|
||||
case "tar"_: return "application/x-tar";
|
||||
case "xht"_:
|
||||
case "xhtml"_: return "application/xhtml+xml";
|
||||
case "xslt"_: return "application/xslt+xml";
|
||||
case "xml"_: return "application/xml";
|
||||
case "gz"_: return "application/gzip";
|
||||
case "zip"_: return "application/zip";
|
||||
case "wasm"_: return "application/wasm";
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline const char *status_message(int status) {
|
||||
@@ -3002,7 +3072,7 @@ inline std::string params_to_query_str(const Params ¶ms) {
|
||||
if (it != params.begin()) { query += "&"; }
|
||||
query += it->first;
|
||||
query += "=";
|
||||
query += encode_url(it->second);
|
||||
query += encode_query_param(it->second);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
@@ -4134,7 +4204,7 @@ inline void Server::stop() {
|
||||
inline bool Server::parse_request_line(const char *s, Request &req) {
|
||||
const static std::regex re(
|
||||
"(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH|PRI) "
|
||||
"(([^?]+)(?:\\?(.*?))?) (HTTP/1\\.[01])\r\n");
|
||||
"(([^? ]+)(?:\\?([^ ]*?))?) (HTTP/1\\.[01])\r\n");
|
||||
|
||||
std::cmatch m;
|
||||
if (std::regex_match(s, m, re)) {
|
||||
@@ -4396,7 +4466,7 @@ inline bool Server::handle_file_request(Request &req, Response &res,
|
||||
for (const auto &kv : entry.headers) {
|
||||
res.set_header(kv.first.c_str(), kv.second);
|
||||
}
|
||||
res.status = 200;
|
||||
res.status = req.has_header("Range") ? 206 : 200;
|
||||
if (!head && file_request_handler_) {
|
||||
file_request_handler_(req, res);
|
||||
}
|
||||
@@ -4943,17 +5013,20 @@ inline void ClientImpl::lock_socket_and_shutdown_and_close() {
|
||||
close_socket(socket_);
|
||||
}
|
||||
|
||||
inline bool ClientImpl::read_response_line(Stream &strm, Response &res) {
|
||||
inline bool ClientImpl::read_response_line(Stream &strm, const Request &req,
|
||||
Response &res) {
|
||||
std::array<char, 2048> buf;
|
||||
|
||||
detail::stream_line_reader line_reader(strm, buf.data(), buf.size());
|
||||
|
||||
if (!line_reader.getline()) { return false; }
|
||||
|
||||
const static std::regex re("(HTTP/1\\.[01]) (\\d+) (.*?)\r\n");
|
||||
const static std::regex re("(HTTP/1\\.[01]) (\\d{3}) (.*?)\r\n");
|
||||
|
||||
std::cmatch m;
|
||||
if (!std::regex_match(line_reader.ptr(), m, re)) { return true; }
|
||||
if (!std::regex_match(line_reader.ptr(), m, re)) {
|
||||
return req.method == "CONNECT";
|
||||
}
|
||||
res.version = std::string(m[1]);
|
||||
res.status = std::stoi(std::string(m[2]));
|
||||
res.reason = std::string(m[3]);
|
||||
@@ -5400,7 +5473,7 @@ inline bool ClientImpl::process_request(Stream &strm, const Request &req,
|
||||
if (!write_request(strm, req, close_connection, error)) { return false; }
|
||||
|
||||
// Receive response and headers
|
||||
if (!read_response_line(strm, res) ||
|
||||
if (!read_response_line(strm, req, res) ||
|
||||
!detail::read_headers(strm, res.headers)) {
|
||||
error = Error::Read;
|
||||
return false;
|
||||
@@ -5441,18 +5514,12 @@ inline bool ClientImpl::process_request(Stream &strm, const Request &req,
|
||||
};
|
||||
|
||||
int dummy_status;
|
||||
// std::cout << "A" << std::endl;
|
||||
if (!detail::read_content(strm, res, (std::numeric_limits<size_t>::max)(),
|
||||
dummy_status, std::move(progress), std::move(out),
|
||||
decompress_)) {
|
||||
// std::cout << "B" << std::endl;
|
||||
if (error != Error::Canceled) {
|
||||
// std::cout << "C" << std::endl;
|
||||
error = Error::Read;
|
||||
}
|
||||
if (error != Error::Canceled) { error = Error::Read; }
|
||||
return false;
|
||||
}
|
||||
// std::cout << "D" << std::endl;
|
||||
}
|
||||
|
||||
if (res.get_header_value("Connection") == "close" ||
|
||||
@@ -6234,8 +6301,9 @@ inline bool SSLServer::process_and_close_socket(socket_t sock) {
|
||||
},
|
||||
[](SSL * /*ssl*/) { return true; });
|
||||
|
||||
bool ret = false;
|
||||
if (ssl) {
|
||||
auto ret = detail::process_server_socket_ssl(
|
||||
ret = detail::process_server_socket_ssl(
|
||||
ssl, sock, keep_alive_max_count_, keep_alive_timeout_sec_,
|
||||
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
|
||||
write_timeout_usec_,
|
||||
@@ -6249,12 +6317,11 @@ inline bool SSLServer::process_and_close_socket(socket_t sock) {
|
||||
// the connection appeared to be closed.
|
||||
const bool shutdown_gracefully = ret;
|
||||
detail::ssl_delete(ctx_mutex_, ssl, shutdown_gracefully);
|
||||
return ret;
|
||||
}
|
||||
|
||||
detail::shutdown_socket(sock);
|
||||
detail::close_socket(sock);
|
||||
return false;
|
||||
return ret;
|
||||
}
|
||||
|
||||
// SSL HTTP client implementation
|
||||
|
||||
@@ -46,6 +46,18 @@ TEST(StartupTest, WSAStartup) {
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(EncodeQueryParamTest, ParseUnescapedChararactersTest){
|
||||
string unescapedCharacters = "-_.!~*'()";
|
||||
|
||||
EXPECT_EQ(detail::encode_query_param(unescapedCharacters), "-_.!~*'()");
|
||||
}
|
||||
|
||||
TEST(EncodeQueryParamTest, ParseReservedCharactersTest){
|
||||
string reservedCharacters = ";,/?:@&=+$";
|
||||
|
||||
EXPECT_EQ(detail::encode_query_param(reservedCharacters), "%3B%2C%2F%3F%3A%40%26%3D%2B%24");
|
||||
}
|
||||
|
||||
TEST(TrimTests, TrimStringTests) {
|
||||
EXPECT_EQ("abc", detail::trim_copy("abc"));
|
||||
EXPECT_EQ("abc", detail::trim_copy(" abc "));
|
||||
@@ -930,6 +942,31 @@ TEST(ErrorHandlerTest, ContentLength) {
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
}
|
||||
|
||||
TEST(InvalidFormatTest, StatusCode) {
|
||||
Server svr;
|
||||
|
||||
svr.Get("/hi", [](const Request & /*req*/, Response &res) {
|
||||
res.set_content("Hello World!\n", "text/plain");
|
||||
res.status = 9999; // Status should be a three-digit code...
|
||||
});
|
||||
|
||||
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
|
||||
// Give GET time to get a few messages.
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
|
||||
{
|
||||
Client cli(HOST, PORT);
|
||||
|
||||
auto res = cli.Get("/hi");
|
||||
ASSERT_FALSE(res);
|
||||
}
|
||||
|
||||
svr.stop();
|
||||
thread.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
}
|
||||
|
||||
class ServerTest : public ::testing::Test {
|
||||
protected:
|
||||
ServerTest()
|
||||
@@ -1676,6 +1713,16 @@ TEST_F(ServerTest, UserDefinedMIMETypeMapping) {
|
||||
EXPECT_EQ("abcde", res->body);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, StaticFileRange) {
|
||||
auto res = cli_.Get("/dir/test.abcde", {{make_range_header({{2, 3}})}});
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(206, res->status);
|
||||
EXPECT_EQ("text/abcde", res->get_header_value("Content-Type"));
|
||||
EXPECT_EQ("2", res->get_header_value("Content-Length"));
|
||||
EXPECT_EQ(true, res->has_header("Content-Range"));
|
||||
EXPECT_EQ(std::string("cd"), res->body);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, InvalidBaseDirMount) {
|
||||
EXPECT_EQ(false, svr_.set_mount_point("invalid_mount_point", "./www3"));
|
||||
}
|
||||
@@ -2957,6 +3004,12 @@ TEST(ServerRequestParsingTest, InvalidHeaderTextWithExtraCR) {
|
||||
"Content-Type: text/plain\r\n\r");
|
||||
}
|
||||
|
||||
TEST(ServerRequestParsingTest, InvalidSpaceInURL) {
|
||||
std::string out;
|
||||
test_raw_request("GET /h i HTTP/1.1\r\n\r\n", &out);
|
||||
EXPECT_EQ("HTTP/1.1 400 Bad Request", out.substr(0, 24));
|
||||
}
|
||||
|
||||
TEST(ServerStopTest, StopServerWithChunkedTransmission) {
|
||||
Server svr;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user