flatten 20260225
This commit is contained in:
9
tjp/core/http/Component.h
Executable file
9
tjp/core/http/Component.h
Executable file
@@ -0,0 +1,9 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
struct Component;
|
||||
|
||||
} // namespace
|
||||
25
tjp/core/http/Component.hpp
Executable file
25
tjp/core/http/Component.hpp
Executable file
@@ -0,0 +1,25 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Component.h"
|
||||
#include "Method.h"
|
||||
#include "Types.h"
|
||||
|
||||
#include "Server.h"
|
||||
#include <tjp/core/string/String.hpp>
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
struct Component
|
||||
{
|
||||
using Server = http::Server;
|
||||
using method = http::method;
|
||||
|
||||
virtual void use(Server &, const String &urlPrefix) = 0;
|
||||
virtual void onBegin() {}
|
||||
virtual void onEnd() {}
|
||||
} ;
|
||||
|
||||
} // namespace
|
||||
|
||||
21
tjp/core/http/Method.h
Executable file
21
tjp/core/http/Method.h
Executable file
@@ -0,0 +1,21 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <tjp/core/string/String.h>
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
using Method = String;
|
||||
|
||||
struct method
|
||||
{
|
||||
static constexpr auto GET = "GET";
|
||||
static constexpr auto PUT = "PUT";
|
||||
static constexpr auto POST = "POST";
|
||||
static constexpr auto HEAD = "HEAD";
|
||||
static constexpr auto INFO = "INFO";
|
||||
static constexpr auto OPTIONS = "OPTIONS";
|
||||
} ;
|
||||
|
||||
} // namespace
|
||||
9
tjp/core/http/Method.hpp
Executable file
9
tjp/core/http/Method.hpp
Executable file
@@ -0,0 +1,9 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Method.h"
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
} // namespace
|
||||
9
tjp/core/http/Server.cpp
Executable file
9
tjp/core/http/Server.cpp
Executable file
@@ -0,0 +1,9 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#if defined(TJP_CORE_HEADER_ONLY)
|
||||
#pragma once
|
||||
#endif
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
}
|
||||
10
tjp/core/http/Server.h
Executable file
10
tjp/core/http/Server.h
Executable file
@@ -0,0 +1,10 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
struct Server;
|
||||
struct Component;
|
||||
|
||||
} // namespace
|
||||
96
tjp/core/http/Server.hpp
Executable file
96
tjp/core/http/Server.hpp
Executable file
@@ -0,0 +1,96 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Server.h"
|
||||
#include "Component.h"
|
||||
#include "Types.hpp"
|
||||
#include <tjp/core/header_only/compile.h>
|
||||
|
||||
#include <tjp/core/ptr/Ptr.h>
|
||||
#include <tjp/core/string/String.h>
|
||||
#include <tjp/core/string/StringView.h>
|
||||
#include <tjp/core/containers/Function.h>
|
||||
|
||||
#include "as_future.hpp"
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
struct Server
|
||||
{
|
||||
virtual ~Server() {}
|
||||
|
||||
virtual void add(
|
||||
const StrongPtr<Component> &component,
|
||||
const String &urlPrefix = {}
|
||||
) = 0;
|
||||
|
||||
template<typename ... Args>
|
||||
void addAll(Args && ...args)
|
||||
{
|
||||
(add(std::forward<Args>(args)), ...);
|
||||
}
|
||||
|
||||
virtual void setNumThreads(size_t) = 0;
|
||||
virtual void start(const String &bindTo, int port) = 0;
|
||||
virtual void run(const String &bindTo, int port) = 0;
|
||||
virtual void stop() = 0;
|
||||
|
||||
virtual bool isRunning() = 0;
|
||||
|
||||
virtual void bind(
|
||||
const Method &method,
|
||||
Matcher &&matcher,
|
||||
FutureHandler &&
|
||||
) = 0;
|
||||
|
||||
void bind(
|
||||
const Method &method,
|
||||
const Pattern &pattern,
|
||||
FutureHandler &&handler
|
||||
)
|
||||
{
|
||||
bind(
|
||||
method,
|
||||
match_regex(String(pattern)),
|
||||
std::move(handler)
|
||||
);
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
void bind_with(
|
||||
const Method &method,
|
||||
Matcher matcher,
|
||||
F &&f
|
||||
)
|
||||
{
|
||||
bind(
|
||||
method,
|
||||
std::move(matcher),
|
||||
as_future(
|
||||
std::forward<F>(f)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
void bind_with(
|
||||
const Method &method,
|
||||
const Pattern &pattern,
|
||||
F &&f
|
||||
)
|
||||
{
|
||||
bind_with(
|
||||
method,
|
||||
match_regex(String(pattern)),
|
||||
std::forward<F>(f)
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#ifdef TJP_CORE_HEADER_ONLY
|
||||
#include "Server.cpp"
|
||||
#endif
|
||||
43
tjp/core/http/Types.h
Executable file
43
tjp/core/http/Types.h
Executable file
@@ -0,0 +1,43 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Method.h"
|
||||
#include <tjp/core/containers/StackArray.h>
|
||||
#include <tjp/core/containers/Function.h>
|
||||
#include <tjp/core/containers/Optional.h>
|
||||
#include <tjp/core/containers/Vector.h>
|
||||
#include <tjp/core/string/String.h>
|
||||
#include <tjp/core/string/StringView.h>
|
||||
|
||||
#include <tjp/core/future/Future.h>
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
using Status = int;
|
||||
|
||||
struct DataSink;
|
||||
struct PipeState;
|
||||
|
||||
struct HeaderView;
|
||||
struct Header;
|
||||
|
||||
using Headers = Vector<Header>;
|
||||
using Matches = StackArray<StringView, 4>;
|
||||
|
||||
struct Request;
|
||||
struct Response;
|
||||
|
||||
struct Result;
|
||||
using BodyType = Result;
|
||||
|
||||
using FutureHandler = core::Function<Future<Result>(const Request &)>;
|
||||
using Matcher = core::Function<Optional<Matches>(StringView)>;
|
||||
using Pattern = StringView;
|
||||
|
||||
Matcher match_exact(const String &value);
|
||||
Matcher match_prefix(const String &prefix);
|
||||
|
||||
Matcher match_regex(const String &pattern);
|
||||
|
||||
} // namespace
|
||||
385
tjp/core/http/Types.hpp
Executable file
385
tjp/core/http/Types.hpp
Executable file
@@ -0,0 +1,385 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Types.h"
|
||||
#include "Method.hpp"
|
||||
#include <tjp/core/header_only/compile.h>
|
||||
|
||||
#include <tjp/core/ptr/Ptr.h>
|
||||
#include <tjp/core/string/String.h>
|
||||
#include <tjp/core/string/StringView.h>
|
||||
#include <tjp/core/containers/Function.h>
|
||||
#include <tjp/core/containers/Optional.h>
|
||||
#include <tjp/core/containers/Map.h>
|
||||
#include <tjp/core/containers/Tuple.h>
|
||||
#include <tjp/core/containers/Vector.h>
|
||||
#include <tjp/core/future/Future.h>
|
||||
#include <tjp/core/containers/StackArray.hpp>
|
||||
|
||||
#include <tjp/core/string/cmp_case.hpp>
|
||||
|
||||
#include <any>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <regex>
|
||||
|
||||
#include <tjp/core/string/starts_with.hpp>
|
||||
#include "../internet/url_param_decode.hpp"
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
using Status = int;
|
||||
|
||||
struct DataSink
|
||||
{
|
||||
std::any preserve;
|
||||
|
||||
virtual ~DataSink() = default;
|
||||
virtual void write(const char *data, size_t size) = 0;
|
||||
virtual void done() = 0;
|
||||
} ;
|
||||
|
||||
struct PipeState :
|
||||
std::enable_shared_from_this<PipeState>
|
||||
{
|
||||
struct Sink : DataSink
|
||||
{
|
||||
std::weak_ptr<PipeState> state;
|
||||
|
||||
explicit Sink(std::weak_ptr<PipeState> state_) :
|
||||
state(std::move(state_))
|
||||
{}
|
||||
|
||||
void write(const char *data, size_t size) override
|
||||
{
|
||||
if (auto state_ = state.lock())
|
||||
state_->onData(data, size);
|
||||
}
|
||||
|
||||
void done() override
|
||||
{
|
||||
if (auto state_ = state.lock())
|
||||
state_->onDone();
|
||||
}
|
||||
} ;
|
||||
|
||||
static std::shared_ptr<PipeState> make(Optional<size_t> requestSize = {})
|
||||
{
|
||||
auto state = std::shared_ptr<PipeState>(new PipeState(requestSize));
|
||||
state->sink_ = std::make_shared<Sink>(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
std::shared_ptr<DataSink> sink() const
|
||||
{
|
||||
return sink_;
|
||||
}
|
||||
|
||||
void setWake(core::Function<void()> wake)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
wake_ = std::move(wake);
|
||||
}
|
||||
|
||||
std::shared_ptr<String> popChunk()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
if (chunks.empty())
|
||||
return {};
|
||||
|
||||
auto out = chunks.front();
|
||||
chunks.pop_front();
|
||||
return out;
|
||||
}
|
||||
|
||||
bool popOrWait(String &chunk, bool &isEnded)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
cv.wait(lock, [&]() {
|
||||
return !chunks.empty() || ended;
|
||||
});
|
||||
|
||||
if (!chunks.empty())
|
||||
{
|
||||
chunk = *chunks.front();
|
||||
chunks.pop_front();
|
||||
isEnded = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
isEnded = ended;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isEnded() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return ended;
|
||||
}
|
||||
|
||||
Optional<size_t> size() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return requestSize;
|
||||
}
|
||||
|
||||
private:
|
||||
explicit PipeState(Optional<size_t> requestSize_) :
|
||||
requestSize(std::move(requestSize_))
|
||||
{}
|
||||
|
||||
void onData(const char *data, size_t size)
|
||||
{
|
||||
if (size == 0)
|
||||
return;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
if (ended)
|
||||
return;
|
||||
|
||||
chunks.emplace_back(std::make_shared<String>(data, size));
|
||||
}
|
||||
|
||||
cv.notify_one();
|
||||
notifyWake();
|
||||
}
|
||||
|
||||
void onDone()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
ended = true;
|
||||
}
|
||||
|
||||
cv.notify_all();
|
||||
notifyWake();
|
||||
}
|
||||
|
||||
void notifyWake()
|
||||
{
|
||||
core::Function<void()> wake;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
wake = wake_;
|
||||
}
|
||||
|
||||
if (wake)
|
||||
wake();
|
||||
}
|
||||
|
||||
Optional<size_t> requestSize;
|
||||
std::shared_ptr<Sink> sink_;
|
||||
|
||||
mutable std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::deque<std::shared_ptr<String>> chunks;
|
||||
bool ended = false;
|
||||
core::Function<void()> wake_;
|
||||
} ;
|
||||
|
||||
struct HeaderView {
|
||||
StringView key;
|
||||
StringView value;
|
||||
} ;
|
||||
|
||||
struct Header {
|
||||
String key;
|
||||
String value;
|
||||
} ;
|
||||
|
||||
using Headers = Vector<Header>;
|
||||
using Matches = StackArray<StringView, 4>;
|
||||
|
||||
inline
|
||||
Optional<StringView> path_param_(const StringView &path, const StringView ¶m)
|
||||
{
|
||||
auto q = path.find('?');
|
||||
if (q == path.npos)
|
||||
return {};
|
||||
|
||||
auto l = q;
|
||||
while (true)
|
||||
{
|
||||
auto p = path.find(param, l);
|
||||
if (p == path.npos)
|
||||
return {};
|
||||
|
||||
auto before = path[p-1];
|
||||
if (before == '?' || before == '&')
|
||||
{
|
||||
// if the path ends with ¶m instead of ¶m=
|
||||
if (path.size() <= p + param.size())
|
||||
return { StringView() };
|
||||
|
||||
auto b = p + param.size();
|
||||
auto after = path[b];
|
||||
if (after == '=' || after == '&')
|
||||
{
|
||||
if (after == '=')
|
||||
b++;
|
||||
|
||||
auto e = path.find('&', b);
|
||||
if (e == path.npos)
|
||||
e = path.size();
|
||||
|
||||
return path.substr(b, e - b);
|
||||
}
|
||||
}
|
||||
|
||||
l = p + param.size();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
inline
|
||||
Optional<String> path_param(const StringView &path, const StringView ¶m)
|
||||
{
|
||||
if (auto encoded_param = path_param_(path, param))
|
||||
return internet::url_param_decode(*encoded_param);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
struct Request
|
||||
{
|
||||
StringView method;
|
||||
StringView target;
|
||||
Vector<HeaderView> headers;
|
||||
Matches matches;
|
||||
|
||||
StringView body;
|
||||
|
||||
Optional<StringView> header(const StringView &key_) const
|
||||
{
|
||||
for (auto &[key, value]: headers)
|
||||
if (core::equal_case(key, key_))
|
||||
return value;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Optional<String> param(const StringView &key_) const
|
||||
{
|
||||
return path_param(target, key_);
|
||||
}
|
||||
};
|
||||
|
||||
struct Response
|
||||
{
|
||||
Status status = 200;
|
||||
Vector<Header> headers;
|
||||
|
||||
bool hasHeader(const StringView &key)
|
||||
{
|
||||
for (auto &[key_, _]: headers)
|
||||
if (core::equal_case(key, key_))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void header(const String &key, const String &value)
|
||||
{
|
||||
headers.emplace_back(Header {key, value});
|
||||
}
|
||||
|
||||
void content_length(size_t size)
|
||||
{
|
||||
header("Content-Length", std::to_string(size));
|
||||
}
|
||||
|
||||
virtual void flushHeaders(const StringView &method) = 0;
|
||||
virtual void write(const StringView &) = 0;
|
||||
virtual std::shared_ptr<DataSink> stream() = 0;
|
||||
|
||||
std::shared_ptr<DataSink> stream(size_t size)
|
||||
{
|
||||
content_length(size);
|
||||
return stream();
|
||||
}
|
||||
} ;
|
||||
|
||||
struct Result
|
||||
{
|
||||
Optional<String> body;
|
||||
Optional<String> type;
|
||||
Optional<Status> status;
|
||||
Vector<Header> headers;
|
||||
|
||||
std::shared_ptr<DataSink> stream;
|
||||
std::shared_ptr<PipeState> streamOwner;
|
||||
Optional<size_t> streamSize;
|
||||
|
||||
std::shared_ptr<DataSink> stream_body()
|
||||
{
|
||||
streamOwner = PipeState::make();
|
||||
streamSize = {};
|
||||
stream = streamOwner->sink();
|
||||
return stream;
|
||||
}
|
||||
|
||||
std::shared_ptr<DataSink> stream_body(size_t size)
|
||||
{
|
||||
streamOwner = PipeState::make(size);
|
||||
streamSize = size;
|
||||
stream = streamOwner->sink();
|
||||
return stream;
|
||||
}
|
||||
} ;
|
||||
|
||||
using FutureHandler = core::Function<Future<Result>(const Request &)>;
|
||||
using Matcher = core::Function<Optional<Matches>(StringView)>;
|
||||
using Pattern = StringView;
|
||||
|
||||
inline Matcher match_exact(const String &value)
|
||||
{
|
||||
auto value_ = String(value);
|
||||
return [value_ = std::move(value_)](StringView url) -> Optional<Matches> {
|
||||
if (url == value_)
|
||||
return Matches({ url });
|
||||
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
inline Matcher match_prefix(const String &prefix)
|
||||
{
|
||||
auto prefix_ = String(prefix);
|
||||
return [prefix_ = std::move(prefix_)](StringView url) -> Optional<Matches> {
|
||||
if (core::starts_with(url, prefix_))
|
||||
return Matches { url.substr(prefix_.size()) };
|
||||
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
inline Matcher match_regex(const String &pattern)
|
||||
{
|
||||
auto regex = std::make_shared<std::regex>(String(pattern));
|
||||
return [regex = std::move(regex), pattern](StringView url) -> Optional<Matches> {
|
||||
std::string s(url);
|
||||
std::match_results<std::string::const_iterator> m;
|
||||
|
||||
if (!std::regex_match(s, m, *regex))
|
||||
return {};
|
||||
|
||||
Matches out;
|
||||
|
||||
for (size_t i = 0; i < m.size(); ++i) {
|
||||
auto pos = m.position(i);
|
||||
auto len = m.length(i);
|
||||
out.emplace_back(url.data() + pos, len);
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
}
|
||||
using BodyType = Result;
|
||||
|
||||
} // namespace
|
||||
21
tjp/core/http/as_future.hpp
Executable file
21
tjp/core/http/as_future.hpp
Executable file
@@ -0,0 +1,21 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Types.hpp"
|
||||
#include "future_response.hpp"
|
||||
#include <tjp/core/future/Future.hpp>
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
template<typename F>
|
||||
auto as_future(F &&f)
|
||||
{
|
||||
return
|
||||
[f=std::move(f)](const Request& req)
|
||||
{
|
||||
return future_with(f, req);
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
985
tjp/core/http/beast/Server.cpp
Normal file
985
tjp/core/http/beast/Server.cpp
Normal file
@@ -0,0 +1,985 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#if defined(TJP_CORE_HEADER_ONLY)
|
||||
#pragma once
|
||||
#endif
|
||||
#include <tjp/core/header_only/compile.h>
|
||||
|
||||
#include "Server.hpp"
|
||||
|
||||
#include "beast.hpp"
|
||||
|
||||
#include <tjp/core/containers/Map.h>
|
||||
#include <tjp/core/containers/Vector.h>
|
||||
#include <tjp/core/exception/Exception.hpp>
|
||||
|
||||
#include <tjp/core/future/Futures.hpp>
|
||||
|
||||
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <cstdio>
|
||||
|
||||
namespace tjp::core::http::beast {
|
||||
|
||||
namespace net = boost::asio;
|
||||
namespace beast_ = boost::beast;
|
||||
namespace http_ = beast_::http;
|
||||
using tcp = net::ip::tcp;
|
||||
|
||||
struct RequestAdapter :
|
||||
http::Request,
|
||||
std::enable_shared_from_this<RequestAdapter>
|
||||
{
|
||||
String methodStorage;
|
||||
String targetStorage;
|
||||
String bodyStorage;
|
||||
Vector<Header> headerStorage;
|
||||
|
||||
RequestAdapter(const http_::request<http_::string_body> &in)
|
||||
{
|
||||
methodStorage = String(in.method_string());
|
||||
targetStorage = String(in.target());
|
||||
bodyStorage = in.body();
|
||||
|
||||
method = methodStorage;
|
||||
target = targetStorage;
|
||||
body = bodyStorage;
|
||||
|
||||
auto headerCount = static_cast<size_t>(std::distance(in.base().begin(), in.base().end()));
|
||||
headerStorage.reserve(headerCount);
|
||||
headers.reserve(headerCount);
|
||||
for (const auto &field: in.base())
|
||||
{
|
||||
headerStorage.emplace_back(Header {
|
||||
String(field.name_string()),
|
||||
String(field.value())
|
||||
});
|
||||
auto &header = headerStorage.back();
|
||||
headers.emplace_back(HeaderView {
|
||||
header.key,
|
||||
header.value
|
||||
});
|
||||
}
|
||||
}
|
||||
} ;
|
||||
|
||||
using StreamState = http::PipeState;
|
||||
|
||||
struct ResponseAdapter :
|
||||
http::Response,
|
||||
std::enable_shared_from_this<ResponseAdapter>
|
||||
{
|
||||
String body;
|
||||
bool deferred = false;
|
||||
std::shared_ptr<RequestAdapter> requestHolder;
|
||||
std::shared_ptr<StreamState> streamState;
|
||||
std::shared_ptr<core::Function<void(std::shared_ptr<ResponseAdapter>)>> onReady;
|
||||
|
||||
void notifyReady()
|
||||
{
|
||||
if (onReady && *onReady)
|
||||
(*onReady)(shared_from_this());
|
||||
}
|
||||
|
||||
void flushHeaders(const StringView &) override
|
||||
{
|
||||
}
|
||||
|
||||
void write(const StringView &data) override
|
||||
{
|
||||
body = String(data);
|
||||
}
|
||||
|
||||
std::shared_ptr<http::DataSink> stream() override
|
||||
{
|
||||
Optional<size_t> size;
|
||||
for (const auto &header: headers)
|
||||
{
|
||||
if (header.key != "Content-Length")
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
size = static_cast<size_t>(std::stoull(String(header.value)));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// ignore malformed values and keep scanning
|
||||
}
|
||||
}
|
||||
|
||||
deferred = true;
|
||||
streamState = StreamState::make(size);
|
||||
return streamState->sink();
|
||||
}
|
||||
|
||||
http_::response<http_::string_body> toBeast(
|
||||
unsigned version,
|
||||
bool keepAlive
|
||||
)
|
||||
{
|
||||
http_::response<http_::string_body> out {
|
||||
static_cast<http_::status>(status),
|
||||
version
|
||||
};
|
||||
|
||||
std::optional<String> contentType;
|
||||
std::optional<String> contentLength;
|
||||
|
||||
out.keep_alive(keepAlive);
|
||||
for (auto &header: headers)
|
||||
{
|
||||
if (header.key == "Content-Length")
|
||||
{
|
||||
contentLength = header.value;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (header.key == "Content-Type")
|
||||
{
|
||||
contentType = header.value;
|
||||
continue;
|
||||
}
|
||||
|
||||
out.set(String(header.key), String(header.value));
|
||||
}
|
||||
|
||||
if (!contentType)
|
||||
contentType = "text/plain";
|
||||
|
||||
if (contentType)
|
||||
out.set(http_::field::content_type, *contentType);
|
||||
|
||||
out.body() = body;
|
||||
|
||||
if (contentLength)
|
||||
out.set(http_::field::content_length, *contentLength);
|
||||
else
|
||||
out.prepare_payload();
|
||||
|
||||
return out;
|
||||
}
|
||||
} ;
|
||||
|
||||
static Optional<size_t> readContentLengthFromHeaders(const Vector<Header> &headers)
|
||||
{
|
||||
for (const auto &header: headers)
|
||||
{
|
||||
if (header.key != "Content-Length")
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
return static_cast<size_t>(std::stoull(String(header.value)));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Ignore malformed content-length and keep scanning.
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static void applyResult(ResponseAdapter &response, Result result)
|
||||
{
|
||||
response.status = result.status.value_or(200);
|
||||
|
||||
for (auto &[key, value]: result.headers)
|
||||
response.header(key, value);
|
||||
|
||||
if (result.type)
|
||||
response.header("Content-Type", *result.type);
|
||||
|
||||
auto streamSize = result.streamSize;
|
||||
if (!streamSize)
|
||||
streamSize = readContentLengthFromHeaders(response.headers);
|
||||
if (streamSize && !response.hasHeader("Content-Length"))
|
||||
response.content_length(*streamSize);
|
||||
|
||||
if (result.streamOwner)
|
||||
{
|
||||
response.deferred = true;
|
||||
response.streamState = std::move(result.streamOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.stream)
|
||||
{
|
||||
response.status = 500;
|
||||
response.header("Content-Type", "text/plain");
|
||||
response.write("Invalid stream owner");
|
||||
return;
|
||||
}
|
||||
|
||||
response.write(result.body.value_or(String {}));
|
||||
}
|
||||
|
||||
struct Server::I
|
||||
{
|
||||
struct Route
|
||||
{
|
||||
Method method;
|
||||
Matcher matcher;
|
||||
FutureHandler handler;
|
||||
} ;
|
||||
|
||||
struct Session;
|
||||
struct Listener;
|
||||
|
||||
net::io_context io;
|
||||
std::unique_ptr<tcp::acceptor> acceptor;
|
||||
std::shared_ptr<Listener> listener;
|
||||
using WorkGuard = net::executor_work_guard<net::io_context::executor_type>;
|
||||
std::unique_ptr<WorkGuard> workGuard;
|
||||
|
||||
std::mutex routesMutex;
|
||||
Vector<Route> routes;
|
||||
Vector<StrongPtr<Component>> components;
|
||||
Futures futures;
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
std::optional<size_t> numThreads;
|
||||
|
||||
std::atomic<bool> running = false;
|
||||
std::atomic<bool> listening = false;
|
||||
|
||||
size_t threadCount() const
|
||||
{
|
||||
if (numThreads && *numThreads > 0)
|
||||
return *numThreads;
|
||||
|
||||
auto n = std::thread::hardware_concurrency();
|
||||
return n == 0 ? 1 : n;
|
||||
}
|
||||
|
||||
bool findHandler(
|
||||
Request &request,
|
||||
FutureHandler &out
|
||||
)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(routesMutex);
|
||||
for (const auto &route: routes)
|
||||
{
|
||||
if (route.method != request.method)
|
||||
continue;
|
||||
|
||||
if (route.matcher)
|
||||
if (auto matches = route.matcher(request.target))
|
||||
{
|
||||
request.matches = *matches;
|
||||
out = route.handler;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
struct Server::I::Session : std::enable_shared_from_this<Session>
|
||||
{
|
||||
I *i;
|
||||
beast_::tcp_stream stream;
|
||||
beast_::flat_buffer buffer;
|
||||
http_::request<http_::string_body> request;
|
||||
std::shared_ptr<ResponseAdapter> activeStreamResponse;
|
||||
std::optional<http_::response<http_::empty_body>> activeStreamHeaderMessage;
|
||||
std::optional<http_::response_serializer<http_::empty_body>> activeStreamHeader;
|
||||
std::shared_ptr<String> activeStreamPayload;
|
||||
std::shared_ptr<String> activeStreamChunkWire;
|
||||
size_t activeStreamBytesWritten = 0;
|
||||
bool activeStreamHeaderSent = false;
|
||||
bool activeStreamWriteInFlight = false;
|
||||
bool activeStreamChunked = false;
|
||||
bool activeStreamHeadOnly = false;
|
||||
|
||||
Session(I *i_, tcp::socket &&socket) :
|
||||
i(i_),
|
||||
stream(std::move(socket))
|
||||
{}
|
||||
|
||||
void run()
|
||||
{
|
||||
doRead();
|
||||
}
|
||||
|
||||
void doRead()
|
||||
{
|
||||
request = {};
|
||||
http_::async_read(
|
||||
stream,
|
||||
buffer,
|
||||
request,
|
||||
[self = shared_from_this()](beast_::error_code ec, size_t bytes) {
|
||||
self->onRead(ec, bytes);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void onRead(beast_::error_code ec, size_t)
|
||||
{
|
||||
if (ec == http_::error::end_of_stream)
|
||||
{
|
||||
doClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ec)
|
||||
{
|
||||
doClose();
|
||||
return;
|
||||
}
|
||||
|
||||
handleRequest();
|
||||
}
|
||||
|
||||
void handleRequest()
|
||||
{
|
||||
auto request_ = std::make_shared<RequestAdapter>(request);
|
||||
auto response = std::make_shared<ResponseAdapter>();
|
||||
response->requestHolder = request_;
|
||||
response->onReady = std::make_shared<core::Function<void(std::shared_ptr<ResponseAdapter>)>>(
|
||||
[self = shared_from_this()](std::shared_ptr<ResponseAdapter> response_) {
|
||||
net::dispatch(
|
||||
self->stream.get_executor(),
|
||||
[self, response_]() {
|
||||
if (response_->streamState)
|
||||
self->startStreaming(response_);
|
||||
else
|
||||
self->send(response_->toBeast(self->request.version(), self->request.keep_alive()));
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
FutureHandler handler;
|
||||
if (i->findHandler(*request_, handler))
|
||||
{
|
||||
response->deferred = true;
|
||||
auto future = handler(*request_).then([response, request_](auto &&future_) {
|
||||
(void)request_;
|
||||
try
|
||||
{
|
||||
applyResult(*response, future_.get());
|
||||
}
|
||||
catch (Result &result)
|
||||
{
|
||||
applyResult(*response, Result(result));
|
||||
}
|
||||
catch (Exception &e)
|
||||
{
|
||||
response->status = 400;
|
||||
response->header("Content-Type", "text/plain");
|
||||
response->write("Error: " + e.what());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
response->status = 400;
|
||||
response->header("Content-Type", "text/plain");
|
||||
response->write("Error");
|
||||
}
|
||||
|
||||
response->notifyReady();
|
||||
});
|
||||
|
||||
i->futures.add(future);
|
||||
}
|
||||
else
|
||||
{
|
||||
response->status = 404;
|
||||
response->header("Content-Type", "text/plain");
|
||||
response->write("Not found");
|
||||
response->notifyReady();
|
||||
}
|
||||
}
|
||||
catch (Result &result)
|
||||
{
|
||||
applyResult(*response, Result(result));
|
||||
response->notifyReady();
|
||||
}
|
||||
catch (Exception &e)
|
||||
{
|
||||
response->status = 400;
|
||||
response->header("Content-Type", "text/plain");
|
||||
response->write("Error: " + e.what());
|
||||
response->notifyReady();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
response->status = 400;
|
||||
response->header("Content-Type", "text/plain");
|
||||
response->write("Error");
|
||||
response->notifyReady();
|
||||
}
|
||||
}
|
||||
|
||||
void send(http_::response<http_::string_body> &&response)
|
||||
{
|
||||
auto keepAlive = response.keep_alive();
|
||||
auto sharedResponse = std::make_shared<http_::response<http_::string_body>>(std::move(response));
|
||||
http_::async_write(
|
||||
stream,
|
||||
*sharedResponse,
|
||||
[self = shared_from_this(), sharedResponse, keepAlive](beast_::error_code ec, size_t bytes) {
|
||||
self->onWrite(keepAlive, ec, bytes);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void onWrite(bool keepAlive, beast_::error_code ec, size_t)
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
doClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!keepAlive)
|
||||
{
|
||||
doClose();
|
||||
return;
|
||||
}
|
||||
|
||||
doRead();
|
||||
}
|
||||
|
||||
void clearStreamingState()
|
||||
{
|
||||
activeStreamResponse.reset();
|
||||
activeStreamHeaderMessage.reset();
|
||||
activeStreamHeader.reset();
|
||||
activeStreamPayload.reset();
|
||||
activeStreamChunkWire.reset();
|
||||
activeStreamBytesWritten = 0;
|
||||
activeStreamHeaderSent = false;
|
||||
activeStreamWriteInFlight = false;
|
||||
activeStreamChunked = false;
|
||||
activeStreamHeadOnly = false;
|
||||
}
|
||||
|
||||
void startStreaming(const std::shared_ptr<ResponseAdapter> &response)
|
||||
{
|
||||
if (!response || !response->streamState)
|
||||
return;
|
||||
|
||||
if (activeStreamResponse)
|
||||
return;
|
||||
|
||||
activeStreamResponse = response;
|
||||
activeStreamHeaderSent = false;
|
||||
activeStreamWriteInFlight = false;
|
||||
activeStreamHeaderMessage.reset();
|
||||
activeStreamHeader.reset();
|
||||
activeStreamPayload.reset();
|
||||
activeStreamChunkWire.reset();
|
||||
activeStreamBytesWritten = 0;
|
||||
activeStreamHeadOnly = request.method() == http_::verb::head;
|
||||
activeStreamChunked = !response->streamState->size().has_value() && !activeStreamHeadOnly;
|
||||
|
||||
response->streamState->setWake([self = shared_from_this()]() {
|
||||
net::dispatch(
|
||||
self->stream.get_executor(),
|
||||
[self]() {
|
||||
self->pumpStreaming();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
pumpStreaming();
|
||||
}
|
||||
|
||||
void pumpStreaming()
|
||||
{
|
||||
if (!activeStreamResponse || !activeStreamResponse->streamState)
|
||||
return;
|
||||
|
||||
if (activeStreamWriteInFlight)
|
||||
return;
|
||||
|
||||
auto response = activeStreamResponse;
|
||||
auto state = response->streamState;
|
||||
auto knownSize = state->size();
|
||||
|
||||
if (!activeStreamHeaderSent)
|
||||
{
|
||||
http_::response<http_::empty_body> header {
|
||||
static_cast<http_::status>(response->status),
|
||||
request.version()
|
||||
};
|
||||
|
||||
header.keep_alive(request.keep_alive());
|
||||
if (knownSize)
|
||||
header.content_length(*knownSize);
|
||||
else
|
||||
if (activeStreamChunked)
|
||||
header.chunked(true);
|
||||
|
||||
std::optional<String> contentType;
|
||||
for (auto &header_: response->headers)
|
||||
{
|
||||
if (header_.key == "Content-Length")
|
||||
continue;
|
||||
|
||||
if (header_.key == "Transfer-Encoding")
|
||||
continue;
|
||||
|
||||
if (header_.key == "Content-Type")
|
||||
{
|
||||
contentType = header_.value;
|
||||
continue;
|
||||
}
|
||||
|
||||
header.set(String(header_.key), String(header_.value));
|
||||
}
|
||||
|
||||
if (!contentType)
|
||||
contentType = "text/plain";
|
||||
|
||||
if (contentType)
|
||||
header.set(http_::field::content_type, *contentType);
|
||||
|
||||
activeStreamHeaderMessage.emplace(std::move(header));
|
||||
activeStreamHeader.emplace(*activeStreamHeaderMessage);
|
||||
activeStreamWriteInFlight = true;
|
||||
http_::async_write_header(
|
||||
stream,
|
||||
*activeStreamHeader,
|
||||
[self = shared_from_this()](beast_::error_code ec, size_t) {
|
||||
self->onStreamHeaderWrite(ec);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeStreamHeadOnly)
|
||||
{
|
||||
auto keepAlive = request.keep_alive();
|
||||
clearStreamingState();
|
||||
if (keepAlive)
|
||||
doRead();
|
||||
else
|
||||
doClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeStreamChunked)
|
||||
{
|
||||
if (!knownSize)
|
||||
{
|
||||
clearStreamingState();
|
||||
doClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeStreamBytesWritten >= *knownSize)
|
||||
{
|
||||
auto keepAlive = request.keep_alive();
|
||||
clearStreamingState();
|
||||
if (keepAlive)
|
||||
doRead();
|
||||
else
|
||||
doClose();
|
||||
return;
|
||||
}
|
||||
|
||||
auto chunk = state->popChunk();
|
||||
if (chunk)
|
||||
{
|
||||
auto remaining = *knownSize - activeStreamBytesWritten;
|
||||
if (chunk->size() > remaining)
|
||||
chunk->resize(remaining);
|
||||
|
||||
activeStreamPayload = chunk;
|
||||
activeStreamWriteInFlight = true;
|
||||
net::async_write(
|
||||
stream,
|
||||
net::buffer(*activeStreamPayload),
|
||||
[self = shared_from_this(), payload = activeStreamPayload](beast_::error_code ec, size_t bytes) {
|
||||
(void)payload;
|
||||
self->onStreamChunkWrite(ec, bytes);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state->isEnded())
|
||||
{
|
||||
// Producer ended early without delivering promised size.
|
||||
clearStreamingState();
|
||||
doClose();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto chunk = state->popChunk();
|
||||
if (chunk)
|
||||
{
|
||||
char sizeHex[32] = {};
|
||||
auto length = std::snprintf(sizeHex, sizeof(sizeHex), "%zx", chunk->size());
|
||||
if (length < 0)
|
||||
{
|
||||
clearStreamingState();
|
||||
doClose();
|
||||
return;
|
||||
}
|
||||
activeStreamChunkWire = std::make_shared<String>();
|
||||
activeStreamChunkWire->append(sizeHex, static_cast<size_t>(length));
|
||||
activeStreamChunkWire->append("\r\n", 2);
|
||||
activeStreamChunkWire->append(*chunk);
|
||||
activeStreamChunkWire->append("\r\n", 2);
|
||||
activeStreamWriteInFlight = true;
|
||||
net::async_write(
|
||||
stream,
|
||||
net::buffer(*activeStreamChunkWire),
|
||||
[self = shared_from_this(), chunkWire = activeStreamChunkWire](beast_::error_code ec, size_t) {
|
||||
(void)chunkWire;
|
||||
self->onStreamChunkedWrite(ec);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state->isEnded())
|
||||
{
|
||||
activeStreamChunkWire = std::make_shared<String>("0\r\n\r\n");
|
||||
activeStreamWriteInFlight = true;
|
||||
net::async_write(
|
||||
stream,
|
||||
net::buffer(*activeStreamChunkWire),
|
||||
[self = shared_from_this(), chunkWire = activeStreamChunkWire](beast_::error_code ec, size_t) {
|
||||
(void)chunkWire;
|
||||
self->onStreamChunkedDoneWrite(ec);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onStreamHeaderWrite(beast_::error_code ec)
|
||||
{
|
||||
activeStreamWriteInFlight = false;
|
||||
activeStreamHeaderSent = true;
|
||||
activeStreamHeaderMessage.reset();
|
||||
activeStreamHeader.reset();
|
||||
if (ec)
|
||||
{
|
||||
clearStreamingState();
|
||||
doClose();
|
||||
return;
|
||||
}
|
||||
|
||||
pumpStreaming();
|
||||
}
|
||||
|
||||
void onStreamChunkWrite(beast_::error_code ec, size_t bytes)
|
||||
{
|
||||
activeStreamWriteInFlight = false;
|
||||
activeStreamBytesWritten += bytes;
|
||||
activeStreamPayload.reset();
|
||||
if (ec)
|
||||
{
|
||||
clearStreamingState();
|
||||
doClose();
|
||||
return;
|
||||
}
|
||||
|
||||
pumpStreaming();
|
||||
}
|
||||
|
||||
void onStreamChunkedWrite(beast_::error_code ec)
|
||||
{
|
||||
activeStreamWriteInFlight = false;
|
||||
activeStreamChunkWire.reset();
|
||||
if (ec)
|
||||
{
|
||||
clearStreamingState();
|
||||
doClose();
|
||||
return;
|
||||
}
|
||||
|
||||
pumpStreaming();
|
||||
}
|
||||
|
||||
void onStreamChunkedDoneWrite(beast_::error_code ec)
|
||||
{
|
||||
activeStreamWriteInFlight = false;
|
||||
activeStreamChunkWire.reset();
|
||||
if (ec)
|
||||
{
|
||||
clearStreamingState();
|
||||
doClose();
|
||||
return;
|
||||
}
|
||||
|
||||
auto keepAlive = request.keep_alive();
|
||||
clearStreamingState();
|
||||
if (keepAlive)
|
||||
doRead();
|
||||
else
|
||||
doClose();
|
||||
}
|
||||
|
||||
void doClose()
|
||||
{
|
||||
clearStreamingState();
|
||||
beast_::error_code ec;
|
||||
stream.socket().shutdown(tcp::socket::shutdown_send, ec);
|
||||
stream.socket().close(ec);
|
||||
}
|
||||
};
|
||||
|
||||
struct Server::I::Listener : std::enable_shared_from_this<Listener>
|
||||
{
|
||||
I *i;
|
||||
|
||||
Listener(I *i_) :
|
||||
i(i_)
|
||||
{}
|
||||
|
||||
void run()
|
||||
{
|
||||
doAccept();
|
||||
}
|
||||
|
||||
void doAccept()
|
||||
{
|
||||
if (!i->acceptor || !i->running)
|
||||
return;
|
||||
|
||||
i->acceptor->async_accept(
|
||||
net::make_strand(i->io),
|
||||
[self = shared_from_this()](beast_::error_code ec, tcp::socket socket) {
|
||||
self->onAccept(ec, std::move(socket));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void onAccept(beast_::error_code ec, tcp::socket socket)
|
||||
{
|
||||
if (!i->running)
|
||||
return;
|
||||
|
||||
if (!ec)
|
||||
std::make_shared<Session>(i, std::move(socket))->run();
|
||||
else
|
||||
if (ec == net::error::operation_aborted)
|
||||
return;
|
||||
|
||||
doAccept();
|
||||
}
|
||||
};
|
||||
|
||||
static tcp::endpoint toEndpoint(const String &bindTo, int port)
|
||||
{
|
||||
beast_::error_code ec;
|
||||
auto address = net::ip::make_address(bindTo, ec);
|
||||
if (ec)
|
||||
throw Exception { "Invalid bind address" };
|
||||
|
||||
if (port < 0 || port > 65535)
|
||||
throw Exception { "Invalid port" };
|
||||
|
||||
return tcp::endpoint {
|
||||
address,
|
||||
static_cast<unsigned short>(port)
|
||||
};
|
||||
}
|
||||
|
||||
static void throwIfError(beast_::error_code ec, const char *message)
|
||||
{
|
||||
if (ec)
|
||||
throw Exception { message };
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
Server::Server()
|
||||
{
|
||||
i = strong<I>();
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
Server::~Server()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
void Server::add(const StrongPtr<Component> &component, const String &urlPrefix)
|
||||
{
|
||||
i->components.emplace_back(component);
|
||||
component->use(*this, urlPrefix);
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
void Server::setNumThreads(size_t n)
|
||||
{
|
||||
i->numThreads = n == 0 ? 1 : n;
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
void Server::start(const String &bindTo, int port)
|
||||
{
|
||||
if (i->running.exchange(true))
|
||||
throw Exception { "Webserver already running" };
|
||||
|
||||
try
|
||||
{
|
||||
i->io.restart();
|
||||
i->acceptor = std::make_unique<tcp::acceptor>(i->io);
|
||||
i->workGuard = std::make_unique<I::WorkGuard>(i->io.get_executor());
|
||||
|
||||
auto endpoint = toEndpoint(bindTo, port);
|
||||
beast_::error_code ec;
|
||||
i->acceptor->open(endpoint.protocol(), ec);
|
||||
throwIfError(ec, "Could not open socket");
|
||||
|
||||
i->acceptor->set_option(net::socket_base::reuse_address(true), ec);
|
||||
throwIfError(ec, "Could not set socket options");
|
||||
|
||||
i->acceptor->bind(endpoint, ec);
|
||||
throwIfError(ec, "Could not bind to endpoint");
|
||||
|
||||
i->acceptor->listen(net::socket_base::max_listen_connections, ec);
|
||||
throwIfError(ec, "Could not listen");
|
||||
|
||||
i->listener = std::make_shared<I::Listener>(i.get());
|
||||
i->listener->run();
|
||||
i->listening = true;
|
||||
|
||||
auto count = i->threadCount();
|
||||
i->threads.reserve(count);
|
||||
for (size_t idx = 0; idx < count; ++idx)
|
||||
{
|
||||
i->threads.emplace_back([state = i.get()]() {
|
||||
state->io.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
stop();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
void Server::run(const String &bindTo, int port)
|
||||
{
|
||||
if (i->running.exchange(true))
|
||||
throw Exception { "Webserver already running" };
|
||||
|
||||
try
|
||||
{
|
||||
i->io.restart();
|
||||
i->acceptor = std::make_unique<tcp::acceptor>(i->io);
|
||||
i->workGuard = std::make_unique<I::WorkGuard>(i->io.get_executor());
|
||||
|
||||
auto endpoint = toEndpoint(bindTo, port);
|
||||
beast_::error_code ec;
|
||||
i->acceptor->open(endpoint.protocol(), ec);
|
||||
throwIfError(ec, "Could not open socket");
|
||||
|
||||
i->acceptor->set_option(net::socket_base::reuse_address(true), ec);
|
||||
throwIfError(ec, "Could not set socket options");
|
||||
|
||||
i->acceptor->bind(endpoint, ec);
|
||||
throwIfError(ec, "Could not bind to endpoint");
|
||||
|
||||
i->acceptor->listen(net::socket_base::max_listen_connections, ec);
|
||||
throwIfError(ec, "Could not listen");
|
||||
|
||||
i->listener = std::make_shared<I::Listener>(i.get());
|
||||
i->listener->run();
|
||||
i->listening = true;
|
||||
|
||||
auto count = i->threadCount();
|
||||
if (count > 1)
|
||||
{
|
||||
i->threads.reserve(count - 1);
|
||||
for (size_t idx = 1; idx < count; ++idx)
|
||||
{
|
||||
i->threads.emplace_back([state = i.get()]() {
|
||||
state->io.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
i->io.run();
|
||||
|
||||
for (auto &thread: i->threads)
|
||||
if (thread.joinable())
|
||||
thread.join();
|
||||
|
||||
i->threads.clear();
|
||||
i->listener.reset();
|
||||
i->acceptor.reset();
|
||||
i->workGuard.reset();
|
||||
i->listening = false;
|
||||
i->running = false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
stop();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
void Server::stop()
|
||||
{
|
||||
if (!i->running.exchange(false))
|
||||
return;
|
||||
|
||||
i->listening = false;
|
||||
|
||||
if (i->acceptor)
|
||||
{
|
||||
beast_::error_code ec;
|
||||
i->acceptor->cancel(ec);
|
||||
i->acceptor->close(ec);
|
||||
}
|
||||
|
||||
i->listener.reset();
|
||||
i->workGuard.reset();
|
||||
i->io.stop();
|
||||
|
||||
for (auto &thread: i->threads)
|
||||
if (thread.joinable())
|
||||
thread.join();
|
||||
|
||||
i->threads.clear();
|
||||
i->acceptor.reset();
|
||||
i->io.restart();
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
bool Server::isRunning()
|
||||
{
|
||||
return i->running && i->listening;
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
void Server::bind(const Method &method, Matcher &&matcher, FutureHandler &&handler)
|
||||
{
|
||||
if (!matcher)
|
||||
throw Exception { "Invalid route matcher" };
|
||||
|
||||
std::lock_guard<std::mutex> lock(i->routesMutex);
|
||||
i->routes.emplace_back(I::Route {
|
||||
method,
|
||||
std::move(matcher),
|
||||
std::move(handler)
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
9
tjp/core/http/beast/Server.h
Normal file
9
tjp/core/http/beast/Server.h
Normal file
@@ -0,0 +1,9 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace tjp::core::http::beast {
|
||||
|
||||
struct Server;
|
||||
|
||||
} // namespace
|
||||
50
tjp/core/http/beast/Server.hpp
Normal file
50
tjp/core/http/beast/Server.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Server.h"
|
||||
#include "../Server.hpp"
|
||||
#include "../Component.hpp"
|
||||
#include <tjp/core/header_only/compile.h>
|
||||
|
||||
#include <tjp/core/ptr/Ptr.h>
|
||||
#include <tjp/core/string/String.h>
|
||||
|
||||
namespace tjp::core::http::beast {
|
||||
|
||||
struct Server : http::Server
|
||||
{
|
||||
using Super = http::Server;
|
||||
using Super::bind;
|
||||
|
||||
struct I;
|
||||
StrongPtr<I> i;
|
||||
|
||||
|
||||
Server();
|
||||
~Server();
|
||||
|
||||
void add(
|
||||
const StrongPtr<Component> &component,
|
||||
const String &urlPrefix = {}
|
||||
) override;
|
||||
|
||||
void setNumThreads(size_t) override;
|
||||
void start(const String &bindTo, int port) override;
|
||||
void run(const String &bindTo, int port) override;
|
||||
void stop() override;
|
||||
|
||||
bool isRunning() override;
|
||||
|
||||
void bind(
|
||||
const Method &method,
|
||||
Matcher &&matcher,
|
||||
FutureHandler &&
|
||||
) override;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#ifdef TJP_CORE_HEADER_ONLY
|
||||
#include "Server.cpp"
|
||||
#endif
|
||||
505
tjp/core/http/beast/_test/Server.cpp
Executable file
505
tjp/core/http/beast/_test/Server.cpp
Executable file
@@ -0,0 +1,505 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#include "../Server.hpp"
|
||||
#include "../../hirose/httplib.h"
|
||||
|
||||
#include <tjp/core/testing/catch.hpp>
|
||||
#include <tjp/core/testing/wait_until.h>
|
||||
#include <tjp/core/hash/Hash.h>
|
||||
#include <tjp/core/types/Types.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace tjp::core::http::beast {
|
||||
|
||||
SCENARIO("tjp::core::http::beast")
|
||||
{
|
||||
GIVEN("server")
|
||||
{
|
||||
Server server;
|
||||
server.setNumThreads(2);
|
||||
auto make_large_payload = []() {
|
||||
String data;
|
||||
data.reserve(64 * 1024);
|
||||
for (size_t i = 0; i < (64 * 1024); ++i)
|
||||
data.push_back(static_cast<char>('a' + (i % 26)));
|
||||
return data;
|
||||
};
|
||||
auto large_payload = make_large_payload();
|
||||
|
||||
server.bind_with("GET", "/hello", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.body = "hello";
|
||||
return result;
|
||||
});
|
||||
server.bind_with("POST", "/echo", [](const Request &req) -> Result {
|
||||
Result result;
|
||||
result.status = 201;
|
||||
result.type = "text/plain";
|
||||
result.body = String(req.body);
|
||||
return result;
|
||||
});
|
||||
server.bind_with("PUT", "/replace", [](const Request &req) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.body = "updated:" + String(req.body);
|
||||
return result;
|
||||
});
|
||||
server.bind_with("HEAD", "/meta", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.headers.push_back(Header { "X-Test-Head", "ok" });
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", "/empty", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.body = "";
|
||||
return result;
|
||||
});
|
||||
server.bind_with("HEAD", "/empty", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.headers.push_back(Header { "Content-Length", "0" });
|
||||
return result;
|
||||
});
|
||||
server.bind_with("HEAD", "/head-sized", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.headers.push_back(Header { "Content-Length", "6"} );
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", "/range", [](const Request &req) -> Result {
|
||||
Result result;
|
||||
String full = "0123456789";
|
||||
auto range = req.header("Range");
|
||||
if (range && *range == "bytes=2-5")
|
||||
{
|
||||
result.status = 206;
|
||||
result.headers.push_back(Header {"Content-Range", "bytes 2-5/10"});
|
||||
result.type = "text/plain";
|
||||
result.body = "2345";
|
||||
return result;
|
||||
}
|
||||
|
||||
result.type = "text/plain";
|
||||
result.body = full;
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", match_exact("/match/exact"), [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.body = "exact";
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", match_prefix("/match/prefix"), [](const Request &req) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.body = "prefix:" + String(req.target);
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", match_regex("^/match/regex/[0-9]+$"), [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.body = "regex";
|
||||
return result;
|
||||
});
|
||||
server.bind_with(
|
||||
"GET",
|
||||
[](StringView url) -> Optional<Matches>
|
||||
{ return url == "/match/lambda" ? Matches() : Optional<Matches>();
|
||||
},
|
||||
[](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.body = "lambda";
|
||||
return result;
|
||||
}
|
||||
);
|
||||
server.bind_with("GET", "/stream", [](const Request &) -> Result {
|
||||
Result result;
|
||||
const String expected = "streamed-body";
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body(expected.size());
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
out->write(expected.data(), expected.size());
|
||||
out->done();
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", "/stream-signal", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body(6);
|
||||
std::thread([out]() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
out->write("sig", 3);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
out->write("nal", 3);
|
||||
out->done();
|
||||
}).detach();
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", "/stream-empty", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body(0);
|
||||
out->done();
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", "/stream-unknown", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body();
|
||||
std::thread([out]() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
out->write("sig", 3);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
out->write("nal", 3);
|
||||
out->done();
|
||||
}).detach();
|
||||
return result;
|
||||
});
|
||||
server.bind_with("HEAD", "/stream-head-known", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body(6);
|
||||
out->write("ABCDEF", 6);
|
||||
out->done();
|
||||
return result;
|
||||
});
|
||||
server.bind_with("HEAD", "/stream-head-unknown", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body();
|
||||
out->write("ignored", 7);
|
||||
out->done();
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", "/stream-large", [large_payload](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body(large_payload.size());
|
||||
const size_t chunk = 8192;
|
||||
for (size_t offset = 0; offset < large_payload.size(); offset += chunk)
|
||||
{
|
||||
auto size = std::min(chunk, large_payload.size() - offset);
|
||||
out->write(large_payload.data() + offset, size);
|
||||
}
|
||||
out->done();
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", "/future", [](const Request &) -> Result {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(25));
|
||||
Result result;
|
||||
result.status = 202;
|
||||
result.type = "text/plain";
|
||||
result.body = "future-ok";
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", "/future-stream-known", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.status = 200;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body(6);
|
||||
std::thread([out]() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
||||
out->write("sig", 3);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
||||
out->write("nal", 3);
|
||||
out->done();
|
||||
}).detach();
|
||||
return result;
|
||||
});
|
||||
server.bind("GET", "/future-stream-unknown", FutureHandler([](const Request &) {
|
||||
Result result;
|
||||
result.status = 200;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body();
|
||||
std::thread([out]() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
||||
out->write("sig", 3);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
||||
out->write("nal", 3);
|
||||
out->done();
|
||||
}).detach();
|
||||
return future_with([result]() {
|
||||
return result;
|
||||
});
|
||||
}));
|
||||
|
||||
auto before = std::chrono::steady_clock::now();
|
||||
server.start("127.0.0.1", 4792);
|
||||
auto after = std::chrono::steady_clock::now();
|
||||
|
||||
REQUIRE((after - before) < std::chrono::seconds(1));
|
||||
core::wait_until(std::chrono::seconds(10), [&]() { return server.isRunning(); });
|
||||
|
||||
WHEN("a get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/hello");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "hello");
|
||||
}
|
||||
|
||||
WHEN("a post is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Post("/echo", "posted-body", "text/plain");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 201);
|
||||
REQUIRE(contents->body == "posted-body");
|
||||
}
|
||||
|
||||
WHEN("a put is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Put("/replace", "payload", "text/plain");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "updated:payload");
|
||||
}
|
||||
|
||||
WHEN("a head is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Head("/meta");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->get_header_value("X-Test-Head") == "ok");
|
||||
REQUIRE(contents->body.empty());
|
||||
}
|
||||
|
||||
WHEN("a get of empty content is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/empty");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body.empty());
|
||||
REQUIRE(contents->get_header_value("Content-Type") == "text/plain");
|
||||
REQUIRE(contents->get_header_value("Content-Length") == "0");
|
||||
}
|
||||
|
||||
WHEN("a head of empty content is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Head("/empty");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body.empty());
|
||||
REQUIRE(contents->get_header_value("Content-Length") == "0");
|
||||
}
|
||||
|
||||
WHEN("a head of non-empty content is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Head("/head-sized");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body.empty());
|
||||
REQUIRE(contents->get_header_value("Content-Type") == "text/plain");
|
||||
REQUIRE(contents->get_header_value("Content-Length") == "6");
|
||||
}
|
||||
|
||||
WHEN("a range request is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
httplib::Headers headers;
|
||||
headers.emplace("Range", "bytes=2-5");
|
||||
auto contents = client.Get("/range", headers);
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 206);
|
||||
REQUIRE(contents->get_header_value("Content-Range") == "bytes 2-5/10");
|
||||
REQUIRE(contents->body == "2345");
|
||||
}
|
||||
|
||||
WHEN("an exact matcher route is requested")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/match/exact");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "exact");
|
||||
}
|
||||
|
||||
WHEN("a prefix matcher route is requested")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/match/prefix/child");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "prefix:/match/prefix/child");
|
||||
}
|
||||
|
||||
WHEN("a regex matcher route is requested")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/match/regex/42");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "regex");
|
||||
}
|
||||
|
||||
WHEN("a lambda matcher route is requested")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/match/lambda");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "lambda");
|
||||
}
|
||||
|
||||
WHEN("an unknown route is requested")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/missing");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 404);
|
||||
}
|
||||
|
||||
WHEN("a future backed get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/future");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 202);
|
||||
REQUIRE(contents->body == "future-ok");
|
||||
}
|
||||
|
||||
WHEN("a future-backed known-size stream get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/future-stream-known");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "signal");
|
||||
REQUIRE(contents->get_header_value("Content-Length") == "6");
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
|
||||
}
|
||||
|
||||
WHEN("a future-backed unknown-size stream get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/future-stream-unknown");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "signal");
|
||||
REQUIRE(contents->get_header_value("Content-Length").empty());
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding") == "chunked");
|
||||
}
|
||||
|
||||
WHEN("a stream backed get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/stream");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "streamed-body");
|
||||
REQUIRE(contents->get_header_value("Content-Type") == "text/plain");
|
||||
REQUIRE(contents->get_header_value("Content-Length") == "13");
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
|
||||
}
|
||||
|
||||
WHEN("a signal-backed stream get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/stream-signal");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "signal");
|
||||
REQUIRE(contents->get_header_value("Content-Length") == "6");
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
|
||||
}
|
||||
|
||||
WHEN("a zero-sized stream get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/stream-empty");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body.empty());
|
||||
REQUIRE(contents->get_header_value("Content-Type") == "text/plain");
|
||||
REQUIRE(contents->get_header_value("Content-Length") == "0");
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
|
||||
}
|
||||
|
||||
WHEN("an unknown-sized stream get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/stream-unknown");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "signal");
|
||||
REQUIRE(contents->get_header_value("Content-Length").empty());
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding") == "chunked");
|
||||
}
|
||||
|
||||
WHEN("a head of known-sized stream is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Head("/stream-head-known");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body.empty());
|
||||
REQUIRE(contents->get_header_value("Content-Length") == "6");
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
|
||||
}
|
||||
|
||||
WHEN("a head of unknown-sized stream is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Head("/stream-head-unknown");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body.empty());
|
||||
REQUIRE(contents->get_header_value("Content-Length").empty());
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
|
||||
}
|
||||
|
||||
WHEN("a large stream get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4792");
|
||||
auto contents = client.Get("/stream-large");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body.size() > 32 * 1024);
|
||||
REQUIRE(contents->body.size() == large_payload.size());
|
||||
auto body_hash = hash_of<u64>(contents->body);
|
||||
auto expected_hash = hash_of<u64>(large_payload);
|
||||
REQUIRE(body_hash == expected_hash);
|
||||
REQUIRE(contents->get_header_value("Content-Length") == std::to_string(large_payload.size()));
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
|
||||
}
|
||||
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
25
tjp/core/http/beast/beast.hpp
Normal file
25
tjp/core/http/beast/beast.hpp
Normal file
@@ -0,0 +1,25 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#if defined __GNUC__
|
||||
#pragma GCC diagnostic push
|
||||
//#pragma GCC diagnostic ignored "-Wcomma"
|
||||
//#pragma GCC diagnostic ignored "-Wdocumentation"
|
||||
//#pragma GCC diagnostic ignored "-Wshorten-64-to-32"
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
//#pragma GCC diagnostic ignored "-W#pragma-messages"
|
||||
|
||||
#pragma clang diagnostic ignored "-Wcomma"
|
||||
#pragma clang diagnostic ignored "-Wdocumentation"
|
||||
#pragma clang diagnostic ignored "-Wshorten-64-to-32"
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
#pragma clang diagnostic ignored "-W#pragma-messages"
|
||||
#pragma clang diagnostic ignored "-Wdocumentation"
|
||||
|
||||
#endif
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/beast.hpp>
|
||||
|
||||
#if defined __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
26
tjp/core/http/collapse_future.hpp
Executable file
26
tjp/core/http/collapse_future.hpp
Executable file
@@ -0,0 +1,26 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "http.hpp"
|
||||
#include "future_response.hpp"
|
||||
#include <tjp/core/future/Future.hpp>
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
template<typename F>
|
||||
auto collapse_future(F &&f)
|
||||
{
|
||||
return
|
||||
[f=std::move(f)](const Request& req)
|
||||
{
|
||||
using R = decltype(f(req));
|
||||
|
||||
if constexpr (is_future<R>::value)
|
||||
return f(req).get();
|
||||
else
|
||||
return f(req);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
19
tjp/core/http/future_response.hpp
Executable file
19
tjp/core/http/future_response.hpp
Executable file
@@ -0,0 +1,19 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Types.hpp"
|
||||
#include <tjp/core/future/Future.hpp>
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
template<typename F>
|
||||
auto future_response(F &&f, const Request& req)
|
||||
{
|
||||
return
|
||||
future_with(
|
||||
[&]() { return f(req); }
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
404
tjp/core/http/hirose/Server.cpp
Executable file
404
tjp/core/http/hirose/Server.cpp
Executable file
@@ -0,0 +1,404 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#if defined(TJP_CORE_HEADER_ONLY)
|
||||
#pragma once
|
||||
#endif
|
||||
#include <tjp/core/header_only/compile.h>
|
||||
|
||||
#include "Server.hpp"
|
||||
|
||||
#include <tjp/core/containers/Vector.h>
|
||||
#include <tjp/core/containers/Optional.h>
|
||||
#include <tjp/core/exception/Exception.hpp>
|
||||
#include <tjp/core/algorithm/map_value.hpp>
|
||||
|
||||
#include <tjp/core/algorithm/remove_const_of_var.hpp>
|
||||
#include "../../internet/Headers.hpp"
|
||||
|
||||
#include "httplib.h"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace tjp::core::http::hirose {
|
||||
|
||||
using ServerImp = ::httplib::Server;
|
||||
|
||||
using StreamState = http::PipeState;
|
||||
|
||||
struct Request : http::Request
|
||||
{
|
||||
httplib::Request *v;
|
||||
|
||||
Request(const httplib::Request &v) {
|
||||
target = v.target;
|
||||
method = v.method;
|
||||
|
||||
headers.reserve(v.headers.size());
|
||||
for (auto &[k, v]: v.headers)
|
||||
headers.emplace_back(HeaderView { k, v });
|
||||
|
||||
body = v.body;
|
||||
};
|
||||
} ;
|
||||
|
||||
struct Response : http::Response
|
||||
{
|
||||
httplib::Response *v;
|
||||
String method;
|
||||
bool headRequest = false;
|
||||
std::shared_ptr<StreamState> streamState;
|
||||
|
||||
Optional<StringView> headerValue(const StringView &name) const
|
||||
{
|
||||
for (auto &header: headers)
|
||||
if (header.key == name)
|
||||
return header.value;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void writeHeaders(bool skipLength)
|
||||
{
|
||||
v->status = status;
|
||||
|
||||
for (auto &header: headers)
|
||||
{
|
||||
auto &key = header.key;
|
||||
auto &value = header.value;
|
||||
|
||||
if (key == "Content-Type") {}
|
||||
else
|
||||
if (key == "Content-Length" && skipLength) {}
|
||||
else
|
||||
v->set_header(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
void flushHeaders(const StringView &method) override
|
||||
{
|
||||
bool skipLength = method == "PUT" || method == "GET";
|
||||
|
||||
writeHeaders(skipLength);
|
||||
}
|
||||
|
||||
void write(const StringView &data) override
|
||||
{
|
||||
auto type = headerValue("Content-Type").value_or("text/plain");
|
||||
v->set_content(data.data(), data.size(), String(type));
|
||||
}
|
||||
|
||||
std::shared_ptr<http::DataSink> stream() override
|
||||
{
|
||||
auto type = headerValue("Content-Type").value_or("text/plain");
|
||||
Optional<size_t> size_;
|
||||
if (auto contentLength = headerValue("Content-Length"))
|
||||
{
|
||||
try
|
||||
{
|
||||
size_ = static_cast<size_t>(std::stoull(String(*contentLength)));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// ignore malformed values
|
||||
}
|
||||
}
|
||||
|
||||
if (!size_ && streamState)
|
||||
size_ = streamState->size();
|
||||
|
||||
if (!streamState)
|
||||
streamState = StreamState::make(size_);
|
||||
|
||||
auto state = streamState;
|
||||
|
||||
if (headRequest)
|
||||
{
|
||||
if (size_)
|
||||
{
|
||||
v->set_content_provider(
|
||||
*size_,
|
||||
String(type),
|
||||
[](size_t, size_t, httplib::DataSink &) {
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
v->set_content_provider(
|
||||
String(type),
|
||||
[](size_t, httplib::DataSink &) {
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return state->sink();
|
||||
}
|
||||
|
||||
if (size_)
|
||||
{
|
||||
v->set_content_provider(
|
||||
*size_,
|
||||
String(type),
|
||||
[state](size_t, size_t, httplib::DataSink &sink_) {
|
||||
String chunk;
|
||||
bool ended = false;
|
||||
if (state->popOrWait(chunk, ended))
|
||||
{
|
||||
sink_.write(chunk.data(), chunk.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ended)
|
||||
{
|
||||
// For fixed-length providers, returning true without writing
|
||||
// bytes keeps offset unchanged and can spin the write loop.
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
[state](bool) {
|
||||
if (auto sink = state->sink())
|
||||
sink->done();
|
||||
}
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
v->set_chunked_content_provider(
|
||||
String(type),
|
||||
[state](size_t, httplib::DataSink &sink_) {
|
||||
String chunk;
|
||||
bool ended = false;
|
||||
if (state->popOrWait(chunk, ended))
|
||||
{
|
||||
sink_.write(chunk.data(), chunk.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ended)
|
||||
{
|
||||
if (sink_.done)
|
||||
sink_.done();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
[state](bool) {
|
||||
if (auto sink = state->sink())
|
||||
sink->done();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return state->sink();
|
||||
}
|
||||
} ;
|
||||
|
||||
struct Server::I
|
||||
{
|
||||
struct Route
|
||||
{
|
||||
Method method;
|
||||
Matcher matcher;
|
||||
FutureHandler handler;
|
||||
} ;
|
||||
|
||||
ServerImp webServer;
|
||||
std::thread thread;
|
||||
bool running = false;
|
||||
Optional<size_t> numThreads;
|
||||
std::mutex routesMutex;
|
||||
Vector<Route> routes;
|
||||
|
||||
Vector<StrongPtr<http::Component>> components;
|
||||
|
||||
bool findHandler(
|
||||
Request &request,
|
||||
FutureHandler &out
|
||||
)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(routesMutex);
|
||||
for (const auto &route: routes)
|
||||
{
|
||||
if (route.method != request.method)
|
||||
continue;
|
||||
|
||||
if (route.matcher)
|
||||
if (auto matches = route.matcher(request.target))
|
||||
{
|
||||
request.matches = *matches;
|
||||
out = route.handler;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
static void applyResult(Response &response, Result result)
|
||||
{
|
||||
response.status = result.status.value_or(200);
|
||||
for (auto &[key, value]: result.headers)
|
||||
response.header(key, value);
|
||||
|
||||
if (result.type)
|
||||
response.header("Content-Type", *result.type);
|
||||
|
||||
if (result.streamSize && !response.hasHeader("Content-Length"))
|
||||
response.content_length(*result.streamSize);
|
||||
|
||||
if (result.streamOwner)
|
||||
{
|
||||
response.streamState = std::move(result.streamOwner);
|
||||
response.stream();
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.stream)
|
||||
{
|
||||
response.status = 500;
|
||||
response.header("Content-Type", "text/plain");
|
||||
response.write("Invalid stream owner");
|
||||
return;
|
||||
}
|
||||
|
||||
response.flushHeaders(response.method);
|
||||
response.write(result.body.value_or(String {}));
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
Server::Server()
|
||||
{
|
||||
i = strong<I>();
|
||||
|
||||
auto dispatch = [state = i.get()](const httplib::Request &request_, httplib::Response &response_) {
|
||||
Request request(request_);
|
||||
|
||||
if (request_.method == "HEAD")
|
||||
core::remove_const_of_var(request_.ranges).clear();
|
||||
|
||||
Response response;
|
||||
response.v = &response_;
|
||||
response.method = request_.method;
|
||||
response.headRequest = request_.method == "HEAD";
|
||||
|
||||
try
|
||||
{
|
||||
FutureHandler handler;
|
||||
if (state->findHandler(request, handler))
|
||||
applyResult(response, handler(request).get());
|
||||
else
|
||||
{
|
||||
response.status = 404;
|
||||
response.header("Content-Type", "text/plain");
|
||||
response.write("Not found");
|
||||
}
|
||||
}
|
||||
catch (Result &result)
|
||||
{
|
||||
applyResult(response, Result(result));
|
||||
}
|
||||
catch (Exception &e)
|
||||
{
|
||||
response.status = 400;
|
||||
response.header("Content-Type", "text/plain");
|
||||
response.write("Error: " + e.what());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
response.status = 400;
|
||||
response.header("Content-Type", "text/plain");
|
||||
response.write("Error");
|
||||
}
|
||||
};
|
||||
|
||||
i->webServer.Get(".*", dispatch);
|
||||
i->webServer.Post(".*", dispatch);
|
||||
i->webServer.Put(".*", dispatch);
|
||||
i->webServer.Options(".*", dispatch);
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
Server::~Server()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
void Server::add(const StrongPtr<Component> &component, const String &urlPrefix)
|
||||
{
|
||||
component->use(*this, urlPrefix);
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
void Server::run(const String &bindTo, int port)
|
||||
{
|
||||
if (i->numThreads)
|
||||
i->webServer.new_task_queue = [n = *i->numThreads] { return new httplib::ThreadPool(n); };
|
||||
|
||||
i->webServer.listen(bindTo, port);
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
void Server::setNumThreads(size_t n)
|
||||
{
|
||||
i->numThreads = n;
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
void Server::start(const String &bindTo, int port)
|
||||
{
|
||||
if (i->running)
|
||||
throw Exception { "Webserver already running" };
|
||||
|
||||
i->running = true;
|
||||
|
||||
i->thread = std::thread([this, bindTo, port]() {
|
||||
run(bindTo, port);
|
||||
});
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
bool Server::isRunning ()
|
||||
{
|
||||
return i->webServer.is_running();
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
void Server::stop()
|
||||
{
|
||||
if (!i->running)
|
||||
return;
|
||||
|
||||
i->webServer.stop();
|
||||
|
||||
if (i->thread.joinable())
|
||||
i->thread.join();
|
||||
|
||||
i->running = false;
|
||||
}
|
||||
|
||||
TJP_CORE_HEADER_ONLY_INLINE
|
||||
void Server::bind(const Method &method, Matcher &&matcher, FutureHandler &&handler)
|
||||
{
|
||||
if (!matcher)
|
||||
throw Exception { "Invalid route matcher" };
|
||||
|
||||
std::lock_guard<std::mutex> lock(i->routesMutex);
|
||||
i->routes.emplace_back(I::Route {
|
||||
method,
|
||||
std::move(matcher),
|
||||
std::move(handler)
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
9
tjp/core/http/hirose/Server.h
Executable file
9
tjp/core/http/hirose/Server.h
Executable file
@@ -0,0 +1,9 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace tjp::core::http::hirose {
|
||||
|
||||
struct Server;
|
||||
|
||||
} // namespace
|
||||
49
tjp/core/http/hirose/Server.hpp
Executable file
49
tjp/core/http/hirose/Server.hpp
Executable file
@@ -0,0 +1,49 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Server.h"
|
||||
#include "../Server.hpp"
|
||||
#include "../Component.hpp"
|
||||
#include <tjp/core/header_only/compile.h>
|
||||
|
||||
#include <tjp/core/ptr/Ptr.h>
|
||||
#include <tjp/core/string/String.h>
|
||||
|
||||
namespace tjp::core::http::hirose {
|
||||
|
||||
struct Server : http::Server
|
||||
{
|
||||
using Super = http::Server;
|
||||
using Super::bind;
|
||||
|
||||
struct I;
|
||||
StrongPtr<I> i;
|
||||
|
||||
Server();
|
||||
~Server();
|
||||
|
||||
void add(
|
||||
const StrongPtr<Component> &component,
|
||||
const String &urlPrefix = {}
|
||||
) override;
|
||||
|
||||
void setNumThreads(size_t) override;
|
||||
void start(const String &bindTo, int port) override;
|
||||
void run(const String &bindTo, int port) override;
|
||||
void stop() override;
|
||||
|
||||
bool isRunning() override;
|
||||
|
||||
void bind(
|
||||
const Method &method,
|
||||
Matcher &&matcher,
|
||||
FutureHandler &&
|
||||
) override;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#ifdef TJP_CORE_HEADER_ONLY
|
||||
#include "Server.cpp"
|
||||
#endif
|
||||
356
tjp/core/http/hirose/_test/Server.cpp
Executable file
356
tjp/core/http/hirose/_test/Server.cpp
Executable file
@@ -0,0 +1,356 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#include "../Server.hpp"
|
||||
#include "../httplib.h"
|
||||
|
||||
#include <tjp/core/testing/catch.hpp>
|
||||
#include <tjp/core/testing/wait_until.h>
|
||||
#include <tjp/core/internet/Headers.hpp>
|
||||
#include <tjp/core/hash/Hash.h>
|
||||
#include <tjp/core/types/Types.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
|
||||
namespace tjp::core::http::hirose {
|
||||
|
||||
SCENARIO("tjp::core::http::hirose")
|
||||
{
|
||||
GIVEN("server")
|
||||
{
|
||||
Server server;
|
||||
auto make_large_payload = []() {
|
||||
String data;
|
||||
data.reserve(64 * 1024);
|
||||
for (size_t i = 0; i < (64 * 1024); ++i)
|
||||
data.push_back(static_cast<char>('a' + (i % 26)));
|
||||
return data;
|
||||
};
|
||||
auto large_payload = make_large_payload();
|
||||
|
||||
server.bind_with("GET", "/hello", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.body = "hello";
|
||||
return result;
|
||||
});
|
||||
server.bind_with("POST", "/echo", [](const Request &req) -> Result {
|
||||
Result result;
|
||||
result.status = 201;
|
||||
result.type = "text/plain";
|
||||
result.body = String(req.body);
|
||||
return result;
|
||||
});
|
||||
server.bind_with("PUT", "/replace", [](const Request &req) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.body = "updated:" + String(req.body);
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", "/range", [](const Request &req) -> Result
|
||||
{
|
||||
Result result;
|
||||
String full = "0123456789";
|
||||
String send;
|
||||
size_t begin = 0;
|
||||
size_t end = full.size() - 1;
|
||||
|
||||
if (auto range_ = req.header("range"))
|
||||
if (auto range = core::internet::parseRange(String(*range_)))
|
||||
{
|
||||
auto &[begin_, end_] = *range;
|
||||
begin = begin_;
|
||||
end = end_;
|
||||
send = full.substr(begin, (end - begin + 1));
|
||||
}
|
||||
|
||||
result.headers.push_back(Header { "Content-Range", core::internet::makeContentRange({ begin, end, full.size() }) });
|
||||
result.type = "text/plain";
|
||||
result.body = send;
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", match_exact("/match/exact"), [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.body = "exact";
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", match_prefix("/match/prefix"), [](const Request &req) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.body = "prefix:" + String(req.target);
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", match_regex("^/match/regex/[0-9]+$"), [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.body = "regex";
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET",
|
||||
[](StringView url) -> Optional<Matches> {
|
||||
return url == "/match/lambda" ? Matches() : Optional<Matches>();
|
||||
},
|
||||
[](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
result.body = "lambda";
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", "/stream-known", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body(6);
|
||||
out->write("signal", 6);
|
||||
out->done();
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", "/stream-unknown", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body();
|
||||
out->write("sig", 3);
|
||||
out->write("nal", 3);
|
||||
out->done();
|
||||
return result;
|
||||
});
|
||||
server.bind_with("HEAD", "/stream-head-known", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body(6);
|
||||
out->write("ABCDEF", 6);
|
||||
out->done();
|
||||
return result;
|
||||
});
|
||||
server.bind_with("HEAD", "/stream-head-unknown", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body();
|
||||
out->write("ignored", 7);
|
||||
out->done();
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", "/stream-large", [large_payload](const Request &) -> Result {
|
||||
Result result;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body(large_payload.size());
|
||||
const size_t chunk = 8192;
|
||||
for (size_t offset = 0; offset < large_payload.size(); offset += chunk)
|
||||
{
|
||||
auto size = std::min(chunk, large_payload.size() - offset);
|
||||
out->write(large_payload.data() + offset, size);
|
||||
}
|
||||
out->done();
|
||||
return result;
|
||||
});
|
||||
server.bind_with("GET", "/future-stream-known", [](const Request &) -> Result {
|
||||
Result result;
|
||||
result.status = 200;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body(6);
|
||||
std::thread([out]() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
||||
out->write("sig", 3);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
||||
out->write("nal", 3);
|
||||
out->done();
|
||||
}).detach();
|
||||
return result;
|
||||
});
|
||||
server.bind("GET", "/future-stream-unknown", FutureHandler([](const Request &) {
|
||||
Result result;
|
||||
result.status = 200;
|
||||
result.type = "text/plain";
|
||||
auto out = result.stream_body();
|
||||
std::thread([out]() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
||||
out->write("sig", 3);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
||||
out->write("nal", 3);
|
||||
out->done();
|
||||
}).detach();
|
||||
return future_with([result]() {
|
||||
return result;
|
||||
});
|
||||
}));
|
||||
|
||||
server.start("127.0.0.1", 4790);
|
||||
|
||||
core::wait_until(std::chrono::seconds(10), [&]() { return server.isRunning();});
|
||||
|
||||
WHEN("a get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
auto contents = client.Get("/hello");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "hello");
|
||||
}
|
||||
|
||||
WHEN("a post is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
auto contents = client.Post("/echo", "posted-body", "text/plain");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 201);
|
||||
REQUIRE(contents->body == "posted-body");
|
||||
}
|
||||
|
||||
WHEN("a put is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
auto contents = client.Put("/replace", "payload", "text/plain");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "updated:payload");
|
||||
}
|
||||
|
||||
WHEN("a range request is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
httplib::Headers headers;
|
||||
headers.emplace("Range", "bytes=2-5");
|
||||
auto contents = client.Get("/range", headers);
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
// REQUIRE(contents->status == 206);
|
||||
REQUIRE(contents->get_header_value("Content-Range") == "bytes 2-5/10");
|
||||
REQUIRE(contents->body == "2345");
|
||||
}
|
||||
|
||||
WHEN("an exact matcher route is requested")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
auto contents = client.Get("/match/exact");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "exact");
|
||||
}
|
||||
|
||||
WHEN("a prefix matcher route is requested")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
auto contents = client.Get("/match/prefix/child");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "prefix:/match/prefix/child");
|
||||
}
|
||||
|
||||
WHEN("a regex matcher route is requested")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
auto contents = client.Get("/match/regex/42");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "regex");
|
||||
}
|
||||
|
||||
WHEN("a lambda matcher route is requested")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
auto contents = client.Get("/match/lambda");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "lambda");
|
||||
}
|
||||
|
||||
WHEN("a known-sized stream get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
auto contents = client.Get("/stream-known");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "signal");
|
||||
REQUIRE(contents->get_header_value("Content-Length") == "6");
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
|
||||
}
|
||||
|
||||
WHEN("an unknown-sized stream get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
auto contents = client.Get("/stream-unknown");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "signal");
|
||||
REQUIRE(contents->get_header_value("Content-Length").empty());
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding") == "chunked");
|
||||
}
|
||||
|
||||
WHEN("a head of known-sized stream is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
auto contents = client.Head("/stream-head-known");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body.empty());
|
||||
REQUIRE(contents->get_header_value("Content-Length") == "6");
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
|
||||
}
|
||||
|
||||
WHEN("a head of unknown-sized stream is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
auto contents = client.Head("/stream-head-unknown");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body.empty());
|
||||
REQUIRE(contents->get_header_value("Content-Length").empty());
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
|
||||
}
|
||||
|
||||
WHEN("a large stream get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
auto contents = client.Get("/stream-large");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body.size() > 32 * 1024);
|
||||
REQUIRE(contents->body.size() == large_payload.size());
|
||||
auto body_hash = hash_of<u64>(contents->body);
|
||||
auto expected_hash = hash_of<u64>(large_payload);
|
||||
REQUIRE(body_hash == expected_hash);
|
||||
REQUIRE(contents->get_header_value("Content-Length") == std::to_string(large_payload.size()));
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
|
||||
}
|
||||
|
||||
WHEN("a future-backed known-size stream get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
auto contents = client.Get("/future-stream-known");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "signal");
|
||||
REQUIRE(contents->get_header_value("Content-Length") == "6");
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
|
||||
}
|
||||
|
||||
WHEN("a future-backed unknown-size stream get is made")
|
||||
{
|
||||
httplib::Client client("http://127.0.0.1:4790");
|
||||
auto contents = client.Get("/future-stream-unknown");
|
||||
|
||||
REQUIRE(bool(contents));
|
||||
REQUIRE(contents->status == 200);
|
||||
REQUIRE(contents->body == "signal");
|
||||
REQUIRE(contents->get_header_value("Content-Length").empty());
|
||||
REQUIRE(contents->get_header_value("Transfer-Encoding") == "chunked");
|
||||
}
|
||||
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
12067
tjp/core/http/hirose/httplib-0_27_0.h
Normal file
12067
tjp/core/http/hirose/httplib-0_27_0.h
Normal file
File diff suppressed because it is too large
Load Diff
3
tjp/core/http/hirose/httplib.h
Normal file
3
tjp/core/http/hirose/httplib.h
Normal file
@@ -0,0 +1,3 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#include "httplib-0_27_0.h"
|
||||
36
tjp/core/http/hirose/write_result.hpp
Executable file
36
tjp/core/http/hirose/write_result.hpp
Executable file
@@ -0,0 +1,36 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "http.hpp"
|
||||
#include <tjp/core/exception/Exception.hpp>
|
||||
|
||||
//namespace tjp::core::http::hirose {}
|
||||
|
||||
namespace tjp::core::http::hirose {
|
||||
|
||||
template<typename F>
|
||||
auto write_result(F &&f)
|
||||
{
|
||||
return
|
||||
[f=std::move(f)](const Request& req, Response& res)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto [body, type] = f(req);
|
||||
res.set_content(body, type);
|
||||
}
|
||||
catch (Exception &e)
|
||||
{
|
||||
res.set_content("Error: " + e.what(), "text/plain");
|
||||
res.status = 400;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
res.set_content("Error", "text/plain");
|
||||
res.status = 400;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
11
tjp/core/http/imp.hpp
Executable file
11
tjp/core/http/imp.hpp
Executable file
@@ -0,0 +1,11 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "hirose/Server.hpp"
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
namespace imp = hirose;
|
||||
|
||||
} // namespace
|
||||
8309
tjp/core/http/old/httplib.h
Normal file
8309
tjp/core/http/old/httplib.h
Normal file
File diff suppressed because it is too large
Load Diff
38
tjp/core/http/remote-run
Executable file
38
tjp/core/http/remote-run
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./remote-run <command>
|
||||
|
||||
Environment variables:
|
||||
REMOTE_HOST Default: host.docker.internal
|
||||
REMOTE_PORT Default: 6666
|
||||
REMOTE_TOKEN Optional shared token
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cmd="$1"
|
||||
host="${REMOTE_HOST:-host.docker.internal}"
|
||||
port="${REMOTE_PORT:-6666}"
|
||||
token="${REMOTE_TOKEN:-}"
|
||||
|
||||
url="http://${host}:${port}/run/${cmd}"
|
||||
|
||||
curl_args=(--silent --show-error --no-buffer "$url")
|
||||
if [[ -n "$token" ]]; then
|
||||
curl_args=(-H "X-Remote-Token: ${token}" "${curl_args[@]}")
|
||||
fi
|
||||
|
||||
exec curl "${curl_args[@]}"
|
||||
18
tjp/core/http/require_header.hpp
Executable file
18
tjp/core/http/require_header.hpp
Executable file
@@ -0,0 +1,18 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Types.hpp"
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
inline
|
||||
auto require_header(const Request& req, const String &key)
|
||||
{
|
||||
if (auto header = req.header(key))
|
||||
return String(*header);
|
||||
|
||||
throw Exception { "Missing header" };
|
||||
}
|
||||
|
||||
} // namespace
|
||||
588
tjp/core/http/temp/compression/_delete/zlib.cpp
Normal file
588
tjp/core/http/temp/compression/_delete/zlib.cpp
Normal file
@@ -0,0 +1,588 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#include "zlib.hpp"
|
||||
|
||||
#include <tjp/core/threads/Lock.hpp>
|
||||
#include <tjp/core/containers/Map.hpp>
|
||||
#include <tjp/core/algorithm/mem_copy.hpp>
|
||||
|
||||
#include <zlib/zlib.h>
|
||||
|
||||
namespace tjp::core::compression::zlib {
|
||||
|
||||
struct CompressContexts {
|
||||
Mutex mutex;
|
||||
Map<int, Vector<void *>> contextsByLevel;
|
||||
|
||||
~CompressContexts()
|
||||
{
|
||||
for (auto &[l, contexts] : contextsByLevel)
|
||||
for (auto *c : contexts)
|
||||
free((z_streamp)c);
|
||||
}
|
||||
|
||||
void *getContext(int level)
|
||||
{
|
||||
{
|
||||
auto lock = lock_of(mutex);
|
||||
|
||||
auto &contexts = contextsByLevel[level];
|
||||
if (!contexts.empty())
|
||||
{
|
||||
auto *v = contexts.back();
|
||||
contexts.pop_back();
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
auto z = (z_streamp)calloc(1, sizeof(z_stream));
|
||||
deflateInit(z, level);
|
||||
return z;
|
||||
}
|
||||
|
||||
void putContext(int level, void *v)
|
||||
{
|
||||
auto lock = lock_of(mutex);
|
||||
contextsByLevel[level].push_back(v);
|
||||
}
|
||||
} ;
|
||||
|
||||
struct DecompressContexts
|
||||
{
|
||||
Mutex mutex;
|
||||
Vector<void *> contexts;
|
||||
|
||||
~DecompressContexts()
|
||||
{
|
||||
for (auto *c : contexts)
|
||||
free((z_streamp)c);
|
||||
}
|
||||
|
||||
void *getContext()
|
||||
{
|
||||
{
|
||||
auto lock = lock_of(mutex);
|
||||
|
||||
if (!contexts.empty())
|
||||
{
|
||||
auto *v = contexts.back();
|
||||
contexts.pop_back();
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
auto z = (z_streamp)calloc(1, sizeof(z_stream));
|
||||
inflateInit(z);
|
||||
return z;
|
||||
}
|
||||
|
||||
void putContext(void *v)
|
||||
{
|
||||
auto lock = lock_of(mutex);
|
||||
contexts.push_back(v);
|
||||
}
|
||||
} ;
|
||||
|
||||
CompressContexts compressContexts;
|
||||
DecompressContexts decompressContexts;
|
||||
|
||||
// --------------
|
||||
|
||||
bool clearStream(void *p, bool inflate)
|
||||
{
|
||||
auto z = (z_streamp)p;
|
||||
|
||||
if (inflate)
|
||||
return (inflateReset(z) == Z_OK);
|
||||
else
|
||||
return (deflateReset(z) == Z_OK);
|
||||
}
|
||||
|
||||
|
||||
bool setDict(void *p, bool inflate, char *dict, size_t size)
|
||||
{
|
||||
auto z = (z_streamp)p;
|
||||
|
||||
int error = 0;
|
||||
if (inflate)
|
||||
error = inflateSetDictionary(z, (const Bytef *)dict, (uInt)size);
|
||||
else
|
||||
error = deflateSetDictionary(z, (const Bytef *)dict, (uInt)size);
|
||||
|
||||
return error == Z_OK;
|
||||
}
|
||||
|
||||
bool getDict(void *p, bool inflate, char *dict, size_t size)
|
||||
{
|
||||
auto z = (z_streamp)p;
|
||||
|
||||
auto size_ = (uInt)size;
|
||||
|
||||
char store[32768];
|
||||
|
||||
int error = 0;
|
||||
if (inflate)
|
||||
error = inflateGetDictionary(z, (Bytef *)&store[0], &size_);
|
||||
else
|
||||
error = deflateGetDictionary(z, (Bytef *)&store[0], &size_);
|
||||
|
||||
// zlib seems to store from end to front
|
||||
auto offset =
|
||||
size_ > size ?
|
||||
size_ - size :
|
||||
0;
|
||||
|
||||
mem_copy(dict, &store[offset], size);
|
||||
|
||||
return error == Z_OK;
|
||||
}
|
||||
|
||||
|
||||
// --------------
|
||||
|
||||
CompressUnordered::CompressUnordered(int level_) :
|
||||
level(level_)
|
||||
{
|
||||
}
|
||||
|
||||
size_t CompressUnordered::execute(char *dest_, size_t destLen_, const char *source_, size_t sourceLen_)
|
||||
{
|
||||
const Bytef *source = (const Bytef *)source_;
|
||||
uLongf sourceLen = sourceLen_;
|
||||
Bytef *dest = (Bytef *)dest_;
|
||||
uLongf destLen = destLen_;
|
||||
|
||||
auto context = (z_streamp)compressContexts.getContext(level);
|
||||
|
||||
context->next_in = (Bytef *)source;
|
||||
context->avail_in = (uInt)sourceLen;
|
||||
|
||||
context->next_out = dest;
|
||||
context->avail_out = (uInt)destLen;
|
||||
|
||||
auto result = deflate(context, Z_FINISH);
|
||||
auto toSize = context->total_out;
|
||||
|
||||
compressContexts.putContext(level, context);
|
||||
|
||||
if (result != Z_STREAM_END)
|
||||
return 0;
|
||||
|
||||
return toSize;
|
||||
}
|
||||
|
||||
size_t UncompressUnordered::execute(char *dest_, size_t destLen_, const char *source_, size_t sourceLen_)
|
||||
{
|
||||
const Bytef *source = (const Bytef *)source_;
|
||||
uLongf sourceLen = sourceLen_;
|
||||
Bytef *dest = (Bytef *)dest_;
|
||||
uLongf destLen = destLen_;
|
||||
|
||||
auto context = (z_streamp)decompressContexts.getContext();
|
||||
|
||||
context->next_in = (Bytef *)source;
|
||||
context->avail_in = (uInt)sourceLen;
|
||||
|
||||
context->next_out = dest;
|
||||
context->avail_out = (uInt)destLen;
|
||||
|
||||
auto result = inflate(context, Z_FINISH);
|
||||
auto toSize = context->total_out;
|
||||
|
||||
decompressContexts.putContext(context);
|
||||
|
||||
if (result != Z_STREAM_END)
|
||||
return 0;
|
||||
|
||||
return toSize;
|
||||
}
|
||||
|
||||
|
||||
// --------
|
||||
|
||||
|
||||
|
||||
CompressOrdered::CompressOrdered(int level_) :
|
||||
level(level_)
|
||||
{
|
||||
memset(dictionary, 0, dictionarySize);
|
||||
}
|
||||
|
||||
CompressOrdered::~CompressOrdered()
|
||||
{
|
||||
}
|
||||
|
||||
size_t CompressOrdered::execute(char *dest_, size_t destLen_, const char *source_, size_t sourceLen_)
|
||||
{
|
||||
const Bytef *source = (const Bytef *)source_;
|
||||
uLongf sourceLen = sourceLen_;
|
||||
Bytef *dest = (Bytef *)dest_;
|
||||
uLongf destLen = destLen_;
|
||||
|
||||
auto context = (z_streamp)compressContexts.getContext(level);
|
||||
|
||||
if (!clearStream(context, false))
|
||||
return 0;
|
||||
|
||||
if (!setDict(context, false, dictionary, dictionarySize))
|
||||
return 0;
|
||||
|
||||
context->next_in = (Bytef *)source;
|
||||
context->avail_in = (uInt)sourceLen;
|
||||
|
||||
context->next_out = dest;
|
||||
context->avail_out = (uInt)destLen;
|
||||
|
||||
auto result = deflate(context, Z_FINISH);
|
||||
auto toSize = context->total_out;
|
||||
|
||||
if (result == Z_STREAM_END)
|
||||
getDict(context, false, dictionary, dictionarySize);
|
||||
|
||||
compressContexts.putContext(level, context);
|
||||
|
||||
if (result != Z_STREAM_END)
|
||||
return 0;
|
||||
|
||||
return toSize;
|
||||
}
|
||||
|
||||
size_t CompressOrdered::execute(Vector<char> &dest, const containers::MemorySegment<char> &source)
|
||||
{
|
||||
auto context = (z_streamp)compressContexts.getContext(level);
|
||||
|
||||
if (!clearStream(context, false))
|
||||
return 0;
|
||||
|
||||
if (!setDict(context, false, dictionary, dictionarySize))
|
||||
return 0;
|
||||
|
||||
context->next_in = (Bytef *)source.data();
|
||||
context->avail_in = (uInt)source.size();
|
||||
|
||||
auto sizeOffset = dest.size();
|
||||
dest.resize(dest.size() + sizeof(u32));
|
||||
auto bos = dest.size();
|
||||
|
||||
int result = 0;
|
||||
|
||||
while(1)
|
||||
{
|
||||
auto newSize = std::max(bos + context->total_out + 2048, (size_t)16384);
|
||||
dest.resize(newSize);
|
||||
|
||||
auto eos = bos + context->total_out;
|
||||
context->next_out = (Bytef *)dest.data() + eos;
|
||||
context->avail_out = (uInt)dest.size() - eos;
|
||||
|
||||
result = deflate(context, Z_FINISH);
|
||||
if (result == Z_STREAM_END)
|
||||
break;
|
||||
|
||||
if (result <= Z_ERRNO)
|
||||
break;
|
||||
}
|
||||
|
||||
getDict(context, false, dictionary, dictionarySize);
|
||||
|
||||
u32 toSize = context->total_out;
|
||||
mem_copy(dest.data() + sizeOffset, (char *)&toSize, sizeof(u32));
|
||||
|
||||
auto eos = bos + context->total_out;
|
||||
dest.resize(eos);
|
||||
|
||||
compressContexts.putContext(level, context);
|
||||
|
||||
if (result != Z_STREAM_END)
|
||||
return 0;
|
||||
|
||||
return toSize;
|
||||
}
|
||||
|
||||
UncompressOrdered::UncompressOrdered()
|
||||
{
|
||||
memset(dictionary, 0, dictionarySize);
|
||||
}
|
||||
|
||||
UncompressOrdered::~UncompressOrdered()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
size_t UncompressOrdered::execute(char *dest_, size_t destLen_, const char *source_, size_t sourceLen_)
|
||||
{
|
||||
const Bytef *source = (const Bytef *)source_;
|
||||
uLongf sourceLen = sourceLen_;
|
||||
Bytef *dest = (Bytef *)dest_;
|
||||
uLongf destLen = destLen_;
|
||||
|
||||
auto context = (z_streamp)decompressContexts.getContext();
|
||||
|
||||
if (!clearStream(context, true))
|
||||
return 0;
|
||||
|
||||
context->next_in = (Bytef *)source;
|
||||
context->avail_in = (uInt)sourceLen;
|
||||
|
||||
context->next_out = dest;
|
||||
context->avail_out = (uInt)destLen;
|
||||
|
||||
auto result = inflate(context, Z_FINISH);
|
||||
|
||||
if (result == Z_NEED_DICT)
|
||||
{
|
||||
if (!setDict(context, true, dictionary, dictionarySize))
|
||||
return 0;
|
||||
|
||||
result = inflate(context, Z_FINISH);
|
||||
}
|
||||
|
||||
auto toSize = context->total_out;
|
||||
|
||||
if (result == Z_STREAM_END)
|
||||
getDict(context, true, dictionary, dictionarySize);
|
||||
|
||||
decompressContexts.putContext(context);
|
||||
|
||||
if (result != Z_STREAM_END)
|
||||
return 0;
|
||||
|
||||
return toSize;
|
||||
}
|
||||
|
||||
size_t UncompressOrdered::execute(const containers::MemorySegment<char> &dest, const containers::MemorySegment<char> &source)
|
||||
{
|
||||
u32 size = 0;
|
||||
mem_copy((char *)&size, source.data(), sizeof(u32));
|
||||
auto dataOffset = sizeof(u32);
|
||||
|
||||
auto context = (z_streamp)decompressContexts.getContext();
|
||||
|
||||
if (!clearStream(context, true))
|
||||
return 0;
|
||||
|
||||
context->next_in = (Bytef *)source.data() + dataOffset;
|
||||
context->avail_in = size;
|
||||
|
||||
int result = 0;
|
||||
|
||||
while(1)
|
||||
{
|
||||
context->next_out = (Bytef *)dest.data() + context->total_out;
|
||||
context->avail_out = (uInt)dest.size() - context->total_out;
|
||||
|
||||
result = inflate(context, Z_FINISH);
|
||||
|
||||
if (result == Z_NEED_DICT)
|
||||
{
|
||||
if (!setDict(context, true, dictionary, dictionarySize))
|
||||
return 0;
|
||||
|
||||
result = inflate(context, Z_FINISH);
|
||||
}
|
||||
|
||||
if (result == Z_STREAM_END)
|
||||
break;
|
||||
|
||||
if (result <= Z_ERRNO)
|
||||
break;
|
||||
}
|
||||
|
||||
auto toSize = context->total_out;
|
||||
debug_assert(toSize == dest.size());
|
||||
|
||||
getDict(context, true, dictionary, dictionarySize);
|
||||
|
||||
decompressContexts.putContext(context);
|
||||
|
||||
if (result != Z_STREAM_END)
|
||||
return 0;
|
||||
|
||||
return size + sizeof(32);
|
||||
}
|
||||
|
||||
size_t UncompressOrdered::execute(Vector<char> &dest, const containers::MemorySegment<char> &source)
|
||||
{
|
||||
u32 size = 0;
|
||||
mem_copy((char *)&size, source.data(), sizeof(u32));
|
||||
auto dataOffset = sizeof(u32);
|
||||
|
||||
auto context = (z_streamp)decompressContexts.getContext();
|
||||
|
||||
if (!clearStream(context, true))
|
||||
return 0;
|
||||
|
||||
context->next_in = (Bytef *)source.data() + dataOffset;
|
||||
context->avail_in = size;
|
||||
|
||||
auto offset = 0;
|
||||
int result = 0;
|
||||
|
||||
while(1)
|
||||
{
|
||||
auto newSize = std::max(offset + context->total_out + 2048, (size_t)16384);
|
||||
dest.resize(newSize);
|
||||
|
||||
context->next_out = (Bytef *)dest.data() + offset + context->total_out;
|
||||
context->avail_out = (uInt)dest.size() - offset - context->total_out;
|
||||
|
||||
result = inflate(context, Z_FINISH);
|
||||
|
||||
if (result == Z_NEED_DICT)
|
||||
{
|
||||
if (!setDict(context, true, dictionary, DictionarySize))
|
||||
return 0;
|
||||
|
||||
result = inflate(context, Z_FINISH);
|
||||
}
|
||||
|
||||
if (result == Z_STREAM_END)
|
||||
break;
|
||||
|
||||
if (result <= Z_ERRNO)
|
||||
break;
|
||||
}
|
||||
|
||||
auto toSize = context->total_out;
|
||||
dest.resize(toSize);
|
||||
|
||||
getDict(context, true, dictionary, dictionarySize);
|
||||
|
||||
decompressContexts.putContext(context);
|
||||
|
||||
if (result != Z_STREAM_END)
|
||||
return 0;
|
||||
|
||||
return size + sizeof(32);
|
||||
}
|
||||
|
||||
// ---------
|
||||
|
||||
CompressStream::CompressStream()
|
||||
{
|
||||
memset(dictionary, 0, dictionarySize);
|
||||
}
|
||||
|
||||
size_t CompressStream::with(Vector<char> &v, Function<void (CompressStream &)> &&f)
|
||||
{
|
||||
auto context = (z_streamp)decompressContexts.getContext();
|
||||
|
||||
if (!clearStream(context, true))
|
||||
return 0;
|
||||
|
||||
if (!setDict(context, true, dictionary, dictionarySize))
|
||||
return 0;
|
||||
|
||||
i = (I *)context;
|
||||
v_ = &v;
|
||||
p = v.size();
|
||||
|
||||
auto initialSize = v.size();
|
||||
f(*this);
|
||||
|
||||
v.resize(p);
|
||||
auto finalSize = v.size();
|
||||
|
||||
decompressContexts.putContext(context);
|
||||
|
||||
return finalSize - initialSize;
|
||||
}
|
||||
|
||||
size_t CompressStream::write(const containers::MemorySegment<char> &s)
|
||||
{
|
||||
auto &v = *v_;
|
||||
|
||||
auto context = (z_streamp)i;
|
||||
context->next_in = (Bytef *)s.data();
|
||||
context->avail_in = (uInt)s.size();
|
||||
|
||||
// beginning of stream
|
||||
auto bos = p;
|
||||
|
||||
int result = 0;
|
||||
|
||||
while(1)
|
||||
{
|
||||
auto newSize = std::max(bos + context->total_out + 2048, (size_t)16384);
|
||||
v.resize(newSize);
|
||||
|
||||
context->next_out = (Bytef *)v.data() + bos + context->total_out;
|
||||
context->avail_out = (uInt)v.size() - bos - context->total_out;
|
||||
|
||||
result = deflate(context, Z_FINISH);
|
||||
|
||||
if (result == Z_STREAM_END)
|
||||
break;
|
||||
|
||||
if (result <= Z_ERRNO)
|
||||
break;
|
||||
}
|
||||
|
||||
auto toSize = context->total_out;
|
||||
p += toSize;
|
||||
|
||||
return toSize;
|
||||
}
|
||||
|
||||
DecompressStream::DecompressStream()
|
||||
{
|
||||
memset(dictionary, 0, dictionarySize);
|
||||
}
|
||||
|
||||
size_t DecompressStream::with(const containers::MemorySegment<char> &v, Function<void (DecompressStream &)> &&f)
|
||||
{
|
||||
auto context = (z_streamp)decompressContexts.getContext();
|
||||
|
||||
if (!clearStream(context, true))
|
||||
return 0;
|
||||
|
||||
if (!setDict(context, true, dictionary, dictionarySize))
|
||||
return 0;
|
||||
|
||||
i = (I *)context;
|
||||
|
||||
context->next_in = (Bytef *)v.data();
|
||||
context->avail_in = (uInt)v.size();
|
||||
|
||||
f(*this);
|
||||
|
||||
auto sizeRead = context->total_in;
|
||||
decompressContexts.putContext(context);
|
||||
|
||||
return sizeRead;
|
||||
}
|
||||
|
||||
size_t DecompressStream::read(const containers::MemorySegment<char> &s)
|
||||
{
|
||||
// beginning of stream
|
||||
int result = 0;
|
||||
|
||||
auto context = (z_streamp)i;
|
||||
context->next_out = (Bytef *)s.data();
|
||||
context->avail_out = (uInt)s.size();
|
||||
|
||||
while(1)
|
||||
{
|
||||
result = inflate(context, Z_FINISH);
|
||||
|
||||
if (result == Z_NEED_DICT)
|
||||
{
|
||||
if (!setDict(context, true, dictionary, DictionarySize))
|
||||
return 0;
|
||||
|
||||
result = inflate(context, Z_FINISH);
|
||||
}
|
||||
|
||||
if (result == Z_STREAM_END)
|
||||
break;
|
||||
|
||||
if (result <= Z_ERRNO)
|
||||
break;
|
||||
}
|
||||
|
||||
auto toSize = context->total_out;
|
||||
|
||||
return toSize;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
88
tjp/core/http/temp/compression/_delete/zlib.hpp
Normal file
88
tjp/core/http/temp/compression/_delete/zlib.hpp
Normal file
@@ -0,0 +1,88 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "zlib.h"
|
||||
#include <tjp/core/containers/Vector.h>
|
||||
#include <tjp/core/containers/MemorySegment.hpp>
|
||||
#include <tjp/core/containers/Function.hpp>
|
||||
|
||||
namespace tjp::core::compression::zlib {
|
||||
|
||||
constexpr size_t DictionarySize = 256;
|
||||
using Dictionary = char[DictionarySize];
|
||||
|
||||
struct CompressUnordered
|
||||
{
|
||||
static constexpr auto dictionarySize = DictionarySize;
|
||||
|
||||
CompressUnordered(int level);
|
||||
int level;
|
||||
|
||||
size_t execute(char *dest, size_t destLen, const char *source, size_t sourceLen);
|
||||
};
|
||||
|
||||
struct UncompressUnordered
|
||||
{
|
||||
size_t execute(char *dest, size_t destLen, const char *source, size_t sourceLen);
|
||||
} ;
|
||||
|
||||
struct CompressOrdered {
|
||||
|
||||
static constexpr auto dictionarySize = DictionarySize;
|
||||
|
||||
CompressOrdered(int level);
|
||||
~CompressOrdered();
|
||||
|
||||
int level;
|
||||
Dictionary dictionary;
|
||||
|
||||
size_t execute(char *dest, size_t destLen, const char *source, size_t sourceLen);
|
||||
size_t execute(Vector<char> &, const containers::MemorySegment<char> &);
|
||||
};
|
||||
|
||||
struct UncompressOrdered
|
||||
{
|
||||
static constexpr auto dictionarySize = DictionarySize;
|
||||
|
||||
UncompressOrdered();
|
||||
~UncompressOrdered();
|
||||
|
||||
Dictionary dictionary;
|
||||
|
||||
size_t execute(char *dest, size_t destLen, const char *source, size_t sourceLen);
|
||||
size_t execute(const containers::MemorySegment<char> &, const containers::MemorySegment<char> &);
|
||||
size_t execute(Vector<char> &, const containers::MemorySegment<char> &);
|
||||
} ;
|
||||
|
||||
struct CompressStream
|
||||
{
|
||||
static constexpr auto dictionarySize = DictionarySize;
|
||||
Dictionary dictionary;
|
||||
|
||||
using I = void;
|
||||
I *i;
|
||||
Vector<char> *v_;
|
||||
size_t p;
|
||||
|
||||
CompressStream();
|
||||
|
||||
size_t with(Vector<char> &, Function<void(CompressStream &)> &&f);
|
||||
size_t write(const containers::MemorySegment<char> &);
|
||||
} ;
|
||||
|
||||
struct DecompressStream
|
||||
{
|
||||
static constexpr auto dictionarySize = DictionarySize;
|
||||
Dictionary dictionary;
|
||||
|
||||
using I = void;
|
||||
I *i;
|
||||
|
||||
DecompressStream();
|
||||
|
||||
size_t with(const containers::MemorySegment<char> &, Function<void(DecompressStream &)> &&f);
|
||||
size_t read(const containers::MemorySegment<char> &);
|
||||
} ;
|
||||
|
||||
} // namespace
|
||||
282
tjp/core/http/temp/compression/_tests/zlib.cpp
Normal file
282
tjp/core/http/temp/compression/_tests/zlib.cpp
Normal file
@@ -0,0 +1,282 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#include <tjp/core/testing/catch.hpp>
|
||||
#include <tjp/core/compression/zlib.hpp>
|
||||
|
||||
#include <tjp/core/hash/Hash.h>
|
||||
#include <tjp/core/types/Types.h>
|
||||
|
||||
namespace tjp::core::compression::zlib {
|
||||
namespace {
|
||||
|
||||
SCENARIO("tjp::core::compression::zlib")
|
||||
{
|
||||
GIVEN("read and write")
|
||||
{
|
||||
CompressStream out(9);
|
||||
Vector<char> packet;
|
||||
|
||||
struct Values {
|
||||
s16 a;
|
||||
Vector<int> b;
|
||||
u32 c;
|
||||
Vector<s16> d;
|
||||
};
|
||||
|
||||
Values expected {
|
||||
.a = 42,
|
||||
.b = { 1, 2, 3, 4, 5 },
|
||||
.c = 13
|
||||
};
|
||||
|
||||
auto largeNumber = 235324;
|
||||
expected.d.assign(largeNumber, 12345);
|
||||
|
||||
out.with(packet, [&](auto &s) {
|
||||
s.value(expected.a);
|
||||
s.vector(expected.b);
|
||||
s.value(expected.c);
|
||||
s.vector(expected.d);
|
||||
});
|
||||
|
||||
Values expectedB {
|
||||
.a = 124,
|
||||
.b = { 5, 2, 3, 4, 5 },
|
||||
.c = 23
|
||||
};
|
||||
expectedB.d.assign(largeNumber, 6666);
|
||||
|
||||
out.with(packet, [&](auto &s) {
|
||||
s.value(expectedB.a);
|
||||
s.vector(expectedB.b);
|
||||
s.value(expectedB.c);
|
||||
s.vector(expectedB.d);
|
||||
});
|
||||
|
||||
Values real;
|
||||
real.b.resize(5);
|
||||
real.d.resize(largeNumber);
|
||||
|
||||
DecompressStream in;
|
||||
|
||||
auto segment = containers::memory_segment_vector<char>(packet);
|
||||
|
||||
in.with(segment, [&](auto &s) {
|
||||
s.value(real.a);
|
||||
s.vector(real.b);
|
||||
s.value(real.c);
|
||||
s.vector(real.d);
|
||||
});
|
||||
|
||||
REQUIRE(real.a == expected.a);
|
||||
REQUIRE(real.b == expected.b);
|
||||
REQUIRE(real.c == expected.c);
|
||||
|
||||
auto d_hash = hash_of<u64>(real.d);
|
||||
auto d_expectedHash = hash_of<u64>(expected.d);
|
||||
REQUIRE(d_hash == d_expectedHash);
|
||||
|
||||
|
||||
Values realB;
|
||||
realB.b.resize(5);
|
||||
realB.d.resize(largeNumber);
|
||||
|
||||
in.with(segment, [&](auto &s) {
|
||||
s.value(realB.a);
|
||||
s.vector(realB.b);
|
||||
s.value(realB.c);
|
||||
s.vector(realB.d);
|
||||
});
|
||||
|
||||
REQUIRE(realB.a == expectedB.a);
|
||||
REQUIRE(realB.b == expectedB.b);
|
||||
REQUIRE(realB.c == expectedB.c);
|
||||
|
||||
auto bd_hash = hash_of<u64>(realB.d);
|
||||
auto bd_expectedHash = hash_of<u64>(expectedB.d);
|
||||
REQUIRE(bd_hash == bd_expectedHash);
|
||||
}
|
||||
|
||||
GIVEN("back-to-back streams")
|
||||
{
|
||||
CompressStream out(9);
|
||||
Vector<char> packet;
|
||||
|
||||
Vector<s16> expectedA;
|
||||
Vector<s16> expectedB;
|
||||
expectedA.assign(32768, 1111);
|
||||
expectedB.assign(32768, 2222);
|
||||
|
||||
auto firstCompressedSize = out.with(packet, [&](auto &s) {
|
||||
s.vector(expectedA);
|
||||
});
|
||||
auto secondCompressedSize = out.with(packet, [&](auto &s) {
|
||||
s.vector(expectedB);
|
||||
});
|
||||
|
||||
REQUIRE(firstCompressedSize > 0);
|
||||
REQUIRE(secondCompressedSize > 0);
|
||||
|
||||
DecompressStream in;
|
||||
auto segment = containers::memory_segment_vector<char>(packet);
|
||||
|
||||
Vector<s16> realA;
|
||||
realA.resize(expectedA.size());
|
||||
in.with(segment, [&](auto &s) {
|
||||
s.vector(realA);
|
||||
});
|
||||
|
||||
auto a_hash = hash_of<u64>(realA);
|
||||
auto a_expectedHash = hash_of<u64>(expectedA);
|
||||
REQUIRE(a_hash == a_expectedHash);
|
||||
REQUIRE(segment.size() == secondCompressedSize);
|
||||
|
||||
Vector<s16> realB;
|
||||
realB.resize(expectedB.size());
|
||||
in.with(segment, [&](auto &s) {
|
||||
s.vector(realB);
|
||||
});
|
||||
|
||||
auto b_hash = hash_of<u64>(realB);
|
||||
auto b_expectedHash = hash_of<u64>(expectedB);
|
||||
REQUIRE(b_hash == b_expectedHash);
|
||||
REQUIRE(segment.size() == 0);
|
||||
}
|
||||
|
||||
GIVEN("gzip read and write")
|
||||
{
|
||||
CompressStream out(9, Format::Gzip);
|
||||
Vector<char> packet;
|
||||
|
||||
struct Values {
|
||||
s16 a;
|
||||
Vector<int> b;
|
||||
u32 c;
|
||||
Vector<s16> d;
|
||||
};
|
||||
|
||||
Values expected {
|
||||
.a = 17,
|
||||
.b = { 9, 8, 7, 6, 5 },
|
||||
.c = 99
|
||||
};
|
||||
|
||||
auto largeNumber = 32768;
|
||||
expected.d.assign(largeNumber, 4321);
|
||||
|
||||
Values expectedB {
|
||||
.a = 18,
|
||||
.b = { 1, 3, 5, 7, 9 },
|
||||
.c = 100
|
||||
};
|
||||
expectedB.d.assign(largeNumber, 8765);
|
||||
|
||||
out.with(packet, [&](auto &s) {
|
||||
s.value(expected.a);
|
||||
s.vector(expected.b);
|
||||
s.value(expected.c);
|
||||
s.vector(expected.d);
|
||||
});
|
||||
|
||||
out.with(packet, [&](auto &s) {
|
||||
s.value(expectedB.a);
|
||||
s.vector(expectedB.b);
|
||||
s.value(expectedB.c);
|
||||
s.vector(expectedB.d);
|
||||
});
|
||||
|
||||
REQUIRE(packet.size() >= 2);
|
||||
REQUIRE((unsigned char)packet[0] == 0x1f);
|
||||
REQUIRE((unsigned char)packet[1] == 0x8b);
|
||||
|
||||
Values real;
|
||||
real.b.resize(expected.b.size());
|
||||
real.d.resize(largeNumber);
|
||||
|
||||
DecompressStream in;
|
||||
auto segment = containers::memory_segment_vector<char>(packet);
|
||||
in.with(segment, [&](auto &s) {
|
||||
s.value(real.a);
|
||||
s.vector(real.b);
|
||||
s.value(real.c);
|
||||
s.vector(real.d);
|
||||
});
|
||||
|
||||
REQUIRE(real.a == expected.a);
|
||||
REQUIRE(real.b == expected.b);
|
||||
REQUIRE(real.c == expected.c);
|
||||
auto d_hash = hash_of<u64>(real.d);
|
||||
auto d_expectedHash = hash_of<u64>(expected.d);
|
||||
REQUIRE(d_hash == d_expectedHash);
|
||||
|
||||
Values realB;
|
||||
realB.b.resize(expectedB.b.size());
|
||||
realB.d.resize(largeNumber);
|
||||
in.with(segment, [&](auto &s) {
|
||||
s.value(realB.a);
|
||||
s.vector(realB.b);
|
||||
s.value(realB.c);
|
||||
s.vector(realB.d);
|
||||
});
|
||||
|
||||
REQUIRE(realB.a == expectedB.a);
|
||||
REQUIRE(realB.b == expectedB.b);
|
||||
REQUIRE(realB.c == expectedB.c);
|
||||
auto bd_hash = hash_of<u64>(realB.d);
|
||||
auto bd_expectedHash = hash_of<u64>(expectedB.d);
|
||||
REQUIRE(bd_hash == bd_expectedHash);
|
||||
REQUIRE(segment.size() == 0);
|
||||
}
|
||||
|
||||
GIVEN("gzip back-to-back streams")
|
||||
{
|
||||
CompressStream out(9, Format::Gzip);
|
||||
Vector<char> packet;
|
||||
|
||||
Vector<s16> expectedA;
|
||||
Vector<s16> expectedB;
|
||||
expectedA.assign(32768, 1111);
|
||||
expectedB.assign(32768, 2222);
|
||||
|
||||
auto firstCompressedSize = out.with(packet, [&](auto &s) {
|
||||
s.vector(expectedA);
|
||||
});
|
||||
auto secondCompressedSize = out.with(packet, [&](auto &s) {
|
||||
s.vector(expectedB);
|
||||
});
|
||||
|
||||
REQUIRE(firstCompressedSize > 0);
|
||||
REQUIRE(secondCompressedSize > 0);
|
||||
REQUIRE(packet.size() >= 2);
|
||||
REQUIRE((unsigned char)packet[0] == 0x1f);
|
||||
REQUIRE((unsigned char)packet[1] == 0x8b);
|
||||
|
||||
DecompressStream in;
|
||||
auto segment = containers::memory_segment_vector<char>(packet);
|
||||
|
||||
Vector<s16> realA;
|
||||
realA.resize(expectedA.size());
|
||||
in.with(segment, [&](auto &s) {
|
||||
s.vector(realA);
|
||||
});
|
||||
|
||||
auto a_hash = hash_of<u64>(realA);
|
||||
auto a_expectedHash = hash_of<u64>(expectedA);
|
||||
REQUIRE(a_hash == a_expectedHash);
|
||||
REQUIRE(segment.size() == secondCompressedSize);
|
||||
|
||||
Vector<s16> realB;
|
||||
realB.resize(expectedB.size());
|
||||
in.with(segment, [&](auto &s) {
|
||||
s.vector(realB);
|
||||
});
|
||||
|
||||
auto b_hash = hash_of<u64>(realB);
|
||||
auto b_expectedHash = hash_of<u64>(expectedB);
|
||||
REQUIRE(b_hash == b_expectedHash);
|
||||
REQUIRE(segment.size() == 0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace
|
||||
148
tjp/core/http/temp/compression/_tests/zstd.cpp
Normal file
148
tjp/core/http/temp/compression/_tests/zstd.cpp
Normal file
@@ -0,0 +1,148 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#include <tjp/core/testing/catch.hpp>
|
||||
#include <tjp/core/compression/zstd.hpp>
|
||||
|
||||
#include <tjp/core/hash/Hash.h>
|
||||
#include <tjp/core/types/Types.h>
|
||||
|
||||
namespace tjp::core::compression::zstd {
|
||||
namespace {
|
||||
|
||||
SCENARIO("tjp::core::compression::zstd")
|
||||
{
|
||||
GIVEN("read and write")
|
||||
{
|
||||
CompressStream out(9);
|
||||
Vector<char> packet;
|
||||
|
||||
struct Values {
|
||||
s16 a;
|
||||
Vector<int> b;
|
||||
u32 c;
|
||||
Vector<s16> d;
|
||||
};
|
||||
|
||||
Values expected {
|
||||
.a = 42,
|
||||
.b = { 1, 2, 3, 4, 5 },
|
||||
.c = 13
|
||||
};
|
||||
|
||||
auto largeNumber = 235324;
|
||||
expected.d.assign(largeNumber, 12345);
|
||||
|
||||
out.with(packet, [&](auto &s) {
|
||||
s.value(expected.a);
|
||||
s.vector(expected.b);
|
||||
s.value(expected.c);
|
||||
s.vector(expected.d);
|
||||
});
|
||||
|
||||
Values expectedB {
|
||||
.a = 124,
|
||||
.b = { 5, 2, 3, 4, 5 },
|
||||
.c = 23
|
||||
};
|
||||
expectedB.d.assign(largeNumber, 6666);
|
||||
|
||||
out.with(packet, [&](auto &s) {
|
||||
s.value(expectedB.a);
|
||||
s.vector(expectedB.b);
|
||||
s.value(expectedB.c);
|
||||
s.vector(expectedB.d);
|
||||
});
|
||||
|
||||
Values real;
|
||||
real.b.resize(5);
|
||||
real.d.resize(largeNumber);
|
||||
|
||||
DecompressStream in;
|
||||
|
||||
auto segment = containers::memory_segment_vector<char>(packet);
|
||||
|
||||
in.with(segment, [&](auto &s) {
|
||||
s.value(real.a);
|
||||
s.vector(real.b);
|
||||
s.value(real.c);
|
||||
s.vector(real.d);
|
||||
});
|
||||
|
||||
REQUIRE(real.a == expected.a);
|
||||
REQUIRE(real.b == expected.b);
|
||||
REQUIRE(real.c == expected.c);
|
||||
|
||||
auto d_hash = hash_of<u64>(real.d);
|
||||
auto d_expectedHash = hash_of<u64>(expected.d);
|
||||
REQUIRE(d_hash == d_expectedHash);
|
||||
|
||||
|
||||
Values realB;
|
||||
realB.b.resize(5);
|
||||
realB.d.resize(largeNumber);
|
||||
|
||||
in.with(segment, [&](auto &s) {
|
||||
s.value(realB.a);
|
||||
s.vector(realB.b);
|
||||
s.value(realB.c);
|
||||
s.vector(realB.d);
|
||||
});
|
||||
|
||||
REQUIRE(realB.a == expectedB.a);
|
||||
REQUIRE(realB.b == expectedB.b);
|
||||
REQUIRE(realB.c == expectedB.c);
|
||||
|
||||
auto bd_hash = hash_of<u64>(realB.d);
|
||||
auto bd_expectedHash = hash_of<u64>(expectedB.d);
|
||||
REQUIRE(bd_hash == bd_expectedHash);
|
||||
}
|
||||
|
||||
GIVEN("back-to-back streams")
|
||||
{
|
||||
CompressStream out(9);
|
||||
Vector<char> packet;
|
||||
|
||||
Vector<s16> expectedA;
|
||||
Vector<s16> expectedB;
|
||||
expectedA.assign(32768, 1111);
|
||||
expectedB.assign(32768, 2222);
|
||||
|
||||
auto firstCompressedSize = out.with(packet, [&](auto &s) {
|
||||
s.vector(expectedA);
|
||||
});
|
||||
auto secondCompressedSize = out.with(packet, [&](auto &s) {
|
||||
s.vector(expectedB);
|
||||
});
|
||||
|
||||
REQUIRE(firstCompressedSize > 0);
|
||||
REQUIRE(secondCompressedSize > 0);
|
||||
|
||||
DecompressStream in;
|
||||
auto segment = containers::memory_segment_vector<char>(packet);
|
||||
|
||||
Vector<s16> realA;
|
||||
realA.resize(expectedA.size());
|
||||
in.with(segment, [&](auto &s) {
|
||||
s.vector(realA);
|
||||
});
|
||||
|
||||
auto a_hash = hash_of<u64>(realA);
|
||||
auto a_expectedHash = hash_of<u64>(expectedA);
|
||||
REQUIRE(a_hash == a_expectedHash);
|
||||
REQUIRE(segment.size() == secondCompressedSize);
|
||||
|
||||
Vector<s16> realB;
|
||||
realB.resize(expectedB.size());
|
||||
in.with(segment, [&](auto &s) {
|
||||
s.vector(realB);
|
||||
});
|
||||
|
||||
auto b_hash = hash_of<u64>(realB);
|
||||
auto b_expectedHash = hash_of<u64>(expectedB);
|
||||
REQUIRE(b_hash == b_expectedHash);
|
||||
REQUIRE(segment.size() == 0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace
|
||||
38
tjp/core/http/temp/compression/remote-run
Executable file
38
tjp/core/http/temp/compression/remote-run
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./remote-run <command>
|
||||
|
||||
Environment variables:
|
||||
REMOTE_HOST Default: host.docker.internal
|
||||
REMOTE_PORT Default: 6666
|
||||
REMOTE_TOKEN Optional shared token
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cmd="$1"
|
||||
host="${REMOTE_HOST:-host.docker.internal}"
|
||||
port="${REMOTE_PORT:-6666}"
|
||||
token="${REMOTE_TOKEN:-}"
|
||||
|
||||
url="http://${host}:${port}/run/${cmd}"
|
||||
|
||||
curl_args=(--silent --show-error --no-buffer "$url")
|
||||
if [[ -n "$token" ]]; then
|
||||
curl_args=(-H "X-Remote-Token: ${token}" "${curl_args[@]}")
|
||||
fi
|
||||
|
||||
exec curl "${curl_args[@]}"
|
||||
525
tjp/core/http/temp/compression/zlib.cpp
Normal file
525
tjp/core/http/temp/compression/zlib.cpp
Normal file
@@ -0,0 +1,525 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#include "zlib.hpp"
|
||||
|
||||
#include <tjp/core/threads/Lock.hpp>
|
||||
#include <tjp/core/containers/Map.hpp>
|
||||
#include <tjp/core/algorithm/mem_copy.hpp>
|
||||
#include <tjp/core/algorithm/ExecuteOnDestruct.hpp>
|
||||
|
||||
#include <zlib/zlib.h>
|
||||
|
||||
namespace tjp::core::compression::zlib {
|
||||
|
||||
enum class ContextType {
|
||||
Compress,
|
||||
Decompress
|
||||
} ;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int ZlibWindowBits = 15;
|
||||
constexpr int GzipWindowBits = ZlibWindowBits + 16;
|
||||
constexpr int InflateAutoWindowBits = ZlibWindowBits + 32;
|
||||
|
||||
int window_bits_for(Format format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case Format::Zlib:
|
||||
return ZlibWindowBits;
|
||||
case Format::Gzip:
|
||||
return GzipWindowBits;
|
||||
default:
|
||||
return ZlibWindowBits;
|
||||
}
|
||||
}
|
||||
|
||||
size_t compress_key(int level, Format format)
|
||||
{
|
||||
return (size_t(level) << 2) | size_t(format);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct CompressContexts {
|
||||
Mutex mutex;
|
||||
Map<size_t, Vector<void *>> contextsByKey;
|
||||
|
||||
~CompressContexts()
|
||||
{
|
||||
for (auto &[k, contexts] : contextsByKey)
|
||||
for (auto *c : contexts)
|
||||
free((z_streamp)c);
|
||||
}
|
||||
|
||||
void *getContext(int level, Format format)
|
||||
{
|
||||
auto key = compress_key(level, format);
|
||||
{
|
||||
auto lock = lock_of(mutex);
|
||||
|
||||
auto &contexts = contextsByKey[key];
|
||||
if (!contexts.empty())
|
||||
{
|
||||
auto *v = contexts.back();
|
||||
contexts.pop_back();
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
auto z = (z_streamp)calloc(1, sizeof(z_stream));
|
||||
auto result = deflateInit2(
|
||||
z,
|
||||
level,
|
||||
Z_DEFLATED,
|
||||
window_bits_for(format),
|
||||
8,
|
||||
Z_DEFAULT_STRATEGY
|
||||
);
|
||||
if (result != Z_OK)
|
||||
{
|
||||
free((void *)z);
|
||||
return nullptr;
|
||||
}
|
||||
return z;
|
||||
}
|
||||
|
||||
void putContext(int level, Format format, void *v)
|
||||
{
|
||||
auto key = compress_key(level, format);
|
||||
auto lock = lock_of(mutex);
|
||||
contextsByKey[key].push_back(v);
|
||||
}
|
||||
} ;
|
||||
|
||||
struct DecompressContexts
|
||||
{
|
||||
Mutex mutex;
|
||||
Vector<void *> contexts;
|
||||
|
||||
~DecompressContexts()
|
||||
{
|
||||
for (auto *c : contexts)
|
||||
free((z_streamp)c);
|
||||
}
|
||||
|
||||
void *getContext()
|
||||
{
|
||||
{
|
||||
auto lock = lock_of(mutex);
|
||||
|
||||
if (!contexts.empty())
|
||||
{
|
||||
auto *v = contexts.back();
|
||||
contexts.pop_back();
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
auto z = (z_streamp)calloc(1, sizeof(z_stream));
|
||||
auto result = inflateInit2(z, InflateAutoWindowBits);
|
||||
if (result != Z_OK)
|
||||
{
|
||||
free((void *)z);
|
||||
return nullptr;
|
||||
}
|
||||
return z;
|
||||
}
|
||||
|
||||
void putContext(void *v)
|
||||
{
|
||||
auto lock = lock_of(mutex);
|
||||
contexts.push_back(v);
|
||||
}
|
||||
} ;
|
||||
|
||||
CompressContexts compressContexts;
|
||||
DecompressContexts decompressContexts;
|
||||
|
||||
#ifdef TIMPREPSCIUS_ZLIB_USE_DICTIONARY
|
||||
|
||||
Dictionary::Dictionary()
|
||||
{
|
||||
memset(block, 0, size);
|
||||
}
|
||||
|
||||
bool setDict(void *p, ContextType type, const Dictionary &d)
|
||||
{
|
||||
auto z = (z_streamp)p;
|
||||
|
||||
int error = 0;
|
||||
if (type == ContextType::Decompress)
|
||||
error = inflateSetDictionary(z, (const Bytef *)d.block, (uInt)d.size);
|
||||
else
|
||||
error = deflateSetDictionary(z, (const Bytef *)d.block, (uInt)d.size);
|
||||
|
||||
return error == Z_OK;
|
||||
}
|
||||
|
||||
bool getDict(void *p, ContextType type, Dictionary &d)
|
||||
{
|
||||
auto z = (z_streamp)p;
|
||||
|
||||
constexpr auto DictStoreSize = 32768;
|
||||
char store[DictStoreSize];
|
||||
memset(store, 0, DictStoreSize);
|
||||
auto size_ = (uInt)DictStoreSize;
|
||||
|
||||
int error = 0;
|
||||
if (type == ContextType::Decompress)
|
||||
error = inflateGetDictionary(z, (Bytef *)&store[0], &size_);
|
||||
else
|
||||
error = deflateGetDictionary(z, (Bytef *)&store[0], &size_);
|
||||
|
||||
// zlib seems to store from end to front
|
||||
auto offset =
|
||||
size_ > d.size ?
|
||||
size_ - d.size :
|
||||
0;
|
||||
|
||||
int firstIndex = -1, lastIndex = -1;
|
||||
for (auto i=0; i<DictStoreSize; ++i)
|
||||
{
|
||||
if (store[i] != 0)
|
||||
{
|
||||
if (firstIndex < 0)
|
||||
firstIndex = i;
|
||||
lastIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
mem_copy(d.block, &store[offset], d.size);
|
||||
|
||||
return error == Z_OK;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
constexpr
|
||||
bool setDict(void *p, ContextType type, const Dictionary &d) { return true; }
|
||||
|
||||
constexpr
|
||||
bool getDict(void *p, ContextType type, Dictionary &d) { return true; }
|
||||
|
||||
#endif
|
||||
|
||||
// --------------
|
||||
|
||||
bool clearStream(void *p, ContextType type)
|
||||
{
|
||||
auto z = (z_streamp)p;
|
||||
|
||||
if (type == ContextType::Decompress)
|
||||
return (inflateReset(z) == Z_OK);
|
||||
else
|
||||
return (deflateReset(z) == Z_OK);
|
||||
}
|
||||
|
||||
|
||||
// --------------
|
||||
|
||||
static size_t round_up_to_multiple(size_t number, size_t multiple) {
|
||||
if (multiple == 0) {
|
||||
return number; // Avoid division by zero
|
||||
}
|
||||
auto remainder = number % multiple;
|
||||
if (remainder == 0) {
|
||||
return number; // Already a multiple
|
||||
}
|
||||
return number + multiple - remainder;
|
||||
}
|
||||
|
||||
Compressor::Compressor(int level_, Format format_) :
|
||||
level(level_),
|
||||
format(format_)
|
||||
{
|
||||
}
|
||||
|
||||
size_t Compressor::with(V &v, F &&f)
|
||||
{
|
||||
error_ = 0;
|
||||
|
||||
auto context = (z_streamp)compressContexts.getContext(level, format);
|
||||
if (!context)
|
||||
return 0;
|
||||
|
||||
auto _ = ExecuteOnDestruct([&]() {
|
||||
compressContexts.putContext(level, format, context);
|
||||
i = nullptr;
|
||||
v_ = nullptr;
|
||||
});
|
||||
|
||||
if (!clearStream(context, ContextType::Compress))
|
||||
return 0;
|
||||
|
||||
if (!setDict(context, ContextType::Compress, dictionary))
|
||||
return 0;
|
||||
|
||||
i = (I *)context;
|
||||
v_ = &v;
|
||||
p = v.size();
|
||||
|
||||
auto p_ = p;
|
||||
|
||||
f(*this);
|
||||
|
||||
// finish the stream
|
||||
while(1)
|
||||
{
|
||||
auto newSize = round_up_to_multiple(p + 1, sizeMultiples);
|
||||
v.resize(newSize);
|
||||
|
||||
context->next_out = (Bytef *)v.data() + p;
|
||||
context->avail_out = uInt(newSize - p);
|
||||
|
||||
auto result = deflate(context, Z_FINISH);
|
||||
p = size_t(context->next_out - (Bytef *)v.data());
|
||||
|
||||
if (result == Z_STREAM_END)
|
||||
break;
|
||||
|
||||
if (result <= Z_ERRNO)
|
||||
{
|
||||
error_ = result;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getDict(context, ContextType::Compress, dictionary);
|
||||
|
||||
p = size_t(context->next_out - (Bytef *)v.data());
|
||||
v.resize(p);
|
||||
|
||||
return p - p_;
|
||||
}
|
||||
|
||||
size_t Compressor::write(const containers::MemorySegment<char> &s)
|
||||
{
|
||||
auto &v = *v_;
|
||||
|
||||
auto context = (z_streamp)i;
|
||||
|
||||
context->next_in = (Bytef *)s.data();
|
||||
context->avail_in = (uInt)s.size();
|
||||
|
||||
// beginning of stream
|
||||
int result = 0;
|
||||
size_t written = 0;
|
||||
|
||||
while(1)
|
||||
{
|
||||
auto newSize = round_up_to_multiple(p + 1, sizeMultiples);
|
||||
v.resize(newSize);
|
||||
|
||||
context->next_out = (Bytef *)v.data() + p;
|
||||
context->avail_out = uInt(newSize - p);
|
||||
|
||||
result = deflate(context, Z_NO_FLUSH);
|
||||
p = size_t(context->next_out - (Bytef *)v.data());
|
||||
written = s.size() - (size_t)context->avail_in;
|
||||
|
||||
if (result == Z_STREAM_END)
|
||||
break;
|
||||
|
||||
if (result <= Z_ERRNO)
|
||||
{
|
||||
error_ = result;
|
||||
break;
|
||||
}
|
||||
|
||||
if (context->avail_in == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
void Compressor::flush()
|
||||
{
|
||||
auto &v = *v_;
|
||||
|
||||
auto context = (z_streamp)i;
|
||||
|
||||
context->next_in = nullptr;
|
||||
context->avail_in = 0;
|
||||
|
||||
// beginning of stream
|
||||
int result = 0;
|
||||
|
||||
while(1)
|
||||
{
|
||||
auto newSize = round_up_to_multiple(p + 1, sizeMultiples);
|
||||
v.resize(newSize);
|
||||
|
||||
context->next_out = (Bytef *)v.data() + p;
|
||||
context->avail_out = uInt(newSize - p);
|
||||
|
||||
result = deflate(context, Z_PARTIAL_FLUSH);
|
||||
p = size_t(context->next_out - (Bytef *)v.data());
|
||||
|
||||
if (result == Z_STREAM_END)
|
||||
break;
|
||||
|
||||
if (result <= Z_ERRNO)
|
||||
{
|
||||
error_ = result;
|
||||
break;
|
||||
}
|
||||
|
||||
if (context->avail_out > 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
size_t Compressor::size() const
|
||||
{
|
||||
return p;
|
||||
}
|
||||
|
||||
bool Compressor::error () const
|
||||
{
|
||||
return error_;
|
||||
}
|
||||
|
||||
CompressStream::CompressStream(int level, Format format) :
|
||||
compressor(level, format)
|
||||
{
|
||||
}
|
||||
|
||||
size_t CompressStream::with(V &v, F &&f)
|
||||
{
|
||||
return compressor.with(v, std::move(f));
|
||||
}
|
||||
|
||||
CompressPacket::CompressPacket(int level_, Format format_) :
|
||||
level(level_),
|
||||
format(format_)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
size_t CompressPacket::with(V &v, F &&f)
|
||||
{
|
||||
Compressor compressor(level, format);
|
||||
return compressor.with(v, std::move(f));
|
||||
}
|
||||
|
||||
Decompressor::Decompressor()
|
||||
{
|
||||
}
|
||||
|
||||
size_t Decompressor::with(V &v, F &&f)
|
||||
{
|
||||
error_ = 0;
|
||||
|
||||
auto context = (z_streamp)decompressContexts.getContext();
|
||||
if (!context)
|
||||
return 0;
|
||||
|
||||
i = (I *)context;
|
||||
v_ = &v;
|
||||
|
||||
auto _ = ExecuteOnDestruct([&]() {
|
||||
decompressContexts.putContext(context);
|
||||
i = nullptr;
|
||||
v_ = nullptr;
|
||||
});
|
||||
|
||||
if (!clearStream(context, ContextType::Decompress))
|
||||
return 0;
|
||||
|
||||
context->next_in = (Bytef *)v.data();
|
||||
context->avail_in = uInt(v.size());
|
||||
p = 0;
|
||||
|
||||
f(*this);
|
||||
|
||||
getDict(context, ContextType::Decompress, dictionary);
|
||||
|
||||
auto written = context->total_out;
|
||||
auto consumed = size_t(context->next_in - (Bytef *)v.data());
|
||||
|
||||
v.data_ = (char *)context->next_in;
|
||||
if (consumed > v.size_)
|
||||
v.size_ = 0;
|
||||
else
|
||||
v.size_ -= consumed;
|
||||
return written;
|
||||
}
|
||||
|
||||
size_t Decompressor::read(const containers::MemorySegment<char> &s)
|
||||
{
|
||||
auto &v = *v_;
|
||||
auto context = (z_streamp)i;
|
||||
|
||||
context->next_out = (Bytef *)s.data();
|
||||
context->avail_out = (uInt)s.size();
|
||||
|
||||
int result = 0;
|
||||
size_t written = 0;
|
||||
|
||||
while(1)
|
||||
{
|
||||
result = inflate(context, Z_SYNC_FLUSH);
|
||||
written = size_t(context->next_out - (Bytef *)s.data());
|
||||
p = size_t(context->next_in - (Bytef *)v.data());
|
||||
|
||||
if (result == Z_NEED_DICT)
|
||||
{
|
||||
if (!setDict(context, ContextType::Decompress, dictionary))
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (result == Z_STREAM_END)
|
||||
break;
|
||||
|
||||
if (result <= Z_ERRNO)
|
||||
{
|
||||
error_ = result;
|
||||
break;
|
||||
}
|
||||
|
||||
if (written == s.size())
|
||||
break;
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
bool Decompressor::eos() const
|
||||
{
|
||||
return error() || p >= v_->size();
|
||||
}
|
||||
|
||||
bool Decompressor::error () const
|
||||
{
|
||||
return error_;
|
||||
}
|
||||
|
||||
DecompressStream::DecompressStream()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
size_t DecompressStream::with(V &v, F &&f)
|
||||
{
|
||||
return decompressor.with(v, std::move(f));
|
||||
}
|
||||
|
||||
DecompressPacket::DecompressPacket()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
size_t DecompressPacket::with(V &v, F &&f)
|
||||
{
|
||||
Decompressor decompressor;
|
||||
|
||||
return decompressor.with(v, std::move(f));
|
||||
}
|
||||
|
||||
|
||||
|
||||
} // namespace
|
||||
11
tjp/core/http/temp/compression/zlib.h
Normal file
11
tjp/core/http/temp/compression/zlib.h
Normal file
@@ -0,0 +1,11 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
namespace tjp::core::compression::zlib {
|
||||
|
||||
struct CompressStream;
|
||||
struct DecompressStream;
|
||||
|
||||
} // namespace
|
||||
158
tjp/core/http/temp/compression/zlib.hpp
Normal file
158
tjp/core/http/temp/compression/zlib.hpp
Normal file
@@ -0,0 +1,158 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "zlib.h"
|
||||
#include <tjp/core/containers/Vector.h>
|
||||
#include <tjp/core/containers/MemorySegment.hpp>
|
||||
#include <tjp/core/containers/Function.hpp>
|
||||
|
||||
namespace tjp::core::compression::zlib {
|
||||
|
||||
enum class Format
|
||||
{
|
||||
Zlib,
|
||||
Gzip
|
||||
} ;
|
||||
|
||||
//#define TIMPREPSCIUS_ZLIB_USE_DICTIONARY
|
||||
#ifdef TIMPREPSCIUS_ZLIB_USE_DICTIONARY
|
||||
struct Dictionary
|
||||
{
|
||||
static constexpr auto size = 256;
|
||||
char block[size];
|
||||
|
||||
Dictionary();
|
||||
} ;
|
||||
#else
|
||||
struct Dictionary {};
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
struct Compressor
|
||||
{
|
||||
[[no_unique_address]]
|
||||
Dictionary dictionary;
|
||||
|
||||
static constexpr auto sizeMultiples = 512;
|
||||
using V = Vector<char>;
|
||||
using F = Function<void(Compressor &)>;
|
||||
|
||||
int level;
|
||||
Format format;
|
||||
using I = void;
|
||||
I *i = nullptr;
|
||||
V *v_ = nullptr;
|
||||
size_t p = 0;
|
||||
int error_ = 0;
|
||||
|
||||
Compressor(int level, Format format = Format::Zlib);
|
||||
|
||||
size_t size() const;
|
||||
|
||||
size_t with(V &, F &&f);
|
||||
size_t write(const containers::MemorySegment<char> &);
|
||||
void flush();
|
||||
|
||||
template<typename T>
|
||||
auto value(T &t) {
|
||||
auto segment = containers::memory_segment_value<char>(t);
|
||||
return write(segment) == segment.size();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
auto vector(T &t) {
|
||||
auto segment = containers::memory_segment_vector<char>(t);
|
||||
return write(segment) == segment.size();
|
||||
}
|
||||
|
||||
bool error() const;
|
||||
} ;
|
||||
|
||||
struct CompressStream
|
||||
{
|
||||
using V = Compressor::V;
|
||||
using F = Compressor::F;
|
||||
|
||||
Compressor compressor;
|
||||
|
||||
CompressStream(int level, Format format = Format::Zlib);
|
||||
|
||||
size_t with(V &, F &&f);
|
||||
} ;
|
||||
|
||||
struct CompressPacket
|
||||
{
|
||||
using V = Compressor::V;
|
||||
using F = Compressor::F;
|
||||
|
||||
int level;
|
||||
Format format;
|
||||
|
||||
CompressPacket(int level, Format format = Format::Zlib);
|
||||
|
||||
size_t with(V &, F &&f);
|
||||
} ;
|
||||
|
||||
|
||||
struct Decompressor
|
||||
{
|
||||
[[no_unique_address]]
|
||||
Dictionary dictionary;
|
||||
|
||||
using V = containers::MemorySegment<char>;
|
||||
using F = Function<void(Decompressor &)>;
|
||||
|
||||
using I = void;
|
||||
I *i = nullptr;
|
||||
V *v_ = nullptr;
|
||||
size_t p = 0;
|
||||
int error_ = 0;
|
||||
|
||||
Decompressor();
|
||||
|
||||
bool eos() const;
|
||||
|
||||
size_t with(V &, F &&f);
|
||||
size_t read(const containers::MemorySegment<char> &);
|
||||
|
||||
template<typename T>
|
||||
auto value(T &t) {
|
||||
auto segment = containers::memory_segment_value<char>(t);
|
||||
return read(segment) == segment.size();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
auto vector(T &t) {
|
||||
auto segment = containers::memory_segment_vector<char>(t);
|
||||
return read(segment) == segment.size();
|
||||
}
|
||||
|
||||
bool error () const;
|
||||
} ;
|
||||
|
||||
struct DecompressStream
|
||||
{
|
||||
using V = Decompressor::V;
|
||||
using F = Decompressor::F;
|
||||
|
||||
Decompressor decompressor;
|
||||
|
||||
DecompressStream();
|
||||
|
||||
size_t with(V &, F &&f);
|
||||
} ;
|
||||
|
||||
struct DecompressPacket
|
||||
{
|
||||
using V = Decompressor::V;
|
||||
using F = Decompressor::F;
|
||||
|
||||
DecompressPacket();
|
||||
|
||||
size_t with(V &, F &&f);
|
||||
} ;
|
||||
|
||||
|
||||
} // namespace
|
||||
356
tjp/core/http/temp/compression/zstd.cpp
Normal file
356
tjp/core/http/temp/compression/zstd.cpp
Normal file
@@ -0,0 +1,356 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#include "zstd.hpp"
|
||||
|
||||
#include <tjp/core/threads/Lock.hpp>
|
||||
#include <tjp/core/containers/Map.hpp>
|
||||
#include <tjp/core/algorithm/mem_copy.hpp>
|
||||
#include <tjp/core/algorithm/ExecuteOnDestruct.hpp>
|
||||
|
||||
#include <zstd/zstd.h>
|
||||
|
||||
namespace tjp::core::compression::zstd {
|
||||
|
||||
enum class ContextType {
|
||||
Compress,
|
||||
Decompress
|
||||
} ;
|
||||
|
||||
struct CompressContexts {
|
||||
Mutex mutex;
|
||||
Map<int, Vector<void *>> contextsByLevel;
|
||||
|
||||
~CompressContexts()
|
||||
{
|
||||
for (auto &[l, contexts] : contextsByLevel)
|
||||
for (auto *c : contexts)
|
||||
ZSTD_freeCStream((ZSTD_CStream *)c);
|
||||
}
|
||||
|
||||
void *getContext(int level)
|
||||
{
|
||||
{
|
||||
auto lock = lock_of(mutex);
|
||||
|
||||
auto &contexts = contextsByLevel[level];
|
||||
if (!contexts.empty())
|
||||
{
|
||||
auto *v = contexts.back();
|
||||
contexts.pop_back();
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
auto *v = ZSTD_createCStream();
|
||||
ZSTD_initCStream(v, level);
|
||||
return v;
|
||||
}
|
||||
|
||||
void putContext(int level, void *v)
|
||||
{
|
||||
auto lock = lock_of(mutex);
|
||||
contextsByLevel[level].push_back(v);
|
||||
}
|
||||
} ;
|
||||
|
||||
struct DecompressContexts
|
||||
{
|
||||
Mutex mutex;
|
||||
Vector<void *> contexts;
|
||||
|
||||
~DecompressContexts()
|
||||
{
|
||||
for (auto *c : contexts)
|
||||
ZSTD_freeDStream((ZSTD_DStream *)c);
|
||||
}
|
||||
|
||||
void *getContext()
|
||||
{
|
||||
{
|
||||
auto lock = lock_of(mutex);
|
||||
|
||||
if (!contexts.empty())
|
||||
{
|
||||
auto *v = contexts.back();
|
||||
contexts.pop_back();
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
void *v = ZSTD_createDStream();
|
||||
return v;
|
||||
}
|
||||
|
||||
void putContext(void *v)
|
||||
{
|
||||
auto lock = lock_of(mutex);
|
||||
contexts.push_back(v);
|
||||
}
|
||||
} ;
|
||||
|
||||
CompressContexts compressContexts;
|
||||
DecompressContexts decompressContexts;
|
||||
|
||||
|
||||
// --------------
|
||||
|
||||
bool clearStream(void *p, ContextType type)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// --------------
|
||||
|
||||
static size_t round_up_to_multiple(size_t number, size_t multiple) {
|
||||
if (multiple == 0) {
|
||||
return number; // Avoid division by zero
|
||||
}
|
||||
auto remainder = number % multiple;
|
||||
if (remainder == 0) {
|
||||
return number; // Already a multiple
|
||||
}
|
||||
return number + multiple - remainder;
|
||||
}
|
||||
|
||||
Compressor::Compressor(int level_) :
|
||||
level(level_)
|
||||
{
|
||||
}
|
||||
|
||||
size_t Compressor::with(V &v, F &&f)
|
||||
{
|
||||
auto lock = lock_of(mutex);
|
||||
auto context = (ZSTD_CStream *)compressContexts.getContext(level);
|
||||
|
||||
auto _ = ExecuteOnDestruct([&]() {
|
||||
compressContexts.putContext(level, context);
|
||||
i = nullptr;
|
||||
v_ = nullptr;
|
||||
});
|
||||
|
||||
if (!clearStream(context, ContextType::Compress))
|
||||
return 0;
|
||||
|
||||
i = (I *)context;
|
||||
v_ = &v;
|
||||
p = v.size();
|
||||
|
||||
auto p_ = p;
|
||||
|
||||
f(*this);
|
||||
|
||||
// finish the stream
|
||||
while(1)
|
||||
{
|
||||
auto newSize = round_up_to_multiple(p + 1, sizeMultiples);
|
||||
v.resize(newSize);
|
||||
|
||||
ZSTD_outBuffer out;
|
||||
out.dst = v.data();
|
||||
out.pos = p;
|
||||
out.size = v.size();
|
||||
|
||||
auto result = ZSTD_endStream(context, &out);
|
||||
p = out.pos;
|
||||
|
||||
if (ZSTD_isError(result))
|
||||
{
|
||||
error_ = result;
|
||||
break;
|
||||
}
|
||||
|
||||
if (result == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
v.resize(p);
|
||||
|
||||
return p - p_;
|
||||
}
|
||||
|
||||
size_t Compressor::write(const containers::MemorySegment<char> &s)
|
||||
{
|
||||
auto &v = *v_;
|
||||
|
||||
auto context = (ZSTD_CStream *)i;
|
||||
|
||||
ZSTD_inBuffer in;
|
||||
in.size = s.size();
|
||||
in.src = s.data();
|
||||
in.pos = 0;
|
||||
|
||||
// beginning of stream
|
||||
size_t result = 0;
|
||||
|
||||
while(1)
|
||||
{
|
||||
auto newSize = round_up_to_multiple(p + 1, sizeMultiples);
|
||||
v.resize(newSize);
|
||||
|
||||
ZSTD_outBuffer out;
|
||||
out.dst = v.data();
|
||||
out.pos = p;
|
||||
out.size = v.size();
|
||||
|
||||
result = ZSTD_compressStream(context, &out, &in);
|
||||
p = out.pos;
|
||||
|
||||
if (ZSTD_isError(result))
|
||||
{
|
||||
error_ = result;
|
||||
break;
|
||||
}
|
||||
|
||||
if (in.pos == in.size)
|
||||
break;
|
||||
}
|
||||
|
||||
return in.pos;
|
||||
}
|
||||
|
||||
void Compressor::flush()
|
||||
{
|
||||
auto &v = *v_;
|
||||
|
||||
auto context = (ZSTD_CStream *)i;
|
||||
|
||||
// beginning of stream
|
||||
size_t result = 0;
|
||||
|
||||
while(1)
|
||||
{
|
||||
auto newSize = round_up_to_multiple(p + 1, sizeMultiples);
|
||||
v.resize(newSize);
|
||||
|
||||
ZSTD_outBuffer out;
|
||||
out.dst = v.data();
|
||||
out.pos = p;
|
||||
out.size = v.size();
|
||||
|
||||
result = ZSTD_flushStream(context, &out);
|
||||
p = out.pos;
|
||||
|
||||
if (ZSTD_isError(result))
|
||||
{
|
||||
error_ = result;
|
||||
break;
|
||||
}
|
||||
|
||||
if (out.pos < out.size)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
size_t Compressor::size() const
|
||||
{
|
||||
return p;
|
||||
}
|
||||
|
||||
bool Compressor::error () const
|
||||
{
|
||||
return error_;
|
||||
}
|
||||
|
||||
CompressPacket::CompressPacket(int level_) :
|
||||
level(level_)
|
||||
{
|
||||
}
|
||||
|
||||
size_t CompressPacket::with(V &v, F &&f)
|
||||
{
|
||||
Compressor compressor(level);
|
||||
return compressor.with(v, std::move(f));
|
||||
}
|
||||
|
||||
|
||||
|
||||
Decompressor::Decompressor()
|
||||
{
|
||||
}
|
||||
|
||||
size_t Decompressor::with(V &v, F &&f)
|
||||
{
|
||||
auto lock = lock_of(mutex);
|
||||
auto context = (ZSTD_DCtx *)decompressContexts.getContext();
|
||||
|
||||
i = (I *)context;
|
||||
v_ = &v;
|
||||
|
||||
auto _ = ExecuteOnDestruct([&]() {
|
||||
decompressContexts.putContext(context);
|
||||
i = nullptr;
|
||||
v_ = nullptr;
|
||||
});
|
||||
|
||||
if (!clearStream(context, ContextType::Decompress))
|
||||
return 0;
|
||||
|
||||
p = 0;
|
||||
f(*this);
|
||||
|
||||
v.data_ += p;
|
||||
v.size_ -= p;
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
size_t Decompressor::read(const containers::MemorySegment<char> &s)
|
||||
{
|
||||
auto &v = *v_;
|
||||
auto context = (ZSTD_DCtx *)i;
|
||||
|
||||
ZSTD_inBuffer in;
|
||||
in.size = v.size();
|
||||
in.src = v.data();
|
||||
in.pos = p;
|
||||
|
||||
ZSTD_outBuffer out;
|
||||
out.dst = (void *)s.data();
|
||||
out.pos = 0;
|
||||
out.size = s.size();
|
||||
|
||||
size_t result = 0;
|
||||
|
||||
while(1)
|
||||
{
|
||||
result = ZSTD_decompressStream(context, &out, &in);
|
||||
p = in.pos;
|
||||
|
||||
if (ZSTD_isError(result))
|
||||
{
|
||||
error_ = result;
|
||||
break;
|
||||
}
|
||||
|
||||
if (in.pos == in.size)
|
||||
break;
|
||||
|
||||
if (out.pos == out.size)
|
||||
break;
|
||||
}
|
||||
|
||||
return out.pos;
|
||||
}
|
||||
|
||||
bool Decompressor::eos() const
|
||||
{
|
||||
return error() || p >= v_->size();
|
||||
}
|
||||
|
||||
bool Decompressor::error () const
|
||||
{
|
||||
return error_;
|
||||
}
|
||||
|
||||
DecompressPacket::DecompressPacket()
|
||||
{
|
||||
}
|
||||
|
||||
size_t DecompressPacket::with(V &v, F &&f)
|
||||
{
|
||||
Decompressor decompressor;
|
||||
return decompressor.with(v, std::move(f));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
14
tjp/core/http/temp/compression/zstd.h
Normal file
14
tjp/core/http/temp/compression/zstd.h
Normal file
@@ -0,0 +1,14 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
namespace tjp::core::compression::zstd {
|
||||
|
||||
struct CompressPacket;
|
||||
struct DecompressPacket;
|
||||
|
||||
using CompressStream = CompressPacket;
|
||||
using DecompressStream = DecompressPacket;
|
||||
|
||||
} // namespace
|
||||
111
tjp/core/http/temp/compression/zstd.hpp
Normal file
111
tjp/core/http/temp/compression/zstd.hpp
Normal file
@@ -0,0 +1,111 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "zstd.h"
|
||||
#include <tjp/core/containers/Vector.h>
|
||||
#include <tjp/core/containers/MemorySegment.hpp>
|
||||
#include <tjp/core/containers/Function.hpp>
|
||||
#include <tjp/core/threads/TestMutex.hpp>
|
||||
|
||||
namespace tjp::core::compression::zstd {
|
||||
|
||||
struct Compressor
|
||||
{
|
||||
core::TestMutex mutex;
|
||||
|
||||
static constexpr auto sizeMultiples = 512;
|
||||
using V = Vector<char>;
|
||||
using I = void;
|
||||
using F = Function<void(Compressor &)>;
|
||||
|
||||
int level;
|
||||
I *i = nullptr;
|
||||
V *v_ = nullptr;
|
||||
size_t p = 0;
|
||||
size_t error_ = 0;
|
||||
|
||||
Compressor(int level);
|
||||
|
||||
size_t with(V &, F &&f);
|
||||
|
||||
size_t size() const;
|
||||
|
||||
size_t write(const containers::MemorySegment<char> &);
|
||||
void flush();
|
||||
|
||||
template<typename T>
|
||||
auto value(const T &t) {
|
||||
auto segment = containers::memory_segment_value<char>(t);
|
||||
return write(segment) == segment.size();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
auto vector(const T &t) {
|
||||
auto segment = containers::memory_segment_vector<char>(t);
|
||||
return write(segment) == segment.size();
|
||||
}
|
||||
|
||||
bool error () const;
|
||||
} ;
|
||||
|
||||
struct CompressPacket
|
||||
{
|
||||
using V = Compressor::V;
|
||||
using F = Compressor::F;
|
||||
|
||||
int level;
|
||||
|
||||
CompressPacket(int level);
|
||||
|
||||
size_t with(V &, F &&f);
|
||||
} ;
|
||||
|
||||
struct Decompressor
|
||||
{
|
||||
core::TestMutex mutex;
|
||||
|
||||
using V = containers::MemorySegment<char>;
|
||||
using I = void;
|
||||
using F = Function<void(Decompressor &)>;
|
||||
|
||||
I *i = nullptr;
|
||||
V *v_ = nullptr;
|
||||
size_t p = 0;
|
||||
size_t error_ = 0;
|
||||
|
||||
Decompressor();
|
||||
|
||||
bool eos() const;
|
||||
|
||||
size_t with(V &, F &&f);
|
||||
|
||||
size_t read(const containers::MemorySegment<char> &);
|
||||
|
||||
template<typename T>
|
||||
auto value(T &t) {
|
||||
auto segment = containers::memory_segment_value<char>(t);
|
||||
return read(segment) == segment.size();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
auto vector(T &t) {
|
||||
auto segment = containers::memory_segment_vector<char>(t);
|
||||
return read(segment) == segment.size();
|
||||
}
|
||||
|
||||
bool error () const;
|
||||
};
|
||||
|
||||
struct DecompressPacket
|
||||
{
|
||||
using V = Decompressor::V;
|
||||
using F = Decompressor::F;
|
||||
|
||||
DecompressPacket();
|
||||
|
||||
size_t with(V &, F &&);
|
||||
} ;
|
||||
|
||||
|
||||
} // namespace
|
||||
55
tjp/core/http/transform.hpp
Executable file
55
tjp/core/http/transform.hpp
Executable file
@@ -0,0 +1,55 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Types.hpp"
|
||||
#include "future_response.hpp"
|
||||
#include <tjp/core/future/Future.hpp>
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
|
||||
template<typename F>
|
||||
auto transform(F &&f)
|
||||
{
|
||||
return
|
||||
[f=std::move(f)](const Request& req)
|
||||
{
|
||||
return future_response(f, req);
|
||||
};
|
||||
}
|
||||
|
||||
template<typename Serializer, typename F>
|
||||
auto transform(F &&f)
|
||||
{
|
||||
return
|
||||
transform(
|
||||
[f=std::move(f)](const Request& req) {
|
||||
auto future = future_response(f, req);
|
||||
return future.then(
|
||||
[](auto &&f) {
|
||||
return Serializer{}(f.get());
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//template<typename Serializer, typename F>
|
||||
//auto transform(F &&f)
|
||||
//{
|
||||
// return
|
||||
// [f=std::move(f)](const Request& req, Response& res)
|
||||
// {
|
||||
// auto future = future_response(f, req);
|
||||
// return future.then(
|
||||
// [](auto &&f) {
|
||||
// auto [body, type] = Serializer{}(result);
|
||||
// res.set_content(body, type);
|
||||
// });
|
||||
// };
|
||||
//}
|
||||
|
||||
|
||||
}
|
||||
29
tjp/core/http/transform_base64_text.hpp
Executable file
29
tjp/core/http/transform_base64_text.hpp
Executable file
@@ -0,0 +1,29 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Types.hpp"
|
||||
#include "../bases/Base64.h"
|
||||
#include "transform.hpp"
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
struct ToBase64Text
|
||||
{
|
||||
template <typename T>
|
||||
BodyType operator()(const T &value) const
|
||||
{
|
||||
return {
|
||||
toBase64(value),
|
||||
"application/base64"
|
||||
};
|
||||
}
|
||||
} ;
|
||||
|
||||
template<typename F>
|
||||
auto transform_base64_text(F &&f)
|
||||
{
|
||||
return transform<ToBase64Text>(std::forward<F>(f));
|
||||
}
|
||||
|
||||
}
|
||||
46
tjp/core/http/transform_binary.hpp
Executable file
46
tjp/core/http/transform_binary.hpp
Executable file
@@ -0,0 +1,46 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Types.hpp"
|
||||
#include <tjp/core/string/as_string.hpp>
|
||||
#include <tjp/core/string/to_string.hpp>
|
||||
#include "transform.hpp"
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
struct ToBinary
|
||||
{
|
||||
template <typename T>
|
||||
BodyType operator()(T &&value) const
|
||||
{
|
||||
return {
|
||||
core::as_string(std::forward<T>(value)),
|
||||
"application/binary"
|
||||
};
|
||||
}
|
||||
} ;
|
||||
|
||||
template<typename Serializer, typename F>
|
||||
auto transform_binary(F &&f)
|
||||
{
|
||||
return
|
||||
[f=std::move(f)](const Request& req)
|
||||
{
|
||||
auto future = future_with(f(req));
|
||||
return future.then([](auto &&f) {
|
||||
return BodyType {
|
||||
Serializer{}(f.get()),
|
||||
"application/binary"
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
auto transform_binary_text(F &&f)
|
||||
{
|
||||
return transform<ToBinary>(std::forward<F>(f));
|
||||
}
|
||||
|
||||
}
|
||||
46
tjp/core/http/transform_json.hpp
Executable file
46
tjp/core/http/transform_json.hpp
Executable file
@@ -0,0 +1,46 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Types.hpp"
|
||||
#include <tjp/core/io/json/out.h>
|
||||
#include <tjp/core/string/as_string.hpp>
|
||||
#include "transform.hpp"
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
struct ToJson
|
||||
{
|
||||
template <typename T>
|
||||
BodyType operator()(const T &value) const
|
||||
{
|
||||
return {
|
||||
core::as_string(core::io::json::serialize(value)),
|
||||
"application/json"
|
||||
};
|
||||
}
|
||||
} ;
|
||||
|
||||
template<typename Serializer, typename F>
|
||||
auto transform_json(F &&f)
|
||||
{
|
||||
return
|
||||
[f=std::move(f)](const Request& req)
|
||||
{
|
||||
auto future = future_with(f(req));
|
||||
return future.then([](auto &&f) {
|
||||
return BodyType {
|
||||
Serializer{}(f.get()),
|
||||
"application/json"
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
auto transform_json(F &&f)
|
||||
{
|
||||
return transform<ToJson>(std::forward<F>(f));
|
||||
}
|
||||
|
||||
}
|
||||
29
tjp/core/http/transform_json_text.hpp
Executable file
29
tjp/core/http/transform_json_text.hpp
Executable file
@@ -0,0 +1,29 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Types.hpp"
|
||||
#include <tjp/core/string/as_string.hpp>
|
||||
#include "transform.hpp"
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
struct ToJsonText
|
||||
{
|
||||
template <typename T>
|
||||
BodyType operator()(const T &value) const
|
||||
{
|
||||
return {
|
||||
core::as_string(value),
|
||||
"application/json"
|
||||
};
|
||||
}
|
||||
} ;
|
||||
|
||||
template<typename F>
|
||||
auto transform_json_text(F &&f)
|
||||
{
|
||||
return transform<ToJsonText>(std::forward<F>(f));
|
||||
}
|
||||
|
||||
}
|
||||
30
tjp/core/http/transform_ok.hpp
Executable file
30
tjp/core/http/transform_ok.hpp
Executable file
@@ -0,0 +1,30 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Types.hpp"
|
||||
#include <tjp/core/io/json/out.h>
|
||||
#include <tjp/core/string/as_string.hpp>
|
||||
#include "transform.hpp"
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
template<typename F>
|
||||
auto transform_ok(F &&f)
|
||||
{
|
||||
return
|
||||
[f=std::move(f)](const Request& req)
|
||||
{
|
||||
auto future = future_with(f, req);
|
||||
return future.then([](auto &&f) {
|
||||
f.get();
|
||||
|
||||
return BodyType {
|
||||
"OK",
|
||||
"text/plain"
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
46
tjp/core/http/transform_plaintext.hpp
Executable file
46
tjp/core/http/transform_plaintext.hpp
Executable file
@@ -0,0 +1,46 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Types.hpp"
|
||||
#include <tjp/core/string/as_string.hpp>
|
||||
#include <tjp/core/string/to_string.hpp>
|
||||
#include "transform.hpp"
|
||||
|
||||
namespace tjp::core::http {
|
||||
|
||||
struct ToPlainText
|
||||
{
|
||||
template <typename T>
|
||||
BodyType operator()(T &&value) const
|
||||
{
|
||||
return {
|
||||
core::to_string(std::forward<T>(value)),
|
||||
"text/plain"
|
||||
};
|
||||
}
|
||||
} ;
|
||||
|
||||
template<typename Serializer, typename F>
|
||||
auto transform_plaintext(F &&f)
|
||||
{
|
||||
return
|
||||
[f=std::move(f)](const Request& req)
|
||||
{
|
||||
auto future = future_with(f(req));
|
||||
return future.then([](auto &&f) {
|
||||
return BodyType {
|
||||
Serializer{}(f.get()),
|
||||
"text/plain"
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
auto transform_plaintext(F &&f)
|
||||
{
|
||||
return transform<ToPlainText>(std::forward<F>(f));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user