mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61c418048d | |||
| 9720ef8c34 | |||
| 978a4f6345 | |||
| 80fb03628b | |||
| 2480c0342c | |||
| eb6f610a45 | |||
| cb74e4191b | |||
| dfa641ca41 | |||
| 969a9f99d5 | |||
| c099b42ba3 | |||
| b8315278cb | |||
| 485f8f2411 | |||
| 953e4f3841 | |||
| adf65cfe61 | |||
| 12c829f6d3 | |||
| 913314f1b1 | |||
| ef63f97afe | |||
| bda74db01d | |||
| 9ff3ff9446 | |||
| c75d071615 | |||
| b4989130da | |||
| 4fc0303bda | |||
| 3d9cc51851 | |||
| f69587656f | |||
| d5fc340c30 | |||
| d79a547dc9 | |||
| bd1da4346a | |||
| 4c2a608a0c | |||
| ee4eb8deaa | |||
| 7196ac8a07 |
@@ -8,8 +8,8 @@ jobs:
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: install brotli
|
||||
run: sudo apt-get update && sudo apt-get install -y libbrotli-dev
|
||||
- name: install libraries
|
||||
run: sudo apt-get update && sudo apt-get install -y libbrotli-dev libcurl4-openssl-dev
|
||||
- name: build and run tests
|
||||
run: cd test && make -j4
|
||||
- name: run fuzz test target
|
||||
|
||||
@@ -21,6 +21,9 @@ test/test.xcodeproj/*/xcuser*
|
||||
test/*.o
|
||||
test/*.pem
|
||||
test/*.srl
|
||||
work/
|
||||
benchmark/server
|
||||
benchmark/server-crow
|
||||
|
||||
*.swp
|
||||
|
||||
@@ -34,6 +37,7 @@ Release
|
||||
*.db
|
||||
ipch
|
||||
*.dSYM
|
||||
*.pyc
|
||||
.*
|
||||
!/.gitattributes
|
||||
!/.travis.yml
|
||||
|
||||
@@ -857,6 +857,19 @@ Dockerfile for static HTTP server is available. Port number of this HTTP server
|
||||
> docker build -t cpp-httplib-server .
|
||||
...
|
||||
|
||||
> docker run --rm -it -p 8080:80 -v ./docker/html:/html cpp-httplib-server
|
||||
Serving HTTP on 0.0.0.0 port 80 ...
|
||||
192.168.65.1 - - [31/Aug/2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1"
|
||||
192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET / HTTP/1.1" 200 599 "-" "Mozilla/5.0 ..."
|
||||
192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0 ..."
|
||||
```
|
||||
|
||||
From Docker Hub
|
||||
|
||||
```bash
|
||||
> docker run --rm -it -p 8080:80 -v ./docker/html:/html yhirose4dockerhub/cpp-httplib-server
|
||||
...
|
||||
|
||||
> docker run --init --rm -it -p 8080:80 -v ./docker/html:/html cpp-httplib-server
|
||||
Serving HTTP on 0.0.0.0 port 80 ...
|
||||
192.168.65.1 - - [31/Aug/2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
CXXFLAGS = -std=c++11 -O2 -I..
|
||||
|
||||
THEAD_POOL_COUNT = 16
|
||||
|
||||
BENCH_CMD = bombardier -c 8 -d 5s localhost:8080
|
||||
# BENCH_CMD = wrk -d 5s http://localhost:8080
|
||||
|
||||
# cpp-httplib
|
||||
bench: server
|
||||
@./server & export PID=$$!; $(BENCH_CMD); kill $${PID}
|
||||
|
||||
run : server
|
||||
@./server
|
||||
|
||||
server : cpp-httplib/main.cpp ../httplib.h
|
||||
g++ -o $@ $(CXXFLAGS) -DCPPHTTPLIB_THREAD_POOL_COUNT=$(THEAD_POOL_COUNT) cpp-httplib/main.cpp
|
||||
|
||||
# crow
|
||||
bench-crow: server-crow
|
||||
@./server-crow & export PID=$$!; $(BENCH_CMD); kill $${PID}
|
||||
|
||||
run-crow : server-crow
|
||||
@./server-crow
|
||||
|
||||
server-crow : crow/main.cpp
|
||||
g++ -o $@ $(CXXFLAGS) crow/main.cpp
|
||||
|
||||
# flask
|
||||
bench-flask:
|
||||
@FLASK_APP=flask/main.py flask run --port=8080 & export PID=$$!; $(BENCH_CMD); kill $${PID}
|
||||
|
||||
run-flask:
|
||||
@FLASK_APP=flask/main.py flask run --port=8080
|
||||
|
||||
# misc
|
||||
bench-all: bench bench-crow bench-flask
|
||||
|
||||
clean:
|
||||
rm -rf server*
|
||||
@@ -0,0 +1,12 @@
|
||||
#include "httplib.h"
|
||||
using namespace httplib;
|
||||
|
||||
int main() {
|
||||
Server svr;
|
||||
|
||||
svr.Get("/", [](const Request &, Response &res) {
|
||||
res.set_content("Hello World!", "text/plain");
|
||||
});
|
||||
|
||||
svr.listen("0.0.0.0", 8080);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
#include "crow_all.h"
|
||||
|
||||
class CustomLogger : public crow::ILogHandler {
|
||||
public:
|
||||
void log(std::string, crow::LogLevel) {}
|
||||
};
|
||||
|
||||
int main() {
|
||||
CustomLogger logger;
|
||||
crow::logger::setHandler(&logger);
|
||||
|
||||
crow::SimpleApp app;
|
||||
|
||||
CROW_ROUTE(app, "/")([]() { return "Hello world!"; });
|
||||
|
||||
app.port(8080).multithreaded().run();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
from flask import Flask
|
||||
app = Flask(__name__)
|
||||
|
||||
import logging
|
||||
logging.getLogger('werkzeug').disabled = True
|
||||
|
||||
@app.route('/')
|
||||
def hello_world():
|
||||
return 'Hello, World!'
|
||||
@@ -8,7 +8,7 @@
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.17.1"
|
||||
#define CPPHTTPLIB_VERSION "0.17.3"
|
||||
|
||||
/*
|
||||
* Configuration
|
||||
@@ -321,16 +321,62 @@ make_unique(std::size_t n) {
|
||||
return std::unique_ptr<T>(new RT[n]);
|
||||
}
|
||||
|
||||
struct ci {
|
||||
bool operator()(const std::string &s1, const std::string &s2) const {
|
||||
return std::lexicographical_compare(s1.begin(), s1.end(), s2.begin(),
|
||||
s2.end(),
|
||||
[](unsigned char c1, unsigned char c2) {
|
||||
return ::tolower(c1) < ::tolower(c2);
|
||||
});
|
||||
namespace case_ignore {
|
||||
|
||||
inline unsigned char to_lower(int c) {
|
||||
const static unsigned char table[256] = {
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
|
||||
30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
|
||||
45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
|
||||
60, 61, 62, 63, 64, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106,
|
||||
107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121,
|
||||
122, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104,
|
||||
105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
|
||||
120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134,
|
||||
135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149,
|
||||
150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164,
|
||||
165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179,
|
||||
180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 224, 225, 226,
|
||||
227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241,
|
||||
242, 243, 244, 245, 246, 215, 248, 249, 250, 251, 252, 253, 254, 223, 224,
|
||||
225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
|
||||
240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254,
|
||||
255,
|
||||
};
|
||||
return table[(unsigned char)(char)c];
|
||||
}
|
||||
|
||||
inline bool equal(const std::string &a, const std::string &b) {
|
||||
return a.size() == b.size() &&
|
||||
std::equal(a.begin(), a.end(), b.begin(),
|
||||
[](char a, char b) { return to_lower(a) == to_lower(b); });
|
||||
}
|
||||
|
||||
struct equal_to {
|
||||
bool operator()(const std::string &a, const std::string &b) const {
|
||||
return equal(a, b);
|
||||
}
|
||||
};
|
||||
|
||||
struct hash {
|
||||
size_t operator()(const std::string &key) const {
|
||||
return hash_core(key.data(), key.size(), 0);
|
||||
}
|
||||
|
||||
size_t hash_core(const char *s, size_t l, size_t h) const {
|
||||
return (l == 0) ? h
|
||||
: hash_core(s + 1, l - 1,
|
||||
// Unsets the 6 high bits of h, therefore no
|
||||
// overflow happens
|
||||
(((std::numeric_limits<size_t>::max)() >> 6) &
|
||||
h * 33) ^
|
||||
static_cast<unsigned char>(to_lower(*s)));
|
||||
}
|
||||
};
|
||||
|
||||
}; // namespace case_ignore
|
||||
|
||||
// This is based on
|
||||
// "http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189".
|
||||
|
||||
@@ -436,7 +482,9 @@ enum StatusCode {
|
||||
NetworkAuthenticationRequired_511 = 511,
|
||||
};
|
||||
|
||||
using Headers = std::multimap<std::string, std::string, detail::ci>;
|
||||
using Headers =
|
||||
std::unordered_multimap<std::string, std::string, detail::case_ignore::hash,
|
||||
detail::case_ignore::equal_to>;
|
||||
|
||||
using Params = std::multimap<std::string, std::string>;
|
||||
using Match = std::smatch;
|
||||
@@ -659,8 +707,6 @@ public:
|
||||
virtual void get_local_ip_and_port(std::string &ip, int &port) const = 0;
|
||||
virtual socket_t socket() const = 0;
|
||||
|
||||
template <typename... Args>
|
||||
ssize_t write_format(const char *fmt, const Args &...args);
|
||||
ssize_t write(const char *ptr);
|
||||
ssize_t write(const std::string &s);
|
||||
};
|
||||
@@ -804,7 +850,6 @@ public:
|
||||
bool match(Request &request) const override;
|
||||
|
||||
private:
|
||||
static constexpr char marker = ':';
|
||||
// Treat segment separators as the end of path parameter capture
|
||||
// Does not need to handle query parameters as they are parsed before path
|
||||
// matching
|
||||
@@ -1951,30 +1996,6 @@ inline uint64_t Response::get_header_value_u64(const std::string &key,
|
||||
return detail::get_header_value_u64(headers, key, def, id);
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
inline ssize_t Stream::write_format(const char *fmt, const Args &...args) {
|
||||
const auto bufsiz = 2048;
|
||||
std::array<char, bufsiz> buf{};
|
||||
|
||||
auto sn = snprintf(buf.data(), buf.size() - 1, fmt, args...);
|
||||
if (sn <= 0) { return sn; }
|
||||
|
||||
auto n = static_cast<size_t>(sn);
|
||||
|
||||
if (n >= buf.size() - 1) {
|
||||
std::vector<char> glowable_buf(buf.size());
|
||||
|
||||
while (n >= glowable_buf.size() - 1) {
|
||||
glowable_buf.resize(glowable_buf.size() * 2);
|
||||
n = static_cast<size_t>(
|
||||
snprintf(&glowable_buf[0], glowable_buf.size() - 1, fmt, args...));
|
||||
}
|
||||
return write(&glowable_buf[0], n);
|
||||
} else {
|
||||
return write(buf.data(), n);
|
||||
}
|
||||
}
|
||||
|
||||
inline void default_socket_options(socket_t sock) {
|
||||
int opt = 1;
|
||||
#ifdef _WIN32
|
||||
@@ -2872,7 +2893,7 @@ inline bool mmap::open(const char *path) {
|
||||
// See:
|
||||
// https://github.com/yhirose/cpp-httplib/issues/1903#issuecomment-2316520721
|
||||
if (static_cast<ULONGLONG>(size.QuadPart) >
|
||||
std::numeric_limits<decltype(size_)>::max()) {
|
||||
(std::numeric_limits<decltype(size_)>::max)()) {
|
||||
// `size_t` might be 32-bits, on 32-bits Windows.
|
||||
return false;
|
||||
}
|
||||
@@ -2967,7 +2988,7 @@ template <typename T> inline ssize_t handle_EINTR(T fn) {
|
||||
while (true) {
|
||||
res = fn();
|
||||
if (res < 0 && errno == EINTR) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{1});
|
||||
std::this_thread::sleep_for(std::chrono::microseconds{1});
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
@@ -3178,25 +3199,6 @@ private:
|
||||
};
|
||||
#endif
|
||||
|
||||
inline bool keep_alive(socket_t sock, time_t keep_alive_timeout_sec) {
|
||||
using namespace std::chrono;
|
||||
auto start = steady_clock::now();
|
||||
while (true) {
|
||||
auto val = select_read(sock, 0, 10000);
|
||||
if (val < 0) {
|
||||
return false;
|
||||
} else if (val == 0) {
|
||||
auto current = steady_clock::now();
|
||||
auto duration = duration_cast<milliseconds>(current - start);
|
||||
auto timeout = keep_alive_timeout_sec * 1000;
|
||||
if (duration.count() > timeout) { return false; }
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline bool
|
||||
process_server_socket_core(const std::atomic<socket_t> &svr_sock, socket_t sock,
|
||||
@@ -3206,7 +3208,7 @@ process_server_socket_core(const std::atomic<socket_t> &svr_sock, socket_t sock,
|
||||
auto ret = false;
|
||||
auto count = keep_alive_max_count;
|
||||
while (svr_sock != INVALID_SOCKET && count > 0 &&
|
||||
keep_alive(sock, keep_alive_timeout_sec)) {
|
||||
select_read(sock, keep_alive_timeout_sec, 0) > 0) {
|
||||
auto close_connection = count == 1;
|
||||
auto connection_closed = false;
|
||||
ret = callback(close_connection, connection_closed);
|
||||
@@ -3250,7 +3252,7 @@ inline int shutdown_socket(socket_t sock) {
|
||||
#endif
|
||||
}
|
||||
|
||||
inline std::string escape_abstract_namespace_unix_domain(const std::string& s) {
|
||||
inline std::string escape_abstract_namespace_unix_domain(const std::string &s) {
|
||||
if (s.size() > 1 && s[0] == '\0') {
|
||||
auto ret = s;
|
||||
ret[0] = '@';
|
||||
@@ -3259,7 +3261,8 @@ inline std::string escape_abstract_namespace_unix_domain(const std::string& s) {
|
||||
return s;
|
||||
}
|
||||
|
||||
inline std::string unescape_abstract_namespace_unix_domain(const std::string& s) {
|
||||
inline std::string
|
||||
unescape_abstract_namespace_unix_domain(const std::string &s) {
|
||||
if (s.size() > 1 && s[0] == '@') {
|
||||
auto ret = s;
|
||||
ret[0] = '\0';
|
||||
@@ -4003,14 +4006,6 @@ inline const char *get_header_value(const Headers &headers,
|
||||
return def;
|
||||
}
|
||||
|
||||
inline bool compare_case_ignore(const std::string &a, const std::string &b) {
|
||||
if (a.size() != b.size()) { return false; }
|
||||
for (size_t i = 0; i < b.size(); i++) {
|
||||
if (::tolower(a[i]) != ::tolower(b[i])) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline bool parse_header(const char *beg, const char *end, T fn) {
|
||||
// Skip trailing spaces and tabs.
|
||||
@@ -4038,7 +4033,7 @@ inline bool parse_header(const char *beg, const char *end, T fn) {
|
||||
if (!key_len) { return false; }
|
||||
|
||||
auto key = std::string(beg, key_end);
|
||||
auto val = compare_case_ignore(key, "Location")
|
||||
auto val = case_ignore::equal(key, "Location")
|
||||
? std::string(p, end)
|
||||
: decode_url(std::string(p, end), false);
|
||||
|
||||
@@ -4073,17 +4068,15 @@ inline bool read_headers(Stream &strm, Headers &headers) {
|
||||
if (line_reader.end_with_crlf()) {
|
||||
// Blank line indicates end of headers.
|
||||
if (line_reader.size() == 2) { break; }
|
||||
#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
|
||||
} else {
|
||||
#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
|
||||
// Blank line indicates end of headers.
|
||||
if (line_reader.size() == 1) { break; }
|
||||
line_terminator_len = 1;
|
||||
}
|
||||
#else
|
||||
} else {
|
||||
continue; // Skip invalid line.
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
|
||||
|
||||
@@ -4205,7 +4198,7 @@ inline bool read_content_chunked(Stream &strm, T &x,
|
||||
}
|
||||
|
||||
inline bool is_chunked_transfer_encoding(const Headers &headers) {
|
||||
return compare_case_ignore(
|
||||
return case_ignore::equal(
|
||||
get_header_value(headers, "Transfer-Encoding", "", 0), "chunked");
|
||||
}
|
||||
|
||||
@@ -4288,13 +4281,36 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
} // namespace detail
|
||||
}
|
||||
|
||||
inline ssize_t write_request_line(Stream &strm, const std::string &method,
|
||||
const std::string &path) {
|
||||
std::string s = method;
|
||||
s += " ";
|
||||
s += path;
|
||||
s += " HTTP/1.1\r\n";
|
||||
return strm.write(s.data(), s.size());
|
||||
}
|
||||
|
||||
inline ssize_t write_response_line(Stream &strm, int status) {
|
||||
std::string s = "HTTP/1.1 ";
|
||||
s += std::to_string(status);
|
||||
s += " ";
|
||||
s += httplib::status_message(status);
|
||||
s += "\r\n";
|
||||
return strm.write(s.data(), s.size());
|
||||
}
|
||||
|
||||
inline ssize_t write_headers(Stream &strm, const Headers &headers) {
|
||||
ssize_t write_len = 0;
|
||||
for (const auto &x : headers) {
|
||||
auto len =
|
||||
strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str());
|
||||
std::string s;
|
||||
s = x.first;
|
||||
s += ": ";
|
||||
s += x.second;
|
||||
s += "\r\n";
|
||||
|
||||
auto len = strm.write(s.data(), s.size());
|
||||
if (len < 0) { return len; }
|
||||
write_len += len;
|
||||
}
|
||||
@@ -4821,7 +4837,9 @@ private:
|
||||
const std::string &b) const {
|
||||
if (a.size() < b.size()) { return false; }
|
||||
for (size_t i = 0; i < b.size(); i++) {
|
||||
if (::tolower(a[i]) != ::tolower(b[i])) { return false; }
|
||||
if (case_ignore::to_lower(a[i]) != case_ignore::to_lower(b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -4904,16 +4922,6 @@ private:
|
||||
size_t buf_epos_ = 0;
|
||||
};
|
||||
|
||||
inline std::string to_lower(const char *beg, const char *end) {
|
||||
std::string out;
|
||||
auto it = beg;
|
||||
while (it != end) {
|
||||
out += static_cast<char>(::tolower(*it));
|
||||
it++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::string random_string(size_t length) {
|
||||
static const char data[] =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
@@ -5844,6 +5852,8 @@ inline socket_t BufferStream::socket() const { return 0; }
|
||||
inline const std::string &BufferStream::get_buffer() const { return buffer; }
|
||||
|
||||
inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern) {
|
||||
static constexpr char marker[] = "/:";
|
||||
|
||||
// One past the last ending position of a path param substring
|
||||
std::size_t last_param_end = 0;
|
||||
|
||||
@@ -5856,13 +5866,14 @@ inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern) {
|
||||
#endif
|
||||
|
||||
while (true) {
|
||||
const auto marker_pos = pattern.find(marker, last_param_end);
|
||||
const auto marker_pos = pattern.find(
|
||||
marker, last_param_end == 0 ? last_param_end : last_param_end - 1);
|
||||
if (marker_pos == std::string::npos) { break; }
|
||||
|
||||
static_fragments_.push_back(
|
||||
pattern.substr(last_param_end, marker_pos - last_param_end));
|
||||
pattern.substr(last_param_end, marker_pos - last_param_end + 1));
|
||||
|
||||
const auto param_name_start = marker_pos + 1;
|
||||
const auto param_name_start = marker_pos + 2;
|
||||
|
||||
auto sep_pos = pattern.find(separator, param_name_start);
|
||||
if (sep_pos == std::string::npos) { sep_pos = pattern.length(); }
|
||||
@@ -5924,7 +5935,7 @@ inline bool PathParamsMatcher::match(Request &request) const {
|
||||
request.path_params.emplace(
|
||||
param_name, request.path.substr(starting_pos, sep_pos - starting_pos));
|
||||
|
||||
// Mark everythin up to '/' as matched
|
||||
// Mark everything up to '/' as matched
|
||||
starting_pos = sep_pos + 1;
|
||||
}
|
||||
// Returns false if the path is longer than the pattern
|
||||
@@ -6287,23 +6298,24 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
||||
if (close_connection || req.get_header_value("Connection") == "close") {
|
||||
res.set_header("Connection", "close");
|
||||
} else {
|
||||
std::stringstream ss;
|
||||
ss << "timeout=" << keep_alive_timeout_sec_
|
||||
<< ", max=" << keep_alive_max_count_;
|
||||
res.set_header("Keep-Alive", ss.str());
|
||||
std::string s = "timeout=";
|
||||
s += std::to_string(keep_alive_timeout_sec_);
|
||||
s += ", max=";
|
||||
s += std::to_string(keep_alive_max_count_);
|
||||
res.set_header("Keep-Alive", s);
|
||||
}
|
||||
|
||||
if (!res.has_header("Content-Type") &&
|
||||
(!res.body.empty() || res.content_length_ > 0 || res.content_provider_)) {
|
||||
if ((!res.body.empty() || res.content_length_ > 0 || res.content_provider_) &&
|
||||
!res.has_header("Content-Type")) {
|
||||
res.set_header("Content-Type", "text/plain");
|
||||
}
|
||||
|
||||
if (!res.has_header("Content-Length") && res.body.empty() &&
|
||||
!res.content_length_ && !res.content_provider_) {
|
||||
if (res.body.empty() && !res.content_length_ && !res.content_provider_ &&
|
||||
!res.has_header("Content-Length")) {
|
||||
res.set_header("Content-Length", "0");
|
||||
}
|
||||
|
||||
if (!res.has_header("Accept-Ranges") && req.method == "HEAD") {
|
||||
if (req.method == "HEAD" && !res.has_header("Accept-Ranges")) {
|
||||
res.set_header("Accept-Ranges", "bytes");
|
||||
}
|
||||
|
||||
@@ -6312,12 +6324,7 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
||||
// Response line and headers
|
||||
{
|
||||
detail::BufferStream bstrm;
|
||||
|
||||
if (!bstrm.write_format("HTTP/1.1 %d %s\r\n", res.status,
|
||||
status_message(res.status))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!detail::write_response_line(bstrm, res.status)) { return false; }
|
||||
if (!header_writer_(bstrm, res.headers)) { return false; }
|
||||
|
||||
// Flush buffer
|
||||
@@ -6511,6 +6518,11 @@ inline bool Server::handle_file_request(const Request &req, Response &res,
|
||||
auto path = entry.base_dir + sub_path;
|
||||
if (path.back() == '/') { path += "index.html"; }
|
||||
|
||||
if (detail::is_dir(path)) {
|
||||
res.set_redirect(sub_path + "/", StatusCode::MovedPermanently_301);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (detail::is_file(path)) {
|
||||
for (const auto &kv : entry.headers) {
|
||||
res.set_header(kv.first, kv.second);
|
||||
@@ -6622,7 +6634,7 @@ inline bool Server::listen_internal() {
|
||||
if (errno == EMFILE) {
|
||||
// The per-process limit of open file descriptors has been reached.
|
||||
// Try to accept new connections after a short sleep.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
std::this_thread::sleep_for(std::chrono::microseconds{1});
|
||||
continue;
|
||||
} else if (errno == EINTR || errno == EAGAIN) {
|
||||
continue;
|
||||
@@ -6972,10 +6984,12 @@ Server::process_request(Stream &strm, bool close_connection,
|
||||
switch (status) {
|
||||
case StatusCode::Continue_100:
|
||||
case StatusCode::ExpectationFailed_417:
|
||||
strm.write_format("HTTP/1.1 %d %s\r\n\r\n", status,
|
||||
status_message(status));
|
||||
detail::write_response_line(strm, status);
|
||||
strm.write("\r\n");
|
||||
break;
|
||||
default: return write_response(strm, close_connection, req, res);
|
||||
default:
|
||||
connection_closed = true;
|
||||
return write_response(strm, true, req, res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7066,8 +7080,7 @@ inline ClientImpl::ClientImpl(const std::string &host, int port,
|
||||
const std::string &client_key_path)
|
||||
: host_(detail::escape_abstract_namespace_unix_domain(host)), port_(port),
|
||||
host_and_port_(adjust_host_string(host_) + ":" + std::to_string(port)),
|
||||
client_cert_path_(client_cert_path), client_key_path_(client_key_path) {
|
||||
}
|
||||
client_cert_path_(client_cert_path), client_key_path_(client_key_path) {}
|
||||
|
||||
inline ClientImpl::~ClientImpl() {
|
||||
std::lock_guard<std::mutex> guard(socket_mutex_);
|
||||
@@ -7582,7 +7595,7 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||
detail::BufferStream bstrm;
|
||||
|
||||
const auto &path = url_encode_ ? detail::encode_url(req.path) : req.path;
|
||||
bstrm.write_format("%s %s HTTP/1.1\r\n", req.method.c_str(), path.c_str());
|
||||
detail::write_request_line(bstrm, req.method, path);
|
||||
|
||||
header_writer_(bstrm, req.headers);
|
||||
|
||||
@@ -8683,7 +8696,7 @@ inline void ssl_delete(std::mutex &ctx_mutex, SSL *ssl, socket_t sock,
|
||||
|
||||
auto ret = SSL_shutdown(ssl);
|
||||
while (ret == 0) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{100});
|
||||
ret = SSL_shutdown(ssl);
|
||||
}
|
||||
#endif
|
||||
@@ -8790,7 +8803,7 @@ inline ssize_t SSLSocketStream::read(char *ptr, size_t size) {
|
||||
if (SSL_pending(ssl_) > 0) {
|
||||
return SSL_read(ssl_, ptr, static_cast<int>(size));
|
||||
} else if (is_readable()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
std::this_thread::sleep_for(std::chrono::microseconds{10});
|
||||
ret = SSL_read(ssl_, ptr, static_cast<int>(size));
|
||||
if (ret >= 0) { return ret; }
|
||||
err = SSL_get_error(ssl_, ret);
|
||||
@@ -8821,7 +8834,7 @@ inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) {
|
||||
while (--n >= 0 && err == SSL_ERROR_WANT_WRITE) {
|
||||
#endif
|
||||
if (is_writable()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
std::this_thread::sleep_for(std::chrono::microseconds{10});
|
||||
ret = SSL_write(ssl_, ptr, static_cast<int>(handle_size));
|
||||
if (ret >= 0) { return ret; }
|
||||
err = SSL_get_error(ssl_, ret);
|
||||
|
||||
+3
-1
@@ -24,9 +24,11 @@ else()
|
||||
FetchContent_MakeAvailable(gtest)
|
||||
endif()
|
||||
|
||||
find_package(CURL REQUIRED)
|
||||
|
||||
add_executable(httplib-test test.cc)
|
||||
target_compile_options(httplib-test PRIVATE "$<$<CXX_COMPILER_ID:MSVC>:/utf-8;/bigobj>")
|
||||
target_link_libraries(httplib-test PRIVATE httplib GTest::gtest_main)
|
||||
target_link_libraries(httplib-test PRIVATE httplib GTest::gtest_main CURL::libcurl)
|
||||
gtest_discover_tests(httplib-test)
|
||||
|
||||
file(
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
|
||||
BROTLI_DIR = $(PREFIX)/opt/brotli
|
||||
BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
|
||||
|
||||
TEST_ARGS = gtest/gtest-all.cc gtest/gtest_main.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) -pthread
|
||||
TEST_ARGS = gtest/gtest-all.cc gtest/gtest_main.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) -pthread -lcurl
|
||||
|
||||
# By default, use standalone_fuzz_target_runner.
|
||||
# This runner does no fuzzing, but simply executes the inputs
|
||||
|
||||
+3
-1
@@ -3,6 +3,7 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
gtest_dep = dependency('gtest', main: true)
|
||||
libcurl_dep = dependency('libcurl')
|
||||
openssl = find_program('openssl')
|
||||
test_conf = files('test.conf')
|
||||
|
||||
@@ -119,7 +120,8 @@ test(
|
||||
'test.cc',
|
||||
dependencies: [
|
||||
cpp_httplib_dep,
|
||||
gtest_dep
|
||||
gtest_dep,
|
||||
libcurl_dep
|
||||
],
|
||||
override_options: test_options
|
||||
),
|
||||
|
||||
+153
@@ -1,6 +1,9 @@
|
||||
#include <httplib.h>
|
||||
#include <signal.h>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <curl/curl.h>
|
||||
#endif
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <atomic>
|
||||
@@ -12,6 +15,7 @@
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#define SERVER_CERT_FILE "./cert.pem"
|
||||
#define SERVER_CERT2_FILE "./cert2.pem"
|
||||
@@ -5064,6 +5068,43 @@ TEST(MountTest, Unmount) {
|
||||
EXPECT_EQ(StatusCode::NotFound_404, res->status);
|
||||
}
|
||||
|
||||
TEST(MountTest, Redicect) {
|
||||
Server svr;
|
||||
|
||||
auto listen_thread = std::thread([&svr]() { svr.listen("localhost", PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
listen_thread.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.set_mount_point("/", "./www");
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", PORT);
|
||||
|
||||
auto res = cli.Get("/dir/");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
|
||||
res = cli.Get("/dir");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::MovedPermanently_301, res->status);
|
||||
|
||||
res = cli.Get("/file");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
|
||||
res = cli.Get("/file/");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::NotFound_404, res->status);
|
||||
|
||||
cli.set_follow_location(true);
|
||||
res = cli.Get("/dir");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
||||
TEST(ExceptionTest, ThrowExceptionInHandler) {
|
||||
Server svr;
|
||||
@@ -7558,6 +7599,18 @@ TEST(PathParamsTest, SequenceOfParams) {
|
||||
EXPECT_EQ(request.path_params, expected_params);
|
||||
}
|
||||
|
||||
TEST(PathParamsTest, SemicolonInTheMiddleIsNotAParam) {
|
||||
const auto pattern = "/prefix:suffix";
|
||||
detail::PathParamsMatcher matcher(pattern);
|
||||
|
||||
Request request;
|
||||
request.path = "/prefix:suffix";
|
||||
ASSERT_TRUE(matcher.match(request));
|
||||
|
||||
const std::unordered_map<std::string, std::string> expected_params = {};
|
||||
EXPECT_EQ(request.path_params, expected_params);
|
||||
}
|
||||
|
||||
TEST(UniversalClientImplTest, Ipv6LiteralAddress) {
|
||||
// If ipv6 regex working, regex match codepath is taken.
|
||||
// else port will default to 80 in Client impl
|
||||
@@ -7604,3 +7657,103 @@ TEST(DirtyDataRequestTest, HeadFieldValueContains_CR_LF_NUL) {
|
||||
Client cli(HOST, PORT);
|
||||
cli.Get("/test", {{"Test", "_\n\r_\n\r_"}});
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
TEST(Expect100ContinueTest, ServerClosesConnection) {
|
||||
static constexpr char reject[] = "Unauthorized";
|
||||
static constexpr char accept[] = "Upload accepted";
|
||||
constexpr size_t total_size = 10 * 1024 * 1024 * 1024ULL;
|
||||
|
||||
Server svr;
|
||||
|
||||
svr.set_expect_100_continue_handler([](const Request &/*req*/, Response &res) {
|
||||
res.status = StatusCode::Unauthorized_401;
|
||||
res.set_content(reject, "text/plain");
|
||||
return res.status;
|
||||
});
|
||||
svr.Post("/", [&](const Request & /*req*/, Response &res) {
|
||||
res.set_content(accept, "text/plain");
|
||||
});
|
||||
|
||||
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
thread.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
{
|
||||
const auto curl = std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>{
|
||||
curl_easy_init(), &curl_easy_cleanup};
|
||||
ASSERT_NE(curl, nullptr);
|
||||
|
||||
curl_easy_setopt(curl.get(), CURLOPT_URL, HOST);
|
||||
curl_easy_setopt(curl.get(), CURLOPT_PORT, PORT);
|
||||
curl_easy_setopt(curl.get(), CURLOPT_POST, 1L);
|
||||
auto list = std::unique_ptr<curl_slist, decltype(&curl_slist_free_all)>{
|
||||
curl_slist_append(nullptr, "Content-Type: application/octet-stream"),
|
||||
&curl_slist_free_all};
|
||||
ASSERT_NE(list, nullptr);
|
||||
curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, list.get());
|
||||
|
||||
struct read_data {
|
||||
size_t read_size;
|
||||
size_t total_size;
|
||||
} data = {0, total_size};
|
||||
using read_callback_t =
|
||||
size_t (*)(char *ptr, size_t size, size_t nmemb, void *userdata);
|
||||
read_callback_t read_callback = [](char *ptr, size_t size, size_t nmemb,
|
||||
void *userdata) -> size_t {
|
||||
read_data *data = (read_data *)userdata;
|
||||
|
||||
if (!userdata || data->read_size >= data->total_size) { return 0; }
|
||||
|
||||
std::fill_n(ptr, size * nmemb, 'A');
|
||||
data->read_size += size * nmemb;
|
||||
return size * nmemb;
|
||||
};
|
||||
curl_easy_setopt(curl.get(), CURLOPT_READDATA, data);
|
||||
curl_easy_setopt(curl.get(), CURLOPT_READFUNCTION, read_callback);
|
||||
|
||||
std::vector<char> buffer;
|
||||
curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &buffer);
|
||||
using write_callback_t =
|
||||
size_t (*)(char *ptr, size_t size, size_t nmemb, void *userdata);
|
||||
write_callback_t write_callback = [](char *ptr, size_t size, size_t nmemb,
|
||||
void *userdata) -> size_t {
|
||||
std::vector<char> *buffer = (std::vector<char> *)userdata;
|
||||
buffer->reserve(buffer->size() + size * nmemb + 1);
|
||||
buffer->insert(buffer->end(), (char *)ptr, (char *)ptr + size * nmemb);
|
||||
return size * nmemb;
|
||||
};
|
||||
curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, write_callback);
|
||||
|
||||
{
|
||||
const auto res = curl_easy_perform(curl.get());
|
||||
ASSERT_EQ(res, CURLE_OK);
|
||||
}
|
||||
|
||||
{
|
||||
auto response_code = long{};
|
||||
const auto res =
|
||||
curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &response_code);
|
||||
ASSERT_EQ(res, CURLE_OK);
|
||||
ASSERT_EQ(response_code, StatusCode::Unauthorized_401);
|
||||
}
|
||||
|
||||
{
|
||||
auto dl = curl_off_t{};
|
||||
const auto res = curl_easy_getinfo(curl.get(), CURLINFO_SIZE_DOWNLOAD_T, &dl);
|
||||
ASSERT_EQ(res, CURLE_OK);
|
||||
ASSERT_EQ(dl, (curl_off_t)sizeof reject - 1);
|
||||
}
|
||||
|
||||
{
|
||||
buffer.push_back('\0');
|
||||
ASSERT_STRCASEEQ(buffer.data(), reject);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
file
|
||||
Reference in New Issue
Block a user