Compare commits

...

9 Commits

Author SHA1 Message Date
Martin Chang
921d8056ae hacky, but working request sending 2023-11-06 10:03:00 +08:00
Martin Chang
356c17c63b format 2023-11-05 22:52:04 +08:00
Martin Chang
bda71e2002 non working: send http frames 2023-11-05 22:46:42 +08:00
Martin Chang
f132339055 properly handler protocol errors 2023-11-05 18:17:39 +08:00
Martin Chang
88f997819a update trantor 2023-11-05 17:01:04 +08:00
Martin Chang
a2dee1f5fd format yet again 2023-11-05 16:40:44 +08:00
Martin Chang
3512090f62 format again 2023-11-05 16:37:46 +08:00
Martin Chang
32dc3b5411 format, etc 2023-11-05 16:35:32 +08:00
Martin Chang
4f6034ab85 Fix dropping 1st request 2023-11-05 15:40:01 +08:00
15 changed files with 6360 additions and 85 deletions

View File

@ -281,7 +281,9 @@ set(DROGON_SOURCES
lib/src/WebSocketConnectionImpl.cc
lib/src/WebsocketControllersRouter.cc
lib/src/YamlConfigAdapter.cc
lib/src/drogon_test.cc)
lib/src/drogon_test.cc
lib/src/hpack/HPacker.cpp
lib/src/hpack/HPackTable.cpp)
set(private_headers
lib/src/AOPAdvice.h
lib/src/CacheFile.h
@ -316,7 +318,11 @@ set(private_headers
lib/src/ConfigAdapterManager.h
lib/src/JsonConfigAdapter.h
lib/src/YamlConfigAdapter.h
lib/src/ConfigAdapter.h)
lib/src/ConfigAdapter.h
lib/src/hpack/HPacker.h
lib/src/hpack/hpack_huffman_table.h
lib/src/hpack/HPackTable.h
lib/src/hpack/StaticTable.h)
if (NOT WIN32)
set(DROGON_SOURCES

View File

@ -16,7 +16,10 @@ int main()
{
trantor::Logger::setLogLevel(trantor::Logger::kTrace);
{
auto client = HttpClient::newHttpClient("https://clehaxze.tw");
auto client = HttpClient::newHttpClient("https://clehaxze.tw:8844",
nullptr,
false,
false);
client->setSockOptCallback([](int fd) {
std::cout << "setSockOptCallback:" << fd << std::endl;
#ifdef __linux__

View File

@ -162,31 +162,27 @@ class DROGON_EXPORT HttpRequest
virtual const std::string &getCookie(const std::string &field) const = 0;
/// Get all headers of the request
virtual const std::unordered_map<std::string,
std::string,
utils::internal::SafeStringHash> &
headers() const = 0;
virtual const std::
unordered_map<std::string, std::string, utils::internal::SafeStringHash>
&headers() const = 0;
/// Get all headers of the request
const std::unordered_map<std::string,
std::string,
utils::internal::SafeStringHash> &
getHeaders() const
const std::
unordered_map<std::string, std::string, utils::internal::SafeStringHash>
&getHeaders() const
{
return headers();
}
/// Get all cookies of the request
virtual const std::unordered_map<std::string,
std::string,
utils::internal::SafeStringHash> &
cookies() const = 0;
virtual const std::
unordered_map<std::string, std::string, utils::internal::SafeStringHash>
&cookies() const = 0;
/// Get all cookies of the request
const std::unordered_map<std::string,
std::string,
utils::internal::SafeStringHash> &
getCookies() const
const std::
unordered_map<std::string, std::string, utils::internal::SafeStringHash>
&getCookies() const
{
return cookies();
}
@ -305,16 +301,14 @@ class DROGON_EXPORT HttpRequest
}
/// Get parameters of the request.
virtual const std::unordered_map<std::string,
std::string,
utils::internal::SafeStringHash> &
parameters() const = 0;
virtual const std::
unordered_map<std::string, std::string, utils::internal::SafeStringHash>
&parameters() const = 0;
/// Get parameters of the request.
const std::unordered_map<std::string,
std::string,
utils::internal::SafeStringHash> &
getParameters() const
const std::
unordered_map<std::string, std::string, utils::internal::SafeStringHash>
&getParameters() const
{
return parameters();
}

View File

@ -199,16 +199,14 @@ class DROGON_EXPORT HttpResponse
virtual void removeHeader(std::string key) = 0;
/// Get all headers of the response
virtual const std::unordered_map<std::string,
std::string,
utils::internal::SafeStringHash> &
headers() const = 0;
virtual const std::
unordered_map<std::string, std::string, utils::internal::SafeStringHash>
&headers() const = 0;
/// Get all headers of the response
const std::unordered_map<std::string,
std::string,
utils::internal::SafeStringHash> &
getHeaders() const
const std::
unordered_map<std::string, std::string, utils::internal::SafeStringHash>
&getHeaders() const
{
return headers();
}
@ -237,13 +235,13 @@ class DROGON_EXPORT HttpResponse
/// Get all cookies.
virtual const std::
unordered_map<std::string, Cookie, utils::internal::SafeStringHash> &
cookies() const = 0;
unordered_map<std::string, Cookie, utils::internal::SafeStringHash>
&cookies() const = 0;
/// Get all cookies.
const std::
unordered_map<std::string, Cookie, utils::internal::SafeStringHash> &
getCookies() const
unordered_map<std::string, Cookie, utils::internal::SafeStringHash>
&getCookies() const
{
return cookies();
}
@ -514,8 +512,8 @@ class DROGON_EXPORT HttpResponse
* newStreamResponse) returns the callback function. Otherwise a
* null function.
*/
virtual const std::function<std::size_t(char *, std::size_t)> &
streamCallback() const = 0;
virtual const std::function<std::size_t(char *, std::size_t)>
&streamCallback() const = 0;
/**
* @brief Returns the content type associated with the response

View File

@ -53,6 +53,12 @@ struct ByteStream
return ptr[offset++];
}
void skip(size_t n)
{
assert(offset <= length - n);
offset += n;
}
size_t size() const
{
return length;
@ -100,25 +106,6 @@ enum class H2SettingsKey
NumEntries
};
// TODO: Either convert this to TMP or remove it
// For now it's only for development purposes and make sure
// I'm not stupid in designing APIs
#ifdef __cpp_concepts
template <typename T>
concept IsH2Frame = requires(T t) {
{
t.prettyToString()
} -> std::convertible_to<std::string>;
{
t.serialize()
} -> std::convertible_to<trantor::MsgBuffer>;
{
T::fromBuffer(ByteStream(nullptr, 0))
// if returns std::nullopt, it's an error
} -> std::convertible_to<std::optional<T>>;
};
#endif
struct SettingsFrame
{
std::vector<std::pair<uint16_t, uint32_t>> settings;
@ -129,7 +116,24 @@ struct WindowUpdateFrame
uint32_t windowSizeIncrement;
};
using H2Frame = std::variant<SettingsFrame, WindowUpdateFrame>;
struct HeadersFrame
{
uint8_t padLength = 0;
bool exclusive = false;
uint32_t streamDependency = 0;
uint8_t weight = 0;
std::vector<uint8_t> headerBlockFragment;
};
struct GoAwayFrame
{
uint32_t lastStreamId;
uint32_t errorCode;
std::vector<uint8_t> additionalDebugData;
};
using H2Frame =
std::variant<SettingsFrame, WindowUpdateFrame, HeadersFrame, GoAwayFrame>;
// Print the HEX and ASCII representation of the buffer side by side
// 16 bytes per line. Same function as the xdd command in linux.
@ -207,12 +211,146 @@ static std::optional<WindowUpdateFrame> parseWindowUpdateFrame(
return frame;
}
static std::optional<GoAwayFrame> parseGoAwayFrame(ByteStream &payload)
{
if (payload.size() < 8)
{
LOG_ERROR << "Invalid go away frame length";
return std::nullopt;
}
GoAwayFrame frame;
frame.lastStreamId = payload.readU32BE();
frame.errorCode = payload.readU32BE();
frame.additionalDebugData.resize(payload.remaining());
for (size_t i = 0; i < frame.additionalDebugData.size(); ++i)
frame.additionalDebugData[i] = payload.readU8();
LOG_TRACE << "Go away frame: lastStreamId=" << frame.lastStreamId
<< " errorCode=" << frame.errorCode
<< " additionalDebugData=" << frame.additionalDebugData.size();
return frame;
}
static std::optional<HeadersFrame> parseHeadersFrame(ByteStream &payload)
{
HeadersFrame frame;
frame.padLength = payload.readU8();
uint32_t streamDependency = payload.readU32BE();
frame.exclusive = streamDependency & (1U << 31);
frame.streamDependency = streamDependency & ((1U << 31) - 1);
frame.weight = payload.readU8();
frame.headerBlockFragment.resize(payload.remaining());
for (size_t i = 0; i < frame.headerBlockFragment.size(); ++i)
frame.headerBlockFragment[i] = payload.readU8();
// TODO: Handle padding
payload.skip(frame.padLength);
return frame;
}
static std::vector<uint8_t> serializeHeadsFrame(const HeadersFrame &frame)
{
std::vector<uint8_t> buffer;
buffer.reserve(6 + frame.headerBlockFragment.size() + frame.padLength);
// buffer.push_back(frame.padLength);
// uint32_t streamDependency = frame.streamDependency;
// if (frame.exclusive)
// streamDependency |= 1U << 31;
// buffer.push_back(streamDependency >> 24);
// buffer.push_back(streamDependency >> 16);
// buffer.push_back(streamDependency >> 8);
// buffer.push_back(streamDependency);
// buffer.push_back(frame.weight);
for (size_t i = 0; i < frame.headerBlockFragment.size(); ++i)
buffer.push_back(frame.headerBlockFragment[i]);
for (size_t i = 0; i < frame.padLength; ++i)
buffer.push_back(0x0);
return buffer;
}
static std::vector<uint8_t> serializeSettingsFrame(const SettingsFrame &frame)
{
if (frame.settings.size() == 0)
return std::vector<uint8_t>();
std::vector<uint8_t> buffer;
buffer.reserve(6 * frame.settings.size());
for (auto &[key, value] : frame.settings)
{
buffer.push_back(key >> 8);
buffer.push_back(key);
buffer.push_back(value >> 24);
buffer.push_back(value >> 16);
buffer.push_back(value >> 8);
buffer.push_back(value);
}
return buffer;
}
static std::vector<uint8_t> serializeWindowUpdateFrame(
const WindowUpdateFrame &frame)
{
std::vector<uint8_t> buffer;
buffer.reserve(4);
buffer.push_back(frame.windowSizeIncrement >> 24);
buffer.push_back(frame.windowSizeIncrement >> 16);
buffer.push_back(frame.windowSizeIncrement >> 8);
buffer.push_back(frame.windowSizeIncrement);
return buffer;
}
static std::vector<uint8_t> serializeFrame(const H2Frame &frame,
size_t streamId,
uint8_t flags = 0)
{
std::vector<uint8_t> buffer;
uint8_t type;
if (std::holds_alternative<HeadersFrame>(frame))
{
const auto &f = std::get<HeadersFrame>(frame);
buffer = serializeHeadsFrame(f);
type = (uint8_t)H2FrameType::Headers;
}
else if (std::holds_alternative<SettingsFrame>(frame))
{
const auto &f = std::get<SettingsFrame>(frame);
buffer = serializeSettingsFrame(f);
type = (uint8_t)H2FrameType::Settings;
}
else if (std::holds_alternative<WindowUpdateFrame>(frame))
{
const auto &f = std::get<WindowUpdateFrame>(frame);
buffer = serializeWindowUpdateFrame(f);
type = (uint8_t)H2FrameType::WindowUpdate;
}
else
{
LOG_ERROR << "Unsupported frame type";
abort();
}
std::vector<uint8_t> full_frame;
full_frame.reserve(9 + buffer.size());
size_t length = buffer.size();
assert(length <= 0xffffff);
full_frame.push_back(length >> 16);
full_frame.push_back(length >> 8);
full_frame.push_back(length);
full_frame.push_back(type);
full_frame.push_back(flags);
full_frame.push_back(streamId >> 24);
full_frame.push_back(streamId >> 16);
full_frame.push_back(streamId >> 8);
full_frame.push_back(streamId);
full_frame.insert(full_frame.end(), buffer.begin(), buffer.end());
return full_frame;
}
// return streamId, frame, error and should continue parsing
// Note that error can orrcur on a stream level or the entire connection
// We need to handle both cases. Also it could happen that the TCP stream
// just cuts off in the middle of a frame (or header). We need to handle that
// too.
// std::tuple<std::optional<H2Frame>, size_t, bool>
static std::tuple<std::optional<H2Frame>, size_t, bool> parseH2Frame(
trantor::MsgBuffer *msg)
{
@ -254,6 +392,10 @@ static std::tuple<std::optional<H2Frame>, size_t, bool> parseH2Frame(
frame = parseSettingsFrame(payload);
else if (type == (uint8_t)H2FrameType::WindowUpdate)
frame = parseWindowUpdateFrame(payload);
else if (type == (uint8_t)H2FrameType::GoAway)
frame = parseGoAwayFrame(payload);
else if (type == (uint8_t)H2FrameType::Headers)
frame = parseHeadersFrame(payload);
else
{
LOG_WARN << "Unsupported H2 frame type: " << (int)type;
@ -274,10 +416,87 @@ static std::tuple<std::optional<H2Frame>, size_t, bool> parseH2Frame(
return {frame, streamId, false};
}
void drogon::Http2Transport::sendRequestInLoop(const HttpRequestPtr &req,
HttpReqCallback &&callback)
{
if (!serverSettingsReceived)
{
bufferedRequests.emplace_back(req, std::move(callback));
return;
}
// HACK: Acknowledge the settings frame, move this somewhere appropriate
LOG_TRACE << "Acknowledge settings frame";
SettingsFrame settings;
auto sb = serializeFrame(settings, 0, 0);
dump_hex_beautiful(sb.data(), sb.size());
connPtr->send(sb.data(), sb.size());
sb = serializeFrame(settings, 0, 0x1);
dump_hex_beautiful(sb.data(), sb.size());
connPtr->send(sb.data(), sb.size());
WindowUpdateFrame windowUpdate;
windowUpdate.windowSizeIncrement = 200 * 1024 * 1024; // 200MB
auto wu = serializeFrame(windowUpdate, 0);
dump_hex_beautiful(wu.data(), wu.size());
connPtr->send(wu.data(), wu.size());
static hpack::HPacker hpack;
auto headers = req->headers();
HeadersFrame frame;
frame.padLength = 0;
frame.exclusive = false;
frame.streamDependency = 0;
frame.weight = 0;
frame.headerBlockFragment.resize(1024);
LOG_TRACE << "Sending HTTP/2 headers: size=" << headers.size();
hpack::HPacker::KeyValueVector headersToEncode;
const std::array<std::string_view, 2> headersToSkip = {
{"host", "connection"}};
headersToEncode.emplace_back(":method", req->methodString());
headersToEncode.emplace_back(":path", req->path());
headersToEncode.emplace_back(":scheme",
connPtr->isSSLConnection() ? "https" : "http");
headersToEncode.emplace_back(":authority", req->getHeader("host"));
for (auto &[key, value] : headers)
{
if (std::find(headersToSkip.begin(), headersToSkip.end(), key) !=
headersToSkip.end())
continue;
headersToEncode.emplace_back(key, value);
}
LOG_TRACE << "Final headers size: " << headersToEncode.size();
for (auto &[key, value] : headersToEncode)
LOG_TRACE << " " << key << ": " << value;
int n = hpack.encode(headersToEncode,
frame.headerBlockFragment.data(),
frame.headerBlockFragment.size());
if (n < 0)
{
LOG_ERROR << "Failed to encode headers";
abort();
return;
}
LOG_TRACE << "Encoded headers size: " << n;
frame.headerBlockFragment.resize(n);
LOG_TRACE << "Encoded headers:";
dump_hex_beautiful(frame.headerBlockFragment.data(),
frame.headerBlockFragment.size());
LOG_TRACE << "Sending headers frame";
auto f = serializeFrame(frame, 1, 0x5);
dump_hex_beautiful(f.data(), f.size());
connPtr->send(f.data(), f.size());
}
void Http2Transport::onRecvMessage(const trantor::TcpConnectionPtr &,
trantor::MsgBuffer *msg)
{
LOG_TRACE << "HTTP/2 message received:";
assert(bytesReceived_ != nullptr);
*bytesReceived_ += msg->readableBytes();
dump_hex_beautiful(msg->peek(), msg->readableBytes());
while (true)
{
@ -285,14 +504,15 @@ void Http2Transport::onRecvMessage(const trantor::TcpConnectionPtr &,
if (error && streamId == 0)
{
abort();
LOG_ERROR << "Fatal protocol error happened on stream 0 (global)";
errorCallback(ReqResult::BadResponse);
}
else if (frameOpt.has_value() == false && !error)
break;
else if (!frameOpt.has_value())
{
LOG_ERROR << "Failed to parse H2 frame??? Shouldn't happen though";
abort();
errorCallback(ReqResult::BadResponse);
}
auto &frame = *frameOpt;
@ -327,10 +547,30 @@ void Http2Transport::onRecvMessage(const trantor::TcpConnectionPtr &,
LOG_TRACE << "Unsupported settings key: " << key;
}
}
if (streamId == 0 && !serverSettingsReceived)
{
serverSettingsReceived = true;
for (auto &[req, cb] : bufferedRequests)
sendRequestInLoop(req, std::move(cb));
bufferedRequests.clear();
}
}
else if (std::holds_alternative<GoAwayFrame>(frame))
{
LOG_ERROR << "Go away frame received. Die!";
auto &f = std::get<GoAwayFrame>(frame);
// TODO: Depening on the streamId, we need to kill the entire
// connection or just the stream
errorCallback(ReqResult::BadResponse);
}
else
{
// TODO: Remove this once we support all frame types
// in that case it'll be a parsing error or bad server
LOG_ERROR << "Boom! The client does not understand this frame";
}
}
throw std::runtime_error("HTTP/2 onRecvMessage not implemented");
}
Http2Transport::Http2Transport(trantor::TcpConnectionPtr connPtr,

View File

@ -1,9 +1,27 @@
#pragma once
#include "HttpTransport.h"
// TOOD: Write our own HPACK implementation
#include "hpack/HPacker.h"
namespace drogon
{
namespace internal
{
// Virtual stream that holds properties for the HTTP/2 stream
// Defaults to stream 0 global properties
struct H2Stream
{
size_t maxConcurrentStreams = 100;
size_t initialWindowSize = 65535;
size_t maxFrameSize = 16384;
size_t avaliableWindowSize = 0;
};
} // namespace internal
class Http2Transport : public HttpTransport
{
private:
@ -17,17 +35,17 @@ class Http2Transport : public HttpTransport
size_t maxFrameSize = 16384;
size_t avaliableWindowSize = 0;
// Set after server settings are received
bool serverSettingsReceived = false;
std::vector<std::pair<HttpRequestPtr, HttpReqCallback>> bufferedRequests;
public:
Http2Transport(trantor::TcpConnectionPtr connPtr,
size_t *bytesSent,
size_t *bytesReceived);
void sendRequestInLoop(const HttpRequestPtr &req,
HttpReqCallback &&callback) override
{
// throw std::runtime_error("HTTP/2 sendRequestInLoop not implemented");
}
HttpReqCallback &&callback) override;
void onRecvMessage(const trantor::TcpConnectionPtr &,
trantor::MsgBuffer *) override;
@ -46,5 +64,8 @@ class Http2Transport : public HttpTransport
{
throw std::runtime_error("HTTP/2 onError not implemented");
}
protected:
void onServerSettingsReceived(){};
};
} // namespace drogon

View File

@ -70,9 +70,8 @@ void Http1xTransport::onRecvMessage(const trantor::TcpConnectionPtr &conn,
}
if (!responseParser->parseResponse(msg))
{
// TODO: Make upper layer flush all requests in the buffer
onError(ReqResult::BadResponse);
*bytesReceived_ += (msgSize - msg->readableBytes());
errorCallback(ReqResult::BadResponse);
return;
}
if (responseParser->gotAll())
@ -190,6 +189,12 @@ void HttpClientImpl::createTcpClient()
return;
thisPtr->handleResponse(resp, std::move(reqAndCb), connPtr);
});
thisPtr->transport_->setErrorCallback([weakPtr](ReqResult result) {
auto thisPtr = weakPtr.lock();
if (!thisPtr)
return;
thisPtr->onError(result);
});
// TODO: respect timeout and pipeliningDepth_
while (!thisPtr->requestsBuffer_.empty())
@ -533,6 +538,16 @@ void HttpClientImpl::sendRequestInLoop(const drogon::HttpRequestPtr &req,
if (!tcpClientPtr_)
{
auto callbackPtr =
std::make_shared<drogon::HttpReqCallback>(std::move(callback));
requestsBuffer_.push_back(
{req,
[thisPtr = shared_from_this(),
callbackPtr =
std::move(callbackPtr)](ReqResult result,
const HttpResponsePtr &response) {
(*callbackPtr)(result, response);
}});
if (domain_.empty() || !isDomainName_)
{
// Valid ip address, no domain, connect directly
@ -549,15 +564,6 @@ void HttpClientImpl::sendRequestInLoop(const drogon::HttpRequestPtr &req,
return;
}
auto callbackPtr =
std::make_shared<drogon::HttpReqCallback>(std::move(callback));
requestsBuffer_.push_back(
{req,
[thisPtr = shared_from_this(),
callbackPtr](ReqResult result, const HttpResponsePtr &response) {
(*callbackPtr)(result, response);
}});
// A dns query is on going.
if (dns_)
{

View File

@ -7,7 +7,6 @@
#include <trantor/net/TcpConnection.h>
#include <trantor/utils/NonCopyable.h>
namespace drogon
{
@ -21,6 +20,10 @@ class HttpTransport : public trantor::NonCopyable
virtual void onRecvMessage(const trantor::TcpConnectionPtr &,
trantor::MsgBuffer *) = 0;
virtual bool handleConnectionClose() = 0;
// XXX: Footgun: This MUST be called by the HttpClient. DO NOT
// call this within Transport. Client needs to know that error
// happened. When in doubt, call error callback.
virtual void onError(ReqResult result) = 0;
virtual size_t requestsInFlight() const = 0;
@ -29,13 +32,20 @@ class HttpTransport : public trantor::NonCopyable
std::function<void(const HttpResponseImplPtr &,
std::pair<HttpRequestPtr, HttpReqCallback> &&,
const trantor::TcpConnectionPtr)>;
using ErrorCallback = std::function<void(ReqResult)>;
void setRespCallback(RespCallback cb)
{
respCallback = std::move(cb);
}
void setErrorCallback(ErrorCallback cb)
{
errorCallback = std::move(cb);
}
RespCallback respCallback;
ErrorCallback errorCallback;
};
}
} // namespace drogon

View File

@ -0,0 +1,220 @@
/* Copyright (c) 2016, Fengping Bao <jamol@live.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <cstdint>
#include "HPackTable.h"
using namespace hpack;
#include "StaticTable.h"
HPackTable::HPackTable()
{
for (int i = 0; i < HPACK_STATIC_TABLE_SIZE; ++i)
{
std::string key =
hpackStaticTable[i].first + hpackStaticTable[i].second;
indexMap_.emplace(std::move(key), std::make_pair(-1, i));
}
}
bool HPackTable::getIndexedName(int index, std::string &name)
{
if (index <= 0)
{
return false;
}
if (index < HPACK_DYNAMIC_START_INDEX)
{
name = hpackStaticTable[index - 1].first;
}
else if (index - HPACK_DYNAMIC_START_INDEX <
static_cast<int>(dynamicTable_.size()))
{
name = dynamicTable_[index - HPACK_DYNAMIC_START_INDEX].first;
}
else
{
return false;
}
return true;
}
bool HPackTable::getIndexedValue(int index, std::string &value)
{
if (index <= 0)
{
return false;
}
if (index < HPACK_DYNAMIC_START_INDEX)
{
value = hpackStaticTable[index - 1].second;
}
else if (index - HPACK_DYNAMIC_START_INDEX <
static_cast<int>(dynamicTable_.size()))
{
value = dynamicTable_[index - HPACK_DYNAMIC_START_INDEX].second;
}
else
{
return false;
}
return true;
}
bool HPackTable::addHeader(const std::string &name, const std::string &value)
{
uint32_t entrySize =
uint32_t(name.length() + value.length() + TABLE_ENTRY_SIZE_EXTRA);
if (entrySize + tableSize_ > limitSize_)
{
evictTableBySize(entrySize + tableSize_ - limitSize_);
}
if (entrySize > limitSize_)
{
return false;
}
dynamicTable_.push_front(std::make_pair(name, value));
tableSize_ += entrySize;
if (isEncoder_)
{
std::string key = name + value;
updateIndex(key, ++indexSequence_);
}
return true;
}
void HPackTable::setMaxSize(size_t maxSize)
{
maxSize_ = maxSize;
if (limitSize_ > maxSize_)
{
updateLimitSize(maxSize_);
}
}
void HPackTable::updateLimitSize(size_t limitSize)
{
if (tableSize_ > limitSize)
{
evictTableBySize(tableSize_ - limitSize);
}
limitSize_ = limitSize;
}
void HPackTable::evictTableBySize(size_t size)
{
uint32_t evicted = 0;
while (evicted < size && !dynamicTable_.empty())
{
auto &entry = dynamicTable_.back();
uint32_t entrySize =
uint32_t(entry.first.length() + entry.second.length() +
TABLE_ENTRY_SIZE_EXTRA);
tableSize_ -= tableSize_ > entrySize ? entrySize : tableSize_;
if (isEncoder_)
{
std::string key = entry.first + entry.second;
removeIndex(key);
}
dynamicTable_.pop_back();
evicted += entrySize;
}
}
int HPackTable::getDynamicIndex(int idxSeq)
{
return -1 == idxSeq ? -1 : indexSequence_ - idxSeq;
}
void HPackTable::updateIndex(const std::string &key, int idxSeq)
{
auto it = indexMap_.find(key);
if (it != indexMap_.end())
{
it->second.first = idxSeq;
}
else
{
indexMap_.emplace(key, std::make_pair(idxSeq, -1));
}
}
void HPackTable::removeIndex(const std::string &key)
{
auto it = indexMap_.find(key);
if (it != indexMap_.end())
{
int idx = getDynamicIndex(it->second.first);
if (idx == dynamicTable_.size() - 1)
{
if (it->second.second == -1)
{
indexMap_.erase(it);
}
else
{
it->second.first = -1; // reset dynamic table index
}
}
}
}
bool HPackTable::getIndex(const std::string &key, int &indexD, int &indexS)
{
indexD = -1;
indexS = -1;
auto it = indexMap_.find(key);
if (it != indexMap_.end())
{
indexD = getDynamicIndex(it->second.first);
indexS = it->second.second;
return true;
}
return false;
}
int HPackTable::getIndex(const std::string &name,
const std::string &value,
bool &valueIndexed)
{
int index = -1, indexD = -1, indexS = -1;
valueIndexed = false;
std::string key = name + value;
if (!getIndex(key, indexD, indexS))
{
getIndex(name, indexD, indexS);
}
if (indexD != -1 && indexD < static_cast<int>(dynamicTable_.size()) &&
name == dynamicTable_[indexD].first)
{
index = indexD + HPACK_DYNAMIC_START_INDEX;
valueIndexed = dynamicTable_[indexD].second == value;
}
else if (indexS != -1 && indexS < HPACK_STATIC_TABLE_SIZE &&
name == hpackStaticTable[indexS].first)
{
index = indexS + 1;
valueIndexed = hpackStaticTable[indexS].second == value;
}
return index;
}

View File

@ -0,0 +1,93 @@
/* Copyright (c) 2016, Fengping Bao <jamol@live.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#ifndef __HPackTable_H__
#define __HPackTable_H__
#include <string>
#include <deque>
#include <map>
namespace hpack
{
class HPackTable
{
public:
using KeyValuePair = std::pair<std::string, std::string>;
public:
HPackTable();
void setMode(bool isEncoder)
{
isEncoder_ = isEncoder;
}
void setMaxSize(size_t maxSize);
void updateLimitSize(size_t limitSize);
int getIndex(const std::string &name,
const std::string &value,
bool &valueIndexed);
bool getIndexedName(int index, std::string &name);
bool getIndexedValue(int index, std::string &value);
bool addHeader(const std::string &name, const std::string &value);
size_t getMaxSize()
{
return maxSize_;
}
size_t getLimitSize()
{
return limitSize_;
}
size_t getTableSize()
{
return tableSize_;
}
private:
int getDynamicIndex(int idxSeq);
void updateIndex(const std::string &key, int idxSeq);
void removeIndex(const std::string &key);
bool getIndex(const std::string &key, int &indexD, int &indexS);
void evictTableBySize(size_t size);
private:
using KeyValueQueue = std::deque<KeyValuePair>;
using IndexMap = std::map<std::string, std::pair<int, int>>;
KeyValueQueue dynamicTable_;
size_t tableSize_ = 0;
size_t limitSize_ = 4096;
size_t maxSize_ = 4096;
bool isEncoder_ = false;
int indexSequence_ = 0;
IndexMap indexMap_; // <key(header name[ + value]), <dynamic index
// sequence, static index>>
};
} // namespace hpack
#endif /* __HPackTable_H__ */

526
lib/src/hpack/HPacker.cpp Normal file
View File

@ -0,0 +1,526 @@
/* Copyright (c) 2016, Fengping Bao <jamol@live.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <cstdint>
#include "HPacker.h"
#include "hpack_huffman_table.h"
#include <math.h>
#include <string.h> // for memcpy
namespace hpack
{
static char *huffDecodeBits(char *dst,
uint8_t bits,
uint8_t *state,
bool *ending)
{
const auto &entry = huff_decode_table[*state][bits];
if ((entry.flags & NGHTTP2_HUFF_FAIL) != 0)
return nullptr;
if ((entry.flags & NGHTTP2_HUFF_SYM) != 0)
*dst++ = entry.sym;
*state = entry.state;
*ending = (entry.flags & NGHTTP2_HUFF_ACCEPTED) != 0;
return dst;
}
static int huffDecode(const uint8_t *src, size_t len, std::string &str)
{
uint8_t state = 0;
bool ending = false;
const uint8_t *src_end = src + len;
std::vector<char> sbuf;
sbuf.resize(2 * len);
char *ptr = &sbuf[0];
for (; src != src_end; ++src)
{
if ((ptr = huffDecodeBits(ptr, *src >> 4, &state, &ending)) == nullptr)
return -1;
if ((ptr = huffDecodeBits(ptr, *src & 0xf, &state, &ending)) == nullptr)
return -1;
}
if (!ending)
{
return -1;
}
int slen = int(ptr - &sbuf[0]);
str.assign(&sbuf[0], slen);
return slen;
}
static int huffEncode(const std::string &str, uint8_t *buf, size_t len)
{
uint8_t *ptr = buf;
// const uint8_t *end = buf + len;
const char *src = str.c_str();
const char *src_end = src + str.length();
uint64_t current = 0;
uint32_t n = 0;
for (; src != src_end;)
{
const auto &sym = huff_sym_table[*src++];
uint32_t code = sym.code;
uint32_t nbits = sym.nbits;
current <<= nbits;
current |= code;
n += nbits;
while (n >= 8)
{
n -= 8;
*ptr++ = static_cast<uint8_t>(current >> n);
}
}
if (n > 0)
{
current <<= (8 - n);
current |= (0xFF >> n);
*ptr++ = static_cast<uint8_t>(current);
}
return int(ptr - buf);
}
static uint32_t huffEncodeLength(const std::string &str)
{
uint32_t len = 0;
for (char c : str)
{
len += huff_sym_table[c].nbits;
}
return (len + 7) >> 3;
}
static int encodeInteger(uint8_t N, uint64_t I, uint8_t *buf, size_t len)
{
uint8_t *ptr = buf;
const uint8_t *end = buf + len;
if (ptr == end)
{
return -1;
}
uint8_t NF = (1 << N) - 1;
if (I < NF)
{
*ptr &= NF ^ 0xFF;
*ptr |= I;
return 1;
}
*ptr++ |= NF;
I -= NF;
while (ptr < end && I >= 128)
{
*ptr++ = I % 128 + 128;
I /= 128;
}
if (ptr == end)
{
return -1;
}
*ptr++ = static_cast<uint8_t>(I);
return int(ptr - buf);
}
static int encodeString(const std::string &str, uint8_t *buf, size_t len)
{
uint8_t *ptr = buf;
uint8_t *end = buf + len;
int slen = int(str.length());
int hlen = huffEncodeLength(str);
if (hlen < slen)
{
*ptr = 0x80;
int ret = encodeInteger(7, hlen, ptr, end - ptr);
if (ret <= 0)
{
return -1;
}
ptr += ret;
ret = huffEncode(str, ptr, end - ptr);
if (ret < 0)
{
return -1;
}
ptr += ret;
}
else
{
*ptr = 0;
int ret = encodeInteger(7, slen, ptr, end - ptr);
if (ret <= 0)
{
return -1;
}
ptr += ret;
if (static_cast<size_t>(end - ptr) < str.length())
{
return -1;
}
memcpy(ptr, str.c_str(), str.length());
ptr += str.length();
}
return int(ptr - buf);
}
static int decodeInteger(uint8_t N, const uint8_t *buf, size_t len, uint64_t &I)
{
if (N > 8)
{
return -1;
}
const uint8_t *ptr = buf;
const uint8_t *end = buf + len;
if (ptr == end)
{
return -1;
}
uint8_t NF = (1 << N) - 1;
uint8_t prefix = (*ptr++) & NF;
if (prefix < NF)
{
I = prefix;
return 1;
}
if (ptr == end)
{
return -1;
}
int m = 0;
uint64_t u64 = prefix;
uint8_t b = 0;
do
{
b = *ptr++;
u64 += static_cast<uint64_t>((b & 127) * pow(2, m));
m += 7;
} while (ptr < end && (b & 128));
if (ptr == end && (b & 128))
{
return -1;
}
I = u64;
return int(ptr - buf);
}
static int decodeString(const uint8_t *buf, size_t len, std::string &str)
{
const uint8_t *ptr = buf;
const uint8_t *end = buf + len;
if (ptr == end)
{
return -1;
}
bool H = !!(*ptr & 0x80);
uint64_t slen = 0;
int ret = decodeInteger(7, ptr, end - ptr, slen);
if (ret <= 0)
{
return -1;
}
ptr += ret;
if (slen > static_cast<size_t>(end - ptr))
{
return -1;
}
if (H)
{
if (huffDecode(ptr, static_cast<size_t>(slen), str) < 0)
{
return -1;
}
}
else
{
str.assign((const char *)ptr, static_cast<size_t>(slen));
}
ptr += slen;
return int(ptr - buf);
}
enum class PrefixType
{
INDEXED_HEADER,
LITERAL_HEADER_WITH_INDEXING,
LITERAL_HEADER_WITHOUT_INDEXING,
TABLE_SIZE_UPDATE
};
static int decodePrefix(const uint8_t *buf,
size_t len,
PrefixType &type,
uint64_t &I)
{
const uint8_t *ptr = buf;
const uint8_t *end = buf + len;
uint8_t N = 0;
if (*ptr & 0x80)
{
N = 7;
type = PrefixType::INDEXED_HEADER;
}
else if (*ptr & 0x40)
{
N = 6;
type = PrefixType::LITERAL_HEADER_WITH_INDEXING;
}
else if (*ptr & 0x20)
{
N = 5;
type = PrefixType::TABLE_SIZE_UPDATE;
}
else
{
N = 4;
type = PrefixType::LITERAL_HEADER_WITHOUT_INDEXING;
}
int ret = decodeInteger(N, ptr, end - ptr, I);
if (ret <= 0)
{
return -1;
}
ptr += ret;
return int(ptr - buf);
}
HPacker::IndexingType HPacker::getIndexingType(const std::string &name,
const std::string &value)
{
if (query_cb_)
{
return query_cb_(name, value);
}
if (name == "cookie" || name == ":authority" || name == "user-agent" ||
name == "pragma")
{
return IndexingType::ALL;
}
return IndexingType::NONE;
}
int HPacker::encodeSizeUpdate(int sz, uint8_t *buf, size_t len)
{
uint8_t *ptr = buf;
const uint8_t *end = buf + len;
*ptr = 0x20;
int ret = encodeInteger(5, sz, ptr, end - ptr);
if (ret <= 0)
{
return -1;
}
ptr += ret;
return int(ptr - buf);
}
int HPacker::encodeHeader(const std::string &name,
const std::string &value,
uint8_t *buf,
size_t len)
{
uint8_t *ptr = buf;
const uint8_t *end = buf + len;
bool valueIndexed = false;
int index = table_.getIndex(name, value, valueIndexed);
bool addToTable = false;
if (index != -1)
{
uint8_t N = 0;
if (valueIndexed)
{ // name and value indexed
*ptr = 0x80;
N = 7;
}
else
{ // name indexed
IndexingType idxType = getIndexingType(name, value);
if (idxType == IndexingType::ALL)
{
*ptr = 0x40;
N = 6;
addToTable = true;
}
else
{
*ptr = 0x10;
N = 4;
}
}
// encode prefix Bits
int ret = encodeInteger(N, index, ptr, end - ptr);
if (ret <= 0)
{
return -1;
}
ptr += ret;
if (!valueIndexed)
{
ret = encodeString(value, ptr, end - ptr);
if (ret <= 0)
{
return -1;
}
ptr += ret;
}
}
else
{
IndexingType idxType = getIndexingType(name, value);
if (idxType == IndexingType::ALL)
{
*ptr++ = 0x40;
addToTable = true;
}
else
{
*ptr++ = 0x10;
}
int ret = encodeString(name, ptr, end - ptr);
if (ret <= 0)
{
return -1;
}
ptr += ret;
ret = encodeString(value, ptr, end - ptr);
if (ret <= 0)
{
return -1;
}
ptr += ret;
}
if (addToTable)
{
table_.addHeader(name, value);
}
return int(ptr - buf);
}
int HPacker::encode(const KeyValueVector &headers, uint8_t *buf, size_t len)
{
table_.setMode(true);
uint8_t *ptr = buf;
const uint8_t *end = buf + len;
if (updateTableSize_)
{
updateTableSize_ = false;
int ret = encodeSizeUpdate(int(table_.getLimitSize()), ptr, end - ptr);
if (ret <= 0)
{
return -1;
}
ptr += ret;
}
for (const auto &hdr : headers)
{
int ret = encodeHeader(hdr.first, hdr.second, ptr, end - ptr);
if (ret <= 0)
{
return -1;
}
ptr += ret;
}
return int(ptr - buf);
}
int HPacker::decode(const uint8_t *buf, size_t len, KeyValueVector &headers)
{
table_.setMode(false);
const uint8_t *ptr = buf;
const uint8_t *end = buf + len;
headers.clear();
while (ptr < end)
{
std::string name;
std::string value;
PrefixType type;
uint64_t I = 0;
int ret = decodePrefix(ptr, end - ptr, type, I);
if (ret <= 0)
{
return -1;
}
ptr += ret;
if (PrefixType::INDEXED_HEADER == type)
{
if (!table_.getIndexedName(int(I), name) ||
!table_.getIndexedValue(int(I), value))
{
return -1;
}
}
else if (PrefixType::LITERAL_HEADER_WITH_INDEXING == type ||
PrefixType::LITERAL_HEADER_WITHOUT_INDEXING == type)
{
if (0 == I)
{
ret = decodeString(ptr, end - ptr, name);
if (ret <= 0)
{
return -1;
}
ptr += ret;
}
else if (!table_.getIndexedName(int(I), name))
{
return -1;
}
ret = decodeString(ptr, end - ptr, value);
if (ret <= 0)
{
return -1;
}
ptr += ret;
if (PrefixType::LITERAL_HEADER_WITH_INDEXING == type)
{
table_.addHeader(name, value);
}
}
else if (PrefixType::TABLE_SIZE_UPDATE == type)
{
if (I > table_.getMaxSize())
{
return -1;
}
table_.updateLimitSize(static_cast<size_t>(I));
continue;
}
headers.emplace_back(std::make_pair(name, value));
}
return int(len);
}
} // namespace hpack

81
lib/src/hpack/HPacker.h Normal file
View File

@ -0,0 +1,81 @@
/* Copyright (c) 2016, Fengping Bao <jamol@live.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#ifndef __HPacker_H__
#define __HPacker_H__
#include <string>
#include <vector>
#include <functional>
#include "HPackTable.h"
namespace hpack
{
class HPacker
{
public:
enum class IndexingType
{
NONE, // not index
NAME, // index name only
ALL // index name and value
};
using KeyValuePair = HPackTable::KeyValuePair;
using KeyValueVector = std::vector<KeyValuePair>;
using IndexingTypeCallback =
std::function<IndexingType(const std::string &, const std::string &)>;
public:
int encode(const KeyValueVector &headers, uint8_t *buf, size_t len);
int decode(const uint8_t *buf, size_t len, KeyValueVector &headers);
void setMaxTableSize(size_t maxSize)
{
table_.setMaxSize(maxSize);
}
void setIndexingTypeCallback(IndexingTypeCallback cb)
{
query_cb_ = std::move(cb);
}
private:
int encodeHeader(const std::string &name,
const std::string &value,
uint8_t *buf,
size_t len);
int encodeSizeUpdate(int sz, uint8_t *buf, size_t len);
IndexingType getIndexingType(const std::string &name,
const std::string &value);
private:
HPackTable table_;
IndexingTypeCallback query_cb_;
bool updateTableSize_ = true;
};
} // namespace hpack
#endif /* __HPacker_H__ */

View File

@ -0,0 +1,84 @@
namespace hpack
{
#define TABLE_ENTRY_SIZE_EXTRA 32
#define HPACK_DYNAMIC_START_INDEX 62
#define HPACK_STATIC_TABLE_SIZE 61
static const HPackTable::KeyValuePair
hpackStaticTable[HPACK_STATIC_TABLE_SIZE] = {
std::make_pair(":authority", ""),
std::make_pair(":method", "GET"),
std::make_pair(":method", "POST"),
std::make_pair(":path", "/"),
std::make_pair(":path", "/index.html"),
std::make_pair(":scheme", "http"),
std::make_pair(":scheme", "https"),
std::make_pair(":status", "200"),
std::make_pair(":status", "204"),
std::make_pair(":status", "206"),
std::make_pair(":status", "304"),
std::make_pair(":status", "400"),
std::make_pair(":status", "404"),
std::make_pair(":status", "500"),
std::make_pair("accept-charset", ""),
std::make_pair("accept-encoding", "gzip, deflate"),
std::make_pair("accept-language", ""),
std::make_pair("accept-ranges", ""),
std::make_pair("accept", ""),
std::make_pair("access-control-allow-origin", ""),
std::make_pair("age", ""),
std::make_pair("allow", ""),
std::make_pair("authorization", ""),
std::make_pair("cache-control", ""),
std::make_pair("content-disposition", ""),
std::make_pair("content-encoding", ""),
std::make_pair("content-language", ""),
std::make_pair("content-length", ""),
std::make_pair("content-location", ""),
std::make_pair("content-range", ""),
std::make_pair("content-type", ""),
std::make_pair("cookie", ""),
std::make_pair("date", ""),
std::make_pair("etag", ""),
std::make_pair("expect", ""),
std::make_pair("expires", ""),
std::make_pair("from", ""),
std::make_pair("host", ""),
std::make_pair("if-match", ""),
std::make_pair("if-modified-since", ""),
std::make_pair("if-none-match", ""),
std::make_pair("if-range", ""),
std::make_pair("if-unmodified-since", ""),
std::make_pair("last-modified", ""),
std::make_pair("link", ""),
std::make_pair("location", ""),
std::make_pair("max-forwards", ""),
std::make_pair("proxy-authenticate", ""),
std::make_pair("proxy-authorization", ""),
std::make_pair("range", ""),
std::make_pair("referer", ""),
std::make_pair("refresh", ""),
std::make_pair("retry-after", ""),
std::make_pair("server", ""),
std::make_pair("set-cookie", ""),
std::make_pair("strict-transport-security", ""),
std::make_pair("transfer-encoding", ""),
std::make_pair("user-agent", ""),
std::make_pair("vary", ""),
std::make_pair("via", ""),
std::make_pair("www-authenticate", "")};
} // namespace hpack

File diff suppressed because it is too large Load Diff

@ -1 +1 @@
Subproject commit 26a599857a2fa5d61823ef85012e3d9123d5a675
Subproject commit f754e1caa2ce58d2457a2aab107bb6f8cec7f79e