Compare commits

...

14 Commits

Author SHA1 Message Date
yhirose 40db42108f Fixed problem with invalid requests including spaces in URL path 2020-12-19 12:03:08 -05:00
Anonymous 24bb1387d6 Update README.md (#806) 2020-12-19 11:12:44 -05:00
Jeremie Rahm d0bd4afb0b Ensure socket is closed after processing in SSLServer (#804) 2020-12-18 19:29:36 -05:00
Yuri Santos 78ea786abd [PR] Special function to encode query params (#801)
* Special function to encode query params

* Fix #include <iomanip>

* Added unescaped charsets to encode_query_param

* Unit tests for encode_query_param
2020-12-18 17:51:11 -05:00
Miosame 9cac2c9ceb typo: specitic => specific (#802) 2020-12-18 15:12:21 -05:00
Anonymous 0cff3245df Extend built-in extension MIME mapping (#799)
* Update README.md

* Update httplib.h

* Update httplib.h

* Update httplib.h

* Update httplib.h

* Remove duplicate cases

Someone left a bunch of duplicate cases, idiot, couldn't have been me.

* Reformat

Modify spacing and whatnot

* Update README.md
2020-12-18 09:32:19 -05:00
yhirose 0e3925db3f Fixed build error 2020-12-18 00:07:48 +00:00
yhirose c9a13d214b Changed not to use string_view 2020-12-17 18:48:27 -05:00
yhirose 0954af2d4c Use user-defined literals for file extention match 2020-12-17 18:27:04 -05:00
yhirose 7c1c952f5a Don't allow invalid status code format (It sould be a three-digit code.) 2020-12-15 20:25:24 -05:00
yhirose a6edfc730a Added a unit test for static file with range 2020-12-15 18:47:51 -05:00
yhirose c1264bfedc Fix problem with mp4 w/ Range header 2020-12-14 22:41:05 -05:00
yhirose 90a5b6ceb0 Updated README 2020-12-04 19:39:39 -05:00
yhirose eb240ad2e5 Code cleanup 2020-12-03 16:03:12 -05:00
3 changed files with 221 additions and 75 deletions
+53 -27
View File
@@ -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
-------
+115 -48
View File
@@ -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 &params) {
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
+53
View File
@@ -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;