优化文件上传速度,增加请求头预校验功能
This commit is contained in:
@@ -120,7 +120,8 @@ endif()
|
||||
# Install lib and header files.
|
||||
# On Linux, if CMAKE_INSTALL_PREFIX is ~, the lib (libwebcc.a) will be installed
|
||||
# to ~/lib and header files will be installed to ~/include.
|
||||
install(TARGETS ${TARGET} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
# install(TARGETS ${TARGET} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(TARGETS ${TARGET} ARCHIVE DESTINATION $<$<CONFIG:Debug>:debug/>${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES ${HEADERS} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/webcc)
|
||||
install(FILES ${PROJECT_BINARY_DIR}/webcc/config.h
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/webcc)
|
||||
|
||||
+307
-235
@@ -8,305 +8,377 @@
|
||||
#include "webcc/string.h"
|
||||
#include "webcc/utility.h"
|
||||
|
||||
namespace webcc {
|
||||
namespace webcc
|
||||
{
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
bool Headers::Set(string_view key, string_view value) {
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
bool Headers::Set(string_view key, string_view value)
|
||||
{
|
||||
if (value.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto it = Find(key);
|
||||
if (it != headers_.end()) {
|
||||
it->second = ToString(value);
|
||||
} else {
|
||||
headers_.push_back({ ToString(key), ToString(value) });
|
||||
}
|
||||
auto it = Find(key);
|
||||
if (it != headers_.end())
|
||||
{
|
||||
it->second = ToString(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
headers_.push_back({ ToString(key), ToString(value) });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Headers::Has(string_view key) const {
|
||||
return const_cast<Headers*>(this)->Find(key) != headers_.end();
|
||||
}
|
||||
bool Headers::Has(string_view key) const
|
||||
{
|
||||
return const_cast<Headers*>(this)->Find(key) != headers_.end();
|
||||
}
|
||||
|
||||
const std::string& Headers::Get(string_view key, bool* existed) const {
|
||||
auto it = const_cast<Headers*>(this)->Find(key);
|
||||
const std::string& Headers::Get(string_view key, bool* existed) const
|
||||
{
|
||||
auto it = const_cast<Headers*>(this)->Find(key);
|
||||
|
||||
if (existed != nullptr) {
|
||||
*existed = (it != headers_.end());
|
||||
}
|
||||
if (existed != nullptr)
|
||||
{
|
||||
*existed = (it != headers_.end());
|
||||
}
|
||||
|
||||
if (it != headers_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
if (it != headers_.end())
|
||||
{
|
||||
return it->second;
|
||||
}
|
||||
|
||||
static const std::string s_no_value;
|
||||
return s_no_value;
|
||||
}
|
||||
static const std::string s_no_value;
|
||||
return s_no_value;
|
||||
}
|
||||
|
||||
std::vector<Header>::iterator Headers::Find(string_view key) {
|
||||
auto it = headers_.begin();
|
||||
for (; it != headers_.end(); ++it) {
|
||||
if (boost::iequals(it->first, key)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return it;
|
||||
}
|
||||
std::vector<Header>::iterator Headers::Find(string_view key)
|
||||
{
|
||||
auto it = headers_.begin();
|
||||
for (; it != headers_.end(); ++it)
|
||||
{
|
||||
if (boost::iequals(it->first, key))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
static bool ParseValue(const std::string& str, const char* expected_key,
|
||||
string_view* value) {
|
||||
string_view key;
|
||||
if (!SplitKV(str, '=', true, &key, value)) {
|
||||
return false;
|
||||
}
|
||||
if (key != expected_key) {
|
||||
return false;
|
||||
}
|
||||
return !value->empty();
|
||||
}
|
||||
static bool ParseValue(const std::string& str, const char* expected_key,
|
||||
string_view* value)
|
||||
{
|
||||
string_view key;
|
||||
if (!SplitKV(str, '=', true, &key, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (key != expected_key)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return !value->empty();
|
||||
}
|
||||
|
||||
ContentType::ContentType(string_view str) {
|
||||
Init(str);
|
||||
}
|
||||
ContentType::ContentType(string_view str)
|
||||
{
|
||||
Init(str);
|
||||
}
|
||||
|
||||
void ContentType::Parse(string_view str) {
|
||||
Reset();
|
||||
Init(str);
|
||||
}
|
||||
void ContentType::Parse(string_view str)
|
||||
{
|
||||
Reset();
|
||||
Init(str);
|
||||
}
|
||||
|
||||
void ContentType::Reset() {
|
||||
media_type_.clear();
|
||||
additional_.clear();
|
||||
multipart_ = false;
|
||||
}
|
||||
void ContentType::Reset()
|
||||
{
|
||||
media_type_.clear();
|
||||
additional_.clear();
|
||||
multipart_ = false;
|
||||
}
|
||||
|
||||
bool ContentType::Valid() const {
|
||||
if (media_type_.empty()) {
|
||||
return false;
|
||||
}
|
||||
bool ContentType::Valid() const
|
||||
{
|
||||
if (media_type_.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (multipart_) {
|
||||
return !boundary().empty();
|
||||
}
|
||||
if (multipart_)
|
||||
{
|
||||
return !boundary().empty();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ContentType::Init(string_view str) {
|
||||
std::string other;
|
||||
void ContentType::Init(string_view str)
|
||||
{
|
||||
std::string other;
|
||||
|
||||
std::size_t pos = str.find(';');
|
||||
if (pos == str.npos) {
|
||||
media_type_ = ToString(str);
|
||||
} else {
|
||||
media_type_ = ToString(str.substr(0, pos));
|
||||
other = ToString(str.substr(pos + 1));
|
||||
}
|
||||
std::size_t pos = str.find(';');
|
||||
if (pos == str.npos)
|
||||
{
|
||||
media_type_ = ToString(str);
|
||||
}
|
||||
else
|
||||
{
|
||||
media_type_ = ToString(str.substr(0, pos));
|
||||
other = ToString(str.substr(pos + 1));
|
||||
}
|
||||
|
||||
boost::trim(media_type_);
|
||||
boost::trim(other);
|
||||
boost::trim(media_type_);
|
||||
boost::trim(other);
|
||||
|
||||
if (media_type_ == "multipart/form-data") {
|
||||
multipart_ = true;
|
||||
string_view boundary;
|
||||
if (ParseValue(other, "boundary", &boundary)) {
|
||||
additional_ = ToString(boundary);
|
||||
LOG_INFO("Content-type multipart boundary: %s", additional_.c_str());
|
||||
} else {
|
||||
LOG_ERRO("Invalid 'multipart/form-data' content-type (no boundary)");
|
||||
}
|
||||
} else {
|
||||
string_view charset;
|
||||
if (ParseValue(other, "charset", &charset)) {
|
||||
additional_ = ToString(charset);
|
||||
LOG_INFO("Content-type charset: %s", additional_.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (media_type_ == "multipart/form-data")
|
||||
{
|
||||
multipart_ = true;
|
||||
string_view boundary;
|
||||
if (ParseValue(other, "boundary", &boundary))
|
||||
{
|
||||
additional_ = ToString(boundary);
|
||||
LOG_INFO("Content-type multipart boundary: %s", additional_.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERRO("Invalid 'multipart/form-data' content-type (no boundary)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string_view charset;
|
||||
if (ParseValue(other, "charset", &charset))
|
||||
{
|
||||
additional_ = ToString(charset);
|
||||
LOG_INFO("Content-type charset: %s", additional_.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// TODO: Use string_view
|
||||
static inline void Unquote(std::string& str) {
|
||||
boost::trim_if(str, boost::is_any_of("\""));
|
||||
}
|
||||
// TODO: Use string_view
|
||||
static inline void Unquote(std::string& str)
|
||||
{
|
||||
boost::trim_if(str, boost::is_any_of("\""));
|
||||
}
|
||||
|
||||
bool ContentDisposition::Init(string_view str) {
|
||||
std::vector<string_view> parts;
|
||||
Split(str, ';', false, &parts);
|
||||
bool ContentDisposition::Init(string_view str)
|
||||
{
|
||||
std::vector<string_view> parts;
|
||||
Split(str, ';', false, &parts);
|
||||
|
||||
if (parts.empty()) {
|
||||
return false;
|
||||
}
|
||||
if (parts.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parts[0] != "form-data") {
|
||||
return false;
|
||||
}
|
||||
if (parts[0] != "form-data")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string_view key;
|
||||
string_view value;
|
||||
for (std::size_t i = 1; i < parts.size(); ++i) {
|
||||
if (!SplitKV(parts[i], '=', true, &key, &value)) {
|
||||
return false;
|
||||
}
|
||||
string_view key;
|
||||
string_view value;
|
||||
for (std::size_t i = 1; i < parts.size(); ++i)
|
||||
{
|
||||
if (!SplitKV(parts[i], '=', true, &key, &value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (key == "name") {
|
||||
name_ = ToString(value);
|
||||
Unquote(name_);
|
||||
} else if (key == "filename") {
|
||||
file_name_ = ToString(value);
|
||||
Unquote(file_name_);
|
||||
}
|
||||
}
|
||||
if (key == "name")
|
||||
{
|
||||
name_ = ToString(value);
|
||||
Unquote(name_);
|
||||
}
|
||||
else if (key == "filename")
|
||||
{
|
||||
file_name_ = ToString(value);
|
||||
Unquote(file_name_);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
FormPartPtr FormPart::New(string_view name, std::string&& data,
|
||||
string_view media_type) {
|
||||
auto form_part = std::make_shared<FormPart>();
|
||||
FormPartPtr FormPart::New(string_view name, std::string&& data,
|
||||
string_view media_type)
|
||||
{
|
||||
auto form_part = std::make_shared<FormPart>();
|
||||
|
||||
form_part->name_ = ToString(name);
|
||||
form_part->data_ = std::move(data);
|
||||
form_part->media_type_ = ToString(media_type);
|
||||
form_part->name_ = ToString(name);
|
||||
form_part->data_ = std::move(data);
|
||||
form_part->media_type_ = ToString(media_type);
|
||||
|
||||
return form_part;
|
||||
}
|
||||
return form_part;
|
||||
}
|
||||
|
||||
FormPartPtr FormPart::NewFile(string_view name, const fs::path& path,
|
||||
string_view media_type) {
|
||||
auto form_part = std::make_shared<FormPart>();
|
||||
FormPartPtr FormPart::NewFile(string_view name, const fs::path& path,
|
||||
string_view media_type)
|
||||
{
|
||||
auto form_part = std::make_shared<FormPart>();
|
||||
|
||||
form_part->name_ = ToString(name);
|
||||
form_part->path_ = path;
|
||||
form_part->media_type_ = ToString(media_type);
|
||||
form_part->name_ = ToString(name);
|
||||
form_part->path_ = path;
|
||||
form_part->media_type_ = ToString(media_type);
|
||||
|
||||
// Determine file name from file path.
|
||||
// TODO: encoding
|
||||
form_part->file_name_ = path.filename().string();
|
||||
// Determine file name from file path.
|
||||
// TODO: encoding
|
||||
form_part->file_name_ = path.filename().string();
|
||||
|
||||
// Determine media type from file extension.
|
||||
// TODO: Default to "application/text"?
|
||||
if (form_part->media_type_.empty()) {
|
||||
auto ext = path.extension().string();
|
||||
form_part->media_type_ = media_types::FromExtension(ext);
|
||||
}
|
||||
// Determine media type from file extension.
|
||||
// TODO: Default to "application/text"?
|
||||
if (form_part->media_type_.empty())
|
||||
{
|
||||
auto ext = path.extension().string();
|
||||
form_part->media_type_ = media_types::FromExtension(ext);
|
||||
}
|
||||
|
||||
return form_part;
|
||||
}
|
||||
return form_part;
|
||||
}
|
||||
|
||||
void FormPart::Prepare(Payload* payload) {
|
||||
using boost::asio::buffer;
|
||||
void FormPart::ReserveData(std::size_t capacity)
|
||||
{
|
||||
data_.reserve(capacity);
|
||||
}
|
||||
|
||||
if (data_.empty() && !path_.empty()) {
|
||||
if (!utility::ReadFile(path_, &data_)) {
|
||||
throw Error{ Error::kFileError, "Cannot read the file" };
|
||||
}
|
||||
}
|
||||
void FormPart::Prepare(Payload* payload)
|
||||
{
|
||||
using boost::asio::buffer;
|
||||
|
||||
// NOTE:
|
||||
// The payload buffers don't own the memory.
|
||||
// It depends on some existing variables/objects to keep the memory.
|
||||
// That's why we need save headers to member variable.
|
||||
if (data_.empty() && !path_.empty())
|
||||
{
|
||||
if (!utility::ReadFile(path_, &data_))
|
||||
{
|
||||
throw Error{ Error::kFileError, "Cannot read the file" };
|
||||
}
|
||||
}
|
||||
|
||||
if (headers_.empty()) {
|
||||
SetHeaders();
|
||||
}
|
||||
// NOTE:
|
||||
// The payload buffers don't own the memory.
|
||||
// It depends on some existing variables/objects to keep the memory.
|
||||
// That's why we need save headers to member variable.
|
||||
|
||||
for (const Header& h : headers_.data()) {
|
||||
payload->push_back(buffer(h.first));
|
||||
payload->push_back(buffer(literal_buffers::HEADER_SEPARATOR));
|
||||
payload->push_back(buffer(h.second));
|
||||
payload->push_back(buffer(literal_buffers::CRLF));
|
||||
}
|
||||
if (headers_.empty())
|
||||
{
|
||||
SetHeaders();
|
||||
}
|
||||
|
||||
payload->push_back(buffer(literal_buffers::CRLF));
|
||||
for (const Header& h : headers_.data())
|
||||
{
|
||||
payload->push_back(buffer(h.first));
|
||||
payload->push_back(buffer(literal_buffers::HEADER_SEPARATOR));
|
||||
payload->push_back(buffer(h.second));
|
||||
payload->push_back(buffer(literal_buffers::CRLF));
|
||||
}
|
||||
|
||||
if (!data_.empty()) {
|
||||
payload->push_back(buffer(data_));
|
||||
}
|
||||
payload->push_back(buffer(literal_buffers::CRLF));
|
||||
|
||||
payload->push_back(buffer(literal_buffers::CRLF));
|
||||
}
|
||||
if (!data_.empty())
|
||||
{
|
||||
payload->push_back(buffer(data_));
|
||||
}
|
||||
|
||||
void FormPart::Free() {
|
||||
data_.clear();
|
||||
data_.shrink_to_fit();
|
||||
}
|
||||
payload->push_back(buffer(literal_buffers::CRLF));
|
||||
}
|
||||
|
||||
std::size_t FormPart::GetSize() {
|
||||
std::size_t size = 0;
|
||||
void FormPart::Free()
|
||||
{
|
||||
data_.clear();
|
||||
data_.shrink_to_fit();
|
||||
}
|
||||
|
||||
if (headers_.empty()) {
|
||||
SetHeaders();
|
||||
}
|
||||
std::size_t FormPart::GetSize()
|
||||
{
|
||||
std::size_t size = 0;
|
||||
|
||||
for (const Header& h : headers_.data()) {
|
||||
size += h.first.size();
|
||||
size += sizeof(literal_buffers::HEADER_SEPARATOR);
|
||||
size += h.second.size();
|
||||
size += sizeof(literal_buffers::CRLF);
|
||||
}
|
||||
size += sizeof(literal_buffers::CRLF);
|
||||
if (headers_.empty())
|
||||
{
|
||||
SetHeaders();
|
||||
}
|
||||
|
||||
size += GetDataSize();
|
||||
for (const Header& h : headers_.data())
|
||||
{
|
||||
size += h.first.size();
|
||||
size += sizeof(literal_buffers::HEADER_SEPARATOR);
|
||||
size += h.second.size();
|
||||
size += sizeof(literal_buffers::CRLF);
|
||||
}
|
||||
size += sizeof(literal_buffers::CRLF);
|
||||
|
||||
size += sizeof(literal_buffers::CRLF);
|
||||
size += GetDataSize();
|
||||
|
||||
return size;
|
||||
}
|
||||
size += sizeof(literal_buffers::CRLF);
|
||||
|
||||
std::size_t FormPart::GetDataSize() {
|
||||
if (!data_.empty()) {
|
||||
return data_.size();
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
auto size = utility::TellSize(path_);
|
||||
if (size == kInvalidLength) {
|
||||
throw Error{ Error::kFileError, "Cannot read the file" };
|
||||
}
|
||||
std::size_t FormPart::GetDataSize()
|
||||
{
|
||||
if (!data_.empty())
|
||||
{
|
||||
return data_.size();
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
auto size = utility::TellSize(path_);
|
||||
if (size == kInvalidLength)
|
||||
{
|
||||
throw Error{ Error::kFileError, "Cannot read the file" };
|
||||
}
|
||||
|
||||
void FormPart::Dump(std::ostream& os, string_view prefix) const {
|
||||
for (auto& h : headers_.data()) {
|
||||
os << prefix << h.first << ": " << h.second << std::endl;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
os << prefix << std::endl;
|
||||
void FormPart::Dump(std::ostream& os, string_view prefix) const
|
||||
{
|
||||
for (auto& h : headers_.data())
|
||||
{
|
||||
os << prefix << h.first << ": " << h.second << std::endl;
|
||||
}
|
||||
|
||||
if (!path_.empty()) {
|
||||
os << prefix << "<file: " << path_.string() << ">" << std::endl;
|
||||
} else {
|
||||
utility::DumpByLine(data_, os, prefix);
|
||||
}
|
||||
}
|
||||
os << prefix << std::endl;
|
||||
|
||||
void FormPart::SetHeaders() {
|
||||
// Header: Content-Disposition
|
||||
if (!path_.empty())
|
||||
{
|
||||
os << prefix << "<file: " << path_.string() << ">" << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
utility::DumpByLine(data_, os, prefix);
|
||||
}
|
||||
}
|
||||
|
||||
std::string content_disposition = "form-data";
|
||||
if (!name_.empty()) {
|
||||
content_disposition.append("; name=\"" + name_ + "\"");
|
||||
}
|
||||
if (!file_name_.empty()) {
|
||||
content_disposition.append("; filename=\"" + file_name_ + "\"");
|
||||
}
|
||||
headers_.Set(headers::kContentDisposition, content_disposition);
|
||||
void FormPart::SetHeaders()
|
||||
{
|
||||
// Header: Content-Disposition
|
||||
|
||||
// Header: Content-Type
|
||||
std::string content_disposition = "form-data";
|
||||
if (!name_.empty())
|
||||
{
|
||||
content_disposition.append("; name=\"" + name_ + "\"");
|
||||
}
|
||||
if (!file_name_.empty())
|
||||
{
|
||||
content_disposition.append("; filename=\"" + file_name_ + "\"");
|
||||
}
|
||||
headers_.Set(headers::kContentDisposition, content_disposition);
|
||||
|
||||
if (!media_type_.empty()) {
|
||||
headers_.Set(headers::kContentType, media_type_);
|
||||
}
|
||||
}
|
||||
// Header: Content-Type
|
||||
|
||||
if (!media_type_.empty())
|
||||
{
|
||||
headers_.Set(headers::kContentType, media_type_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
+214
-178
@@ -9,241 +9,277 @@
|
||||
#include "webcc/fs.h"
|
||||
#include "webcc/globals.h"
|
||||
|
||||
namespace webcc {
|
||||
namespace webcc
|
||||
{
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Header = std::pair<std::string, std::string>;
|
||||
using Header = std::pair<std::string, std::string>;
|
||||
|
||||
class Headers {
|
||||
public:
|
||||
std::size_t size() const {
|
||||
return headers_.size();
|
||||
}
|
||||
class Headers
|
||||
{
|
||||
//fix core dumped
|
||||
public:
|
||||
Headers() : headers_()
|
||||
{
|
||||
|
||||
bool empty() const {
|
||||
return headers_.empty();
|
||||
}
|
||||
}
|
||||
public:
|
||||
std::size_t size() const
|
||||
{
|
||||
return headers_.size();
|
||||
}
|
||||
|
||||
const std::vector<Header>& data() const {
|
||||
return headers_;
|
||||
}
|
||||
bool empty() const
|
||||
{
|
||||
return headers_.empty();
|
||||
}
|
||||
|
||||
bool Set(string_view key, string_view value);
|
||||
const std::vector<Header>& data() const
|
||||
{
|
||||
return headers_;
|
||||
}
|
||||
|
||||
bool Has(string_view key) const;
|
||||
bool Set(string_view key, string_view value);
|
||||
|
||||
// Get header by index.
|
||||
const Header& Get(std::size_t index) const {
|
||||
assert(index < size());
|
||||
return headers_[index];
|
||||
}
|
||||
bool Has(string_view key) const;
|
||||
|
||||
// Get header value by key.
|
||||
// If there's no such header with the given key, besides return empty, the
|
||||
// optional |existed| parameter will be set to false.
|
||||
const std::string& Get(string_view key, bool* existed = nullptr) const;
|
||||
// Get header by index.
|
||||
const Header& Get(std::size_t index) const
|
||||
{
|
||||
assert(index < size());
|
||||
return headers_[index];
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
headers_.clear();
|
||||
}
|
||||
// Get header value by key.
|
||||
// If there's no such header with the given key, besides return empty, the
|
||||
// optional |existed| parameter will be set to false.
|
||||
const std::string& Get(string_view key, bool* existed = nullptr) const;
|
||||
|
||||
private:
|
||||
std::vector<Header>::iterator Find(string_view key);
|
||||
void Clear()
|
||||
{
|
||||
headers_.clear();
|
||||
}
|
||||
|
||||
std::vector<Header> headers_;
|
||||
};
|
||||
private:
|
||||
std::vector<Header>::iterator Find(string_view key);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
std::vector<Header> headers_;
|
||||
};
|
||||
|
||||
// Content-Type header.
|
||||
// Syntax:
|
||||
// Content-Type: text/html; charset=utf-8
|
||||
// Content-Type: multipart/form-data; boundary=something
|
||||
class ContentType {
|
||||
public:
|
||||
explicit ContentType(string_view str = "");
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
void Parse(string_view str);
|
||||
// Content-Type header.
|
||||
// Syntax:
|
||||
// Content-Type: text/html; charset=utf-8
|
||||
// Content-Type: multipart/form-data; boundary=something
|
||||
class ContentType
|
||||
{
|
||||
public:
|
||||
explicit ContentType(string_view str = "");
|
||||
|
||||
void Reset();
|
||||
void Parse(string_view str);
|
||||
|
||||
bool Valid() const;
|
||||
void Reset();
|
||||
|
||||
bool multipart() const {
|
||||
return multipart_;
|
||||
}
|
||||
bool Valid() const;
|
||||
|
||||
const std::string& media_type() const {
|
||||
return media_type_;
|
||||
}
|
||||
bool multipart() const
|
||||
{
|
||||
return multipart_;
|
||||
}
|
||||
|
||||
const std::string& charset() const {
|
||||
assert(!multipart_);
|
||||
return additional_;
|
||||
}
|
||||
const std::string& media_type() const
|
||||
{
|
||||
return media_type_;
|
||||
}
|
||||
|
||||
const std::string& boundary() const {
|
||||
assert(multipart_);
|
||||
return additional_;
|
||||
}
|
||||
const std::string& charset() const
|
||||
{
|
||||
assert(!multipart_);
|
||||
return additional_;
|
||||
}
|
||||
|
||||
private:
|
||||
void Init(string_view str);
|
||||
const std::string& boundary() const
|
||||
{
|
||||
assert(multipart_);
|
||||
return additional_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string media_type_;
|
||||
std::string additional_;
|
||||
bool multipart_ = false;
|
||||
};
|
||||
private:
|
||||
void Init(string_view str);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
private:
|
||||
std::string media_type_;
|
||||
std::string additional_;
|
||||
bool multipart_ = false;
|
||||
};
|
||||
|
||||
// Content-Disposition header.
|
||||
// Syntax:
|
||||
// Content-Disposition: form-data
|
||||
// Content-Disposition: form-data; name="fieldName"
|
||||
// Content-Disposition: form-data; name="fieldName"; filename="filename.jpg"
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
|
||||
class ContentDisposition {
|
||||
public:
|
||||
explicit ContentDisposition(string_view str) {
|
||||
valid_ = Init(str);
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
bool valid() const {
|
||||
return valid_;
|
||||
}
|
||||
// Content-Disposition header.
|
||||
// Syntax:
|
||||
// Content-Disposition: form-data
|
||||
// Content-Disposition: form-data; name="fieldName"
|
||||
// Content-Disposition: form-data; name="fieldName"; filename="filename.jpg"
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
|
||||
class ContentDisposition
|
||||
{
|
||||
public:
|
||||
explicit ContentDisposition(string_view str)
|
||||
{
|
||||
valid_ = Init(str);
|
||||
}
|
||||
|
||||
const std::string& name() const {
|
||||
return name_;
|
||||
}
|
||||
bool valid() const
|
||||
{
|
||||
return valid_;
|
||||
}
|
||||
|
||||
const std::string& file_name() const {
|
||||
return file_name_;
|
||||
}
|
||||
const std::string& name() const
|
||||
{
|
||||
return name_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool Init(string_view str);
|
||||
const std::string& file_name() const
|
||||
{
|
||||
return file_name_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
std::string file_name_;
|
||||
bool valid_ = false;
|
||||
};
|
||||
private:
|
||||
bool Init(string_view str);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
private:
|
||||
std::string name_;
|
||||
std::string file_name_;
|
||||
bool valid_ = false;
|
||||
};
|
||||
|
||||
class FormPart;
|
||||
using FormPartPtr = std::shared_ptr<FormPart>;
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// A part of the multipart form data.
|
||||
class FormPart {
|
||||
public:
|
||||
FormPart() = default;
|
||||
class FormPart;
|
||||
using FormPartPtr = std::shared_ptr<FormPart>;
|
||||
|
||||
FormPart(const FormPart&) = delete;
|
||||
FormPart& operator=(const FormPart&) = delete;
|
||||
// A part of the multipart form data.
|
||||
class FormPart
|
||||
{
|
||||
public:
|
||||
FormPart() = default;
|
||||
|
||||
// Construct a non-file part.
|
||||
// The data will be moved, no file name is needed.
|
||||
// The media type is optional. If the data is a JSON string, you can specify
|
||||
// media type as "application/json".
|
||||
static FormPartPtr New(string_view name, std::string&& data,
|
||||
string_view media_type = "");
|
||||
FormPart(const FormPart&) = delete;
|
||||
FormPart& operator=(const FormPart&) = delete;
|
||||
|
||||
// Construct a file part.
|
||||
// The file name will be extracted from path.
|
||||
// The media type, if not provided, will be inferred from file extension.
|
||||
static FormPartPtr NewFile(string_view name, const fs::path& path,
|
||||
string_view media_type = "");
|
||||
// Construct a non-file part.
|
||||
// The data will be moved, no file name is needed.
|
||||
// The media type is optional. If the data is a JSON string, you can specify
|
||||
// media type as "application/json".
|
||||
static FormPartPtr New(string_view name, std::string&& data,
|
||||
string_view media_type = "");
|
||||
|
||||
// API: SERVER
|
||||
const std::string& name() const {
|
||||
return name_;
|
||||
}
|
||||
// Construct a file part.
|
||||
// The file name will be extracted from path.
|
||||
// The media type, if not provided, will be inferred from file extension.
|
||||
static FormPartPtr NewFile(string_view name, const fs::path& path,
|
||||
string_view media_type = "");
|
||||
|
||||
// API: SERVER/PARSER
|
||||
void set_name(const std::string& name) {
|
||||
name_ = name;
|
||||
}
|
||||
void ReserveData(std::size_t capacity);
|
||||
|
||||
// API: SERVER
|
||||
const std::string& file_name() const {
|
||||
return file_name_;
|
||||
}
|
||||
// API: SERVER
|
||||
const std::string& name() const
|
||||
{
|
||||
return name_;
|
||||
}
|
||||
|
||||
// API: SERVER/PARSER
|
||||
void set_file_name(const std::string& file_name) {
|
||||
file_name_ = file_name;
|
||||
}
|
||||
// API: SERVER/PARSER
|
||||
void set_name(const std::string& name)
|
||||
{
|
||||
name_ = name;
|
||||
}
|
||||
|
||||
// API: SERVER
|
||||
const std::string& media_type() const {
|
||||
return media_type_;
|
||||
}
|
||||
// API: SERVER
|
||||
const std::string& file_name() const
|
||||
{
|
||||
return file_name_;
|
||||
}
|
||||
|
||||
// API: SERVER
|
||||
const std::string& data() const {
|
||||
return data_;
|
||||
}
|
||||
// API: SERVER/PARSER
|
||||
void set_file_name(const std::string& file_name)
|
||||
{
|
||||
file_name_ = file_name;
|
||||
}
|
||||
|
||||
// API: SERVER/PARSER
|
||||
void AppendData(const std::string& data) {
|
||||
data_.append(data);
|
||||
}
|
||||
// API: SERVER
|
||||
const std::string& media_type() const
|
||||
{
|
||||
return media_type_;
|
||||
}
|
||||
|
||||
// API: SERVER/PARSER
|
||||
void AppendData(const char* data, std::size_t count) {
|
||||
data_.append(data, count);
|
||||
}
|
||||
// API: SERVER
|
||||
const std::string& data() const
|
||||
{
|
||||
return data_;
|
||||
}
|
||||
|
||||
// API: CLIENT
|
||||
void Prepare(Payload* payload);
|
||||
// API: SERVER/PARSER
|
||||
void AppendData(const std::string& data)
|
||||
{
|
||||
data_.append(data);
|
||||
}
|
||||
|
||||
// Free the memory of the data.
|
||||
void Free();
|
||||
// API: SERVER/PARSER
|
||||
void AppendData(const char* data, std::size_t count)
|
||||
{
|
||||
data_.append(data, count);
|
||||
}
|
||||
|
||||
// Get the size of the whole payload.
|
||||
// Used by the request to calculate content length.
|
||||
std::size_t GetSize();
|
||||
// API: CLIENT
|
||||
void Prepare(Payload* payload);
|
||||
|
||||
// Get the size of the data.
|
||||
std::size_t GetDataSize();
|
||||
// Free the memory of the data.
|
||||
void Free();
|
||||
|
||||
// Dump to output stream for logging purpose.
|
||||
void Dump(std::ostream& os, string_view prefix) const;
|
||||
// Get the size of the whole payload.
|
||||
// Used by the request to calculate content length.
|
||||
std::size_t GetSize();
|
||||
|
||||
private:
|
||||
// Generate headers from properties.
|
||||
void SetHeaders();
|
||||
// Get the size of the data.
|
||||
std::size_t GetDataSize();
|
||||
|
||||
private:
|
||||
// The <input> name within the original HTML form.
|
||||
// E.g., given HTML form:
|
||||
// <input name="file1" type="file">
|
||||
// the name will be "file1".
|
||||
std::string name_;
|
||||
// Dump to output stream for logging purpose.
|
||||
void Dump(std::ostream& os, string_view prefix) const;
|
||||
|
||||
// The path of the file to post.
|
||||
fs::path path_;
|
||||
private:
|
||||
// Generate headers from properties.
|
||||
void SetHeaders();
|
||||
|
||||
// The original local file name.
|
||||
// E.g., "baby.jpg".
|
||||
std::string file_name_;
|
||||
private:
|
||||
// The <input> name within the original HTML form.
|
||||
// E.g., given HTML form:
|
||||
// <input name="file1" type="file">
|
||||
// the name will be "file1".
|
||||
std::string name_;
|
||||
|
||||
// The content-type if the media type is known (e.g., inferred from the file
|
||||
// extension or operating system typing information) or as
|
||||
// application/octet-stream.
|
||||
// E.g., "image/jpeg".
|
||||
std::string media_type_;
|
||||
// The path of the file to post.
|
||||
fs::path path_;
|
||||
|
||||
// Headers generated from the above properties.
|
||||
// Only Used to prepare payload.
|
||||
Headers headers_;
|
||||
// The original local file name.
|
||||
// E.g., "baby.jpg".
|
||||
std::string file_name_;
|
||||
|
||||
std::string data_;
|
||||
};
|
||||
// The content-type if the media type is known (e.g., inferred from the file
|
||||
// extension or operating system typing information) or as
|
||||
// application/octet-stream.
|
||||
// E.g., "image/jpeg".
|
||||
std::string media_type_;
|
||||
|
||||
// Headers generated from the above properties.
|
||||
// Only Used to prepare payload.
|
||||
Headers headers_;
|
||||
|
||||
std::string data_;
|
||||
};
|
||||
|
||||
inline std::size_t g_max_multipart_size = 1024 * 1024 * 1024;
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
|
||||
+228
-165
@@ -1,4 +1,4 @@
|
||||
#include "webcc/connection.h"
|
||||
#include "webcc/connection.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
@@ -10,225 +10,288 @@
|
||||
|
||||
using boost::asio::ip::tcp;
|
||||
|
||||
namespace webcc {
|
||||
namespace webcc
|
||||
{
|
||||
|
||||
Connection::Connection(tcp::socket socket, ConnectionPool* pool,
|
||||
Queue<ConnectionPtr>* queue, ViewMatcher&& view_matcher,
|
||||
std::size_t buffer_size)
|
||||
: socket_(std::move(socket)), pool_(pool), queue_(queue),
|
||||
view_matcher_(std::move(view_matcher)), buffer_(buffer_size) {
|
||||
}
|
||||
Connection::Connection(tcp::socket socket, ConnectionPool* pool,
|
||||
Queue<ConnectionPtr>* queue, ViewMatcher&& view_matcher,
|
||||
std::size_t buffer_size)
|
||||
: socket_(std::move(socket)), pool_(pool), queue_(queue),
|
||||
view_matcher_(std::move(view_matcher)), buffer_(buffer_size)
|
||||
{
|
||||
}
|
||||
|
||||
void Connection::Start() {
|
||||
request_.reset(new Request{});
|
||||
void Connection::Start()
|
||||
{
|
||||
request_.reset(new Request{});
|
||||
|
||||
boost::system::error_code ec;
|
||||
auto endpoint = socket_.remote_endpoint(ec);
|
||||
if (!ec) {
|
||||
request_->set_address(endpoint.address().to_string());
|
||||
}
|
||||
boost::system::error_code ec;
|
||||
auto endpoint = socket_.remote_endpoint(ec);
|
||||
if (!ec)
|
||||
{
|
||||
request_->set_address(endpoint.address().to_string());
|
||||
}
|
||||
|
||||
request_parser_.Init(request_.get(), view_matcher_);
|
||||
request_parser_.Init(request_.get(), view_matcher_);
|
||||
|
||||
AsyncRead();
|
||||
}
|
||||
AsyncRead();
|
||||
}
|
||||
|
||||
void Connection::Close() {
|
||||
LOG_INFO("Shutdown socket");
|
||||
void Connection::Close()
|
||||
{
|
||||
LOG_INFO("Shutdown socket");
|
||||
|
||||
// Initiate graceful connection closure.
|
||||
// Socket close VS. shutdown:
|
||||
// https://stackoverflow.com/questions/4160347/close-vs-shutdown-socket
|
||||
boost::system::error_code ec;
|
||||
socket_.shutdown(tcp::socket::shutdown_both, ec);
|
||||
// Initiate graceful connection closure.
|
||||
// Socket close VS. shutdown:
|
||||
// https://stackoverflow.com/questions/4160347/close-vs-shutdown-socket
|
||||
boost::system::error_code ec;
|
||||
socket_.shutdown(tcp::socket::shutdown_both, ec);
|
||||
|
||||
if (ec) {
|
||||
LOG_WARN("Socket shutdown error (%s)", ec.message().c_str());
|
||||
ec.clear();
|
||||
// Don't return, try to close the socket anywhere.
|
||||
}
|
||||
if (ec)
|
||||
{
|
||||
LOG_WARN("Socket shutdown error (%s)", ec.message().c_str());
|
||||
ec.clear();
|
||||
// Don't return, try to close the socket anywhere.
|
||||
}
|
||||
|
||||
LOG_INFO("Close socket");
|
||||
LOG_INFO("Close socket");
|
||||
|
||||
socket_.close(ec);
|
||||
socket_.close(ec);
|
||||
|
||||
if (ec) {
|
||||
LOG_ERRO("Socket close error (%s)", ec.message().c_str());
|
||||
}
|
||||
}
|
||||
if (ec)
|
||||
{
|
||||
LOG_ERRO("Socket close error (%s)", ec.message().c_str());
|
||||
}
|
||||
|
||||
void Connection::SendResponse(ResponsePtr response, bool no_keep_alive) {
|
||||
assert(response);
|
||||
header_validated_ = false;
|
||||
}
|
||||
|
||||
response_ = response;
|
||||
void Connection::SendResponse(ResponsePtr response, bool no_keep_alive)
|
||||
{
|
||||
assert(response);
|
||||
|
||||
if (!no_keep_alive && request_->IsConnectionKeepAlive()) {
|
||||
response_->SetHeader(headers::kConnection, "Keep-Alive");
|
||||
} else {
|
||||
response_->SetHeader(headers::kConnection, "Close");
|
||||
}
|
||||
response_ = response;
|
||||
|
||||
response_->Prepare();
|
||||
if (!no_keep_alive && request_->IsConnectionKeepAlive())
|
||||
{
|
||||
response_->SetHeader(headers::kConnection, "Keep-Alive");
|
||||
}
|
||||
else
|
||||
{
|
||||
response_->SetHeader(headers::kConnection, "Close");
|
||||
}
|
||||
|
||||
AsyncWrite();
|
||||
}
|
||||
response_->Prepare();
|
||||
|
||||
void Connection::SendResponse(Status status, bool no_keep_alive) {
|
||||
auto response = std::make_shared<Response>(status);
|
||||
header_validated_ = false;
|
||||
|
||||
// According to the testing based on HTTPie (and Chrome), the `Content-Length`
|
||||
// header is expected for a response with status like 404 even when the body
|
||||
// is empty.
|
||||
response->SetBody(std::make_shared<Body>(), true);
|
||||
AsyncWrite();
|
||||
}
|
||||
|
||||
SendResponse(response, no_keep_alive);
|
||||
}
|
||||
void Connection::SendResponse(Status status, bool no_keep_alive)
|
||||
{
|
||||
auto response = std::make_shared<Response>(status);
|
||||
|
||||
void Connection::SendResponse(Status status, std::string server_name,
|
||||
bool no_keep_alive) {
|
||||
auto response = std::make_shared<Response>(status);
|
||||
// According to the testing based on HTTPie (and Chrome), the `Content-Length`
|
||||
// header is expected for a response with status like 404 even when the body
|
||||
// is empty.
|
||||
response->SetBody(std::make_shared<Body>(), true);
|
||||
|
||||
// According to the testing based on HTTPie (and Chrome), the `Content-Length`
|
||||
// header is expected for a response with status like 404 even when the body
|
||||
// is empty.
|
||||
response->SetBody(std::make_shared<Body>(), true);
|
||||
response->SetHeader(headers::kServer, server_name);
|
||||
SendResponse(response, no_keep_alive);
|
||||
}
|
||||
|
||||
SendResponse(response, no_keep_alive);
|
||||
}
|
||||
void Connection::SendResponse(Status status, std::string server_name,
|
||||
bool no_keep_alive)
|
||||
{
|
||||
auto response = std::make_shared<Response>(status);
|
||||
|
||||
void Connection::AsyncRead() {
|
||||
// According to the testing based on HTTPie (and Chrome), the `Content-Length`
|
||||
// header is expected for a response with status like 404 even when the body
|
||||
// is empty.
|
||||
response->SetBody(std::make_shared<Body>(), true);
|
||||
response->SetHeader(headers::kServer, server_name);
|
||||
|
||||
SendResponse(response, no_keep_alive);
|
||||
}
|
||||
|
||||
void Connection::AsyncRead()
|
||||
{
|
||||
#if WEBCC_STUDY_SERVER_THREADING
|
||||
LOG_USER("[%u] AsyncRead()", (unsigned int)this);
|
||||
LOG_USER("[%u] AsyncRead()", (unsigned int)this);
|
||||
#endif
|
||||
|
||||
socket_.async_read_some(boost::asio::buffer(buffer_),
|
||||
std::bind(&Connection::OnRead, shared_from_this(),
|
||||
std::placeholders::_1,
|
||||
std::placeholders::_2));
|
||||
}
|
||||
socket_.async_read_some(boost::asio::buffer(buffer_),
|
||||
std::bind(&Connection::OnRead, shared_from_this(),
|
||||
std::placeholders::_1,
|
||||
std::placeholders::_2));
|
||||
}
|
||||
|
||||
void Connection::OnRead(boost::system::error_code ec, std::size_t length) {
|
||||
void Connection::OnRead(boost::system::error_code ec, std::size_t length)
|
||||
{
|
||||
#if WEBCC_STUDY_SERVER_THREADING
|
||||
LOG_USER("[%u] OnRead()", (unsigned int)this);
|
||||
LOG_USER("[%u] OnRead()", (unsigned int)this);
|
||||
#endif
|
||||
|
||||
if (ec) {
|
||||
if (ec == boost::asio::error::eof) {
|
||||
LOG_INFO("Socket read EOF (%s)", ec.message().c_str());
|
||||
} else if (ec == boost::asio::error::operation_aborted) {
|
||||
// The socket of this connection has been closed.
|
||||
// This happens, e.g., when the server was stopped by a signal (Ctrl-C).
|
||||
LOG_WARN("Socket operation aborted (%s)", ec.message().c_str());
|
||||
} else {
|
||||
LOG_ERRO("Socket read error (%s)", ec.message().c_str());
|
||||
}
|
||||
if (ec)
|
||||
{
|
||||
if (ec == boost::asio::error::eof)
|
||||
{
|
||||
LOG_INFO("Socket read EOF (%s)", ec.message().c_str());
|
||||
}
|
||||
else if (ec == boost::asio::error::operation_aborted)
|
||||
{
|
||||
// The socket of this connection has been closed.
|
||||
// This happens, e.g., when the server was stopped by a signal (Ctrl-C).
|
||||
LOG_WARN("Socket operation aborted (%s)", ec.message().c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERRO("Socket read error (%s)", ec.message().c_str());
|
||||
}
|
||||
|
||||
// Don't try to send any response back.
|
||||
// Don't try to send any response back.
|
||||
|
||||
if (ec != boost::asio::error::operation_aborted) {
|
||||
pool_->Close(shared_from_this());
|
||||
} // else: The socket of this connection has already been closed.
|
||||
if (ec != boost::asio::error::operation_aborted)
|
||||
{
|
||||
pool_->Close(shared_from_this());
|
||||
} // else: The socket of this connection has already been closed.
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!request_parser_.Parse(buffer_.data(), length)) {
|
||||
LOG_ERRO("Failed to parse request");
|
||||
// Send Bad Request (400) to the client and no Keep-Alive.
|
||||
SendResponse(Status::kBadRequest, true);
|
||||
// Close the socket connection.
|
||||
pool_->Close(shared_from_this());
|
||||
return;
|
||||
}
|
||||
if (!request_parser_.Parse(buffer_.data(), length))
|
||||
{
|
||||
LOG_ERRO("Failed to parse request");
|
||||
// Send Bad Request (400) to the client and no Keep-Alive.
|
||||
SendResponse(Status::kBadRequest, true);
|
||||
// Close the socket connection.
|
||||
pool_->Close(shared_from_this());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!request_parser_.finished()) {
|
||||
// Continue to read the request.
|
||||
AsyncRead();
|
||||
return;
|
||||
}
|
||||
// =================== 【最小改动:仅插入这 8 行】 ===================
|
||||
if (request_parser_.IsHeaderParsed() && !header_validated_)
|
||||
{
|
||||
header_validated_ = true;
|
||||
if (auto view = request_parser_.MatchedView())
|
||||
{
|
||||
if (!view->ValidateHeader(request_))
|
||||
{
|
||||
LOG_ERRO("Header validation failed, closing connection.");
|
||||
pool_->Close(shared_from_this());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// =================================================================
|
||||
|
||||
LOG_VERB("Request:\n%s", request_->Dump().c_str());
|
||||
if (!request_parser_.finished())
|
||||
{
|
||||
// Continue to read the request.
|
||||
AsyncRead();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enqueue this connection once the request has been read.
|
||||
// Some worker thread will handle the request later.
|
||||
queue_->Push(shared_from_this());
|
||||
}
|
||||
LOG_VERB("Request:\n%s", request_->Dump().c_str());
|
||||
|
||||
void Connection::AsyncWrite() {
|
||||
// Enqueue this connection once the request has been read.
|
||||
// Some worker thread will handle the request later.
|
||||
queue_->Push(shared_from_this());
|
||||
}
|
||||
|
||||
void Connection::AsyncWrite()
|
||||
{
|
||||
#if WEBCC_STUDY_SERVER_THREADING
|
||||
LOG_USER("[%u] AsyncWrite()", (unsigned int)this);
|
||||
LOG_USER("[%u] AsyncWrite()", (unsigned int)this);
|
||||
#endif
|
||||
|
||||
LOG_VERB("Response:\n%s", response_->Dump().c_str());
|
||||
LOG_VERB("Response:\n%s", response_->Dump().c_str());
|
||||
|
||||
// Firstly, write the headers.
|
||||
boost::asio::async_write(socket_, response_->GetPayload(),
|
||||
std::bind(&Connection::OnWriteHeaders,
|
||||
shared_from_this(), std::placeholders::_1,
|
||||
std::placeholders::_2));
|
||||
}
|
||||
// Firstly, write the headers.
|
||||
boost::asio::async_write(socket_, response_->GetPayload(),
|
||||
std::bind(&Connection::OnWriteHeaders,
|
||||
shared_from_this(), std::placeholders::_1,
|
||||
std::placeholders::_2));
|
||||
}
|
||||
|
||||
void Connection::OnWriteHeaders(boost::system::error_code ec,
|
||||
std::size_t length) {
|
||||
void Connection::OnWriteHeaders(boost::system::error_code ec,
|
||||
std::size_t length)
|
||||
{
|
||||
#if WEBCC_STUDY_SERVER_THREADING
|
||||
LOG_USER("[%u] OnWriteHeaders()", (unsigned int)this);
|
||||
LOG_USER("[%u] OnWriteHeaders()", (unsigned int)this);
|
||||
#endif
|
||||
|
||||
if (ec) {
|
||||
HandleWriteError(ec);
|
||||
} else {
|
||||
// Write the body payload by payload.
|
||||
response_->body()->InitPayload();
|
||||
AsyncWriteBody();
|
||||
}
|
||||
}
|
||||
if (ec)
|
||||
{
|
||||
HandleWriteError(ec);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write the body payload by payload.
|
||||
response_->body()->InitPayload();
|
||||
AsyncWriteBody();
|
||||
}
|
||||
}
|
||||
|
||||
void Connection::AsyncWriteBody() {
|
||||
auto payload = response_->body()->NextPayload();
|
||||
void Connection::AsyncWriteBody()
|
||||
{
|
||||
auto payload = response_->body()->NextPayload();
|
||||
|
||||
if (!payload.empty()) {
|
||||
boost::asio::async_write(socket_, payload,
|
||||
std::bind(&Connection::OnWriteBody,
|
||||
shared_from_this(),
|
||||
std::placeholders::_1,
|
||||
std::placeholders::_2));
|
||||
} else {
|
||||
// No more body payload left, we're done.
|
||||
HandleWriteOK();
|
||||
}
|
||||
}
|
||||
if (!payload.empty())
|
||||
{
|
||||
boost::asio::async_write(socket_, payload,
|
||||
std::bind(&Connection::OnWriteBody,
|
||||
shared_from_this(),
|
||||
std::placeholders::_1,
|
||||
std::placeholders::_2));
|
||||
}
|
||||
else
|
||||
{
|
||||
// No more body payload left, we're done.
|
||||
HandleWriteOK();
|
||||
}
|
||||
}
|
||||
|
||||
void Connection::OnWriteBody(boost::system::error_code ec, std::size_t length) {
|
||||
void Connection::OnWriteBody(boost::system::error_code ec, std::size_t length)
|
||||
{
|
||||
#if WEBCC_STUDY_SERVER_THREADING
|
||||
LOG_USER("[%u] OnWriteBody()", (unsigned int)this);
|
||||
LOG_USER("[%u] OnWriteBody()", (unsigned int)this);
|
||||
#endif
|
||||
|
||||
if (ec) {
|
||||
HandleWriteError(ec);
|
||||
} else {
|
||||
AsyncWriteBody();
|
||||
}
|
||||
}
|
||||
if (ec)
|
||||
{
|
||||
HandleWriteError(ec);
|
||||
}
|
||||
else
|
||||
{
|
||||
AsyncWriteBody();
|
||||
}
|
||||
}
|
||||
|
||||
void Connection::HandleWriteOK() {
|
||||
LOG_INFO("Response has been sent back");
|
||||
void Connection::HandleWriteOK()
|
||||
{
|
||||
LOG_INFO("Response has been sent back");
|
||||
|
||||
if (request_->IsConnectionKeepAlive()) {
|
||||
LOG_INFO("The client asked for a keep-alive connection");
|
||||
LOG_INFO("Continue to read the next request");
|
||||
Start();
|
||||
} else {
|
||||
pool_->Close(shared_from_this());
|
||||
}
|
||||
}
|
||||
if (request_->IsConnectionKeepAlive())
|
||||
{
|
||||
LOG_INFO("The client asked for a keep-alive connection");
|
||||
LOG_INFO("Continue to read the next request");
|
||||
Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
pool_->Close(shared_from_this());
|
||||
}
|
||||
}
|
||||
|
||||
void Connection::HandleWriteError(boost::system::error_code ec) {
|
||||
LOG_ERRO("Socket write error (%s)", ec.message().c_str());
|
||||
void Connection::HandleWriteError(boost::system::error_code ec)
|
||||
{
|
||||
LOG_ERRO("Socket write error (%s)", ec.message().c_str());
|
||||
|
||||
if (ec != boost::asio::error::operation_aborted) {
|
||||
pool_->Close(shared_from_this());
|
||||
}
|
||||
}
|
||||
if (ec != boost::asio::error::operation_aborted)
|
||||
{
|
||||
pool_->Close(shared_from_this());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
#ifndef WEBCC_CONNECTION_H_
|
||||
#ifndef WEBCC_CONNECTION_H_
|
||||
#define WEBCC_CONNECTION_H_
|
||||
|
||||
#include <memory>
|
||||
@@ -101,6 +101,9 @@ private:
|
||||
|
||||
// The response to be sent back to the client.
|
||||
ResponsePtr response_;
|
||||
|
||||
// 标识是否检查过请求头
|
||||
bool header_validated_ = false;
|
||||
};
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
+123
-3
@@ -1,4 +1,4 @@
|
||||
#ifndef WEBCC_GLOBALS_H_
|
||||
#ifndef WEBCC_GLOBALS_H_
|
||||
#define WEBCC_GLOBALS_H_
|
||||
|
||||
#include <cassert>
|
||||
@@ -108,49 +108,169 @@ const char* const kPatch = "PATCH";
|
||||
// The full list is available here:
|
||||
// https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
|
||||
enum Status {
|
||||
// 此临时响应表明客户端应继续请求,或者如果请求已完成,则忽略此响应。
|
||||
kContinue = 100,
|
||||
// 此代码是在响应客户端的 Upgrade 请求标头时发送的,用于指示服务器即将切换到的协议。
|
||||
kSwitchingProtocols = 101,
|
||||
// 此代码曾在 WebDAV 上下文中使用,表示服务器已收到请求,但在响应时无法提供状态。
|
||||
kProcessing = 102,
|
||||
// 此状态码主要与 Link 标头一起使用,允许用户代理在服务器准备响应时开始预加载资源,或预连接到页面需要资源的源站。
|
||||
kEarlyHints = 103,
|
||||
|
||||
// 请求成功。
|
||||
kOK = 200,
|
||||
// 请求成功,并因此创建了一个新资源。
|
||||
kCreated = 201,
|
||||
// 请求已被接收但尚未处理。
|
||||
kAccepted = 202,
|
||||
// 此响应代码表示返回的元数据与原始服务器上可用的不完全相同,而是从本地或第三方副本收集的。这主要用于另一个资源的镜像或备份。
|
||||
kNonAuthoritativeInformation = 203,
|
||||
// 对于此请求,没有内容可发送,但响应头可能有用。
|
||||
kNoContent = 204,
|
||||
// 告知用户代理重置发送此请求的文档。
|
||||
kResetContent = 205,
|
||||
// 当客户端请求了资源的一部分时,使用此响应代码进行响应。
|
||||
kPartialContent = 206,
|
||||
// 在可能需要多个状态码的情况下,传递关于多个资源的信息。
|
||||
kMultiStatus = 207,
|
||||
// 在 <dav:propstat> 响应元素内部使用,以避免重复枚举同一集合的多个绑定的内部成员。
|
||||
kAlreadyReported = 208,
|
||||
// 服务器已完成了对资源的 GET 请求,并且响应是对当前实例应用了一个或多个实例操作后的结果表示。
|
||||
kIMUsed = 226,
|
||||
|
||||
// 在代理驱动(agent-driven)的内容协商中,请求有多个可能的响应,用户代理或用户应选择其中之一。
|
||||
kMultipleChoices = 300,
|
||||
// 请求资源的 URL 已永久更改。新 URL 在响应中给出。
|
||||
kMovedPermanently = 301,
|
||||
// 此响应代码意味着请求资源的 URI 已暂时更改。未来可能还会对 URI 进行进一步更改,因此客户端在未来的请求中应使用相同的 URI。
|
||||
kFound = 302,
|
||||
// 服务器发送此响应以指示客户端使用 GET 请求在另一个 URI 获取请求的资源。
|
||||
kSeeOther = 303,
|
||||
// 用于缓存目的。它告知客户端响应未被修改,因此客户端可以继续使用相同的缓存响应版本。
|
||||
kNotModified = 304,
|
||||
// 在 HTTP 规范的前一版本中定义,表示请求的响应必须通过代理访问。由于涉及代理带内配置的安全问题,此状态码已被弃用。
|
||||
kUseProxy = 305,
|
||||
// 此响应代码不再使用,但被保留。它曾在 HTTP/1.1 规范的先前版本中使用。
|
||||
k__Unused = 306,
|
||||
// 服务器发送此响应以指示客户端使用与先前请求相同的方法在另一个 URI 获取请求的资源。其语义与 302 Found 响应代码相同,但用户代理不得更改使用的 HTTP 方法:如果在第一个请求中使用了 POST,则在重定向请求中也必须使用 POST。
|
||||
kTemporaryRedirect = 307,
|
||||
// 表示资源现在永久位于另一个 URI,由 Location 响应头指定。其语义与 301 Moved Permanently HTTP 响应代码相同,但用户代理不得更改使用的 HTTP 方法:如果在第一个请求中使用了 POST,则在第二个请求中也必须使用 POST。
|
||||
kPermanentRedirect = 308,
|
||||
|
||||
// 由于被认为是客户端错误的原因(例如,格式错误的请求语法、无效的请求消息结构或欺骗性的请求路由),服务器无法或不会处理该请求。
|
||||
kBadRequest = 400,
|
||||
// 尽管 HTTP 标准指定为 "unauthorized",但从语义上讲,此响应的意思是 "unauthenticated"。即,客户端必须进行身份验证才能获得请求的响应。
|
||||
kUnauthorized = 401,
|
||||
// 此代码最初用于数字支付系统,但此状态码很少使用,且不存在标准约定。
|
||||
kPaymentRequired = 402,
|
||||
// 客户端没有访问内容的权利;也就是说,它是未授权的,因此服务器拒绝提供请求的资源。与 401 Unauthorized 不同,服务器知道客户端的身份。
|
||||
kForbidden = 403,
|
||||
// 服务器找不到请求的资源。
|
||||
kNotFound = 404,
|
||||
// 服务器知道请求方法,但目标资源不支持该方法。
|
||||
kMethodNotAllowed = 405,
|
||||
// 当 Web 服务器执行服务器驱动的内容协商后,找不到任何符合用户代理给定条件的内容时,会发送此响应。
|
||||
kNotAcceptable = 406,
|
||||
// 类似于 401 Unauthorized,但需要通过代理进行身份验证。
|
||||
kProxyAuthenticationRequired = 407,
|
||||
// 某些服务器会在空闲连接上发送此响应,即使客户端之前没有任何请求。这意味着服务器希望关闭此未使用的连接。
|
||||
kRequestTimeout = 408,
|
||||
// 当请求与服务器的当前状态冲突时,发送此响应。在
|
||||
kConflict = 409,
|
||||
// 当请求的内容已从服务器永久删除,且没有转发地址时,发送此响应。
|
||||
kGone = 410,
|
||||
kLocked = 423,
|
||||
// 服务器拒绝了请求,因为未定义 Content-Length 标头字段,而服务器需要它。
|
||||
kLengthRequired = 411,
|
||||
// 在条件请求中,客户端在其标头中指明了服务器不满足的前提条件。
|
||||
kPreconditionFailed = 412,
|
||||
// 请求体大于服务器定义的限制。
|
||||
kContentTooLarge = 413,
|
||||
// 客户端请求的 URI 长度超过了服务器愿意解释的长度。
|
||||
kURITooLong = 414,
|
||||
// 服务器不支持请求数据的媒体格式,因此服务器拒绝该请求。
|
||||
kUnsupportedMediaType = 415,
|
||||
// 无法满足请求中 Range 标头字段指定的范围。可能范围超出了目标资源数据的大小。
|
||||
kRangeNotSatisfiable = 416,
|
||||
// 此响应代码表示服务器无法满足 Expect 请求标头字段指示的期望。
|
||||
kExpectationFailed = 417,
|
||||
// 服务器拒绝尝试用茶壶煮咖啡。
|
||||
kIamATeapot = 418,
|
||||
// 请求被发送到了一个无法产生响应的服务器。
|
||||
kMisdirectedRequest = 421,
|
||||
// 请求格式正确,但由于语义错误而无法被遵循。
|
||||
kUnprocessableContent = 422,
|
||||
// 正在访问的资源已被锁定。
|
||||
kLocked = 423,
|
||||
// 由于先前的请求失败,导致当前请求失败。
|
||||
kFailedDependency = 424,
|
||||
// 表示服务器不愿意冒险处理一个可能被重放的请求。
|
||||
kTooEarly = 425,
|
||||
// 服务器拒绝使用当前协议执行请求,但可能在客户端升级到其他协议后愿意执行。服务器在 426 响应中发送 Upgrade 标头以指示所需的协议。
|
||||
kUpgradeRequired = 426,
|
||||
// 原始服务器要求请求是有条件的。此响应旨在防止"丢失更新"问题,即客户端 GET 资源状态,修改后 PUT 回服务器,而同时第三方已修改了服务器上的状态,导致冲突。
|
||||
kPreconditionRequired = 428,
|
||||
// 用户在给定的时间内发送了太多请求(速率限制)。
|
||||
kTooManyRequests = 429,
|
||||
// 服务器因请求头字段太大而不愿意处理该请求。
|
||||
kRequestHeaderFieldsTooLarge = 431,
|
||||
// 用户代理请求了一个无法合法提供的资源,例如被政府审查的网页。
|
||||
kUnavailableForLegalReasons = 451,
|
||||
|
||||
// 服务器遇到了不知道如何处理的情况。此错误是通用性的,表示服务器找不到更合适的 5XX 状态码来响应。
|
||||
kInternalServerError = 500,
|
||||
// 服务器不支持请求方法,无法处理。
|
||||
kNotImplemented = 501,
|
||||
// 此错误响应意味着服务器作为网关或代理时,收到了一个无效的响应。
|
||||
kBadGateway = 502,
|
||||
// 服务器尚未准备好处理请求。
|
||||
kServiceUnavailable = 503,
|
||||
// 当服务器作为网关或代理,无法及时获得响应时,会给出此错误响应。
|
||||
kGatewayTimeout = 504,
|
||||
// 服务器不支持请求中使用的 HTTP 版本。
|
||||
kHTTPVersionNotSupported = 505,
|
||||
// 服务器存在内部配置错误:在内容协商过程中,被选中的变体被配置为自身参与内容协商,这导致在创建响应时出现循环引用。
|
||||
kVariantAlsoNegotiates = 506,
|
||||
// 由于服务器无法存储成功完成请求所需的表示,因此无法对资源执行该方法。
|
||||
kInsufficientStorage = 507,
|
||||
// 服务器在处理请求时检测到无限循环。
|
||||
kLoopDetected = 508,
|
||||
// 客户端请求声明了一个应使用 HTTP 扩展(RFC 2774)来处理请求,但该扩展不受支持。
|
||||
kNotExtended = 510,
|
||||
// 表示客户端需要进行身份验证才能获得网络访问权限。
|
||||
kNetworkAuthenticationRequired = 511,
|
||||
|
||||
//Not Standard Code By UnknownObject
|
||||
|
||||
// 请求载体的格式错误,如:无法解析的JSON等。
|
||||
k_uRequestFormatError = 489,
|
||||
// 请求无效。可能是由于未正确携带数据等必要信息。
|
||||
k_uRequestInvalid = 490,
|
||||
k_uSubProcessFalied = 491,
|
||||
// 请求URL超范围。此响应表示请求的URL是错误的。
|
||||
k_uURLOutOfRange = 492,
|
||||
// 无效的请求主机。指示请求时使用了错误的域名/IP。
|
||||
k_uInvalidRequestHost = 493,
|
||||
// IP地址被封禁。
|
||||
k_uIPBlocked = 494,
|
||||
// 非法上传请求。指示本次上传请求不符合服务器规定。
|
||||
k_uIllegalUpload = 495,
|
||||
// 文件格式错误。指示上传的文件格式不符合服务器规定。
|
||||
k_uFileFormatError = 496,
|
||||
// 无效文件。处理请求所需的文件已过期/无法访问。
|
||||
k_uInvalidFile = 497,
|
||||
// 上传的文件过大。非文件上传时应使用 413 Content Too Large。
|
||||
k_uFileTooLarge = 498,
|
||||
// 每秒请求数过多。仅在一些特殊API中使用,常规情况需使用 429 Too Many Requests。
|
||||
k_uRPSLimited = 499,
|
||||
|
||||
// 子过程失败。服务器在处理请求的某个步骤中遇到无法恢复的错误。
|
||||
k_uSubProcessFalied = 533,
|
||||
// 服务器检测到漏洞利用/可执行文件上传等网络攻击行为。
|
||||
k_uServerHateYou = 540,
|
||||
// 检测到拒绝服务漏洞攻击。
|
||||
k_uDoSFound = 550,
|
||||
// 检测到分布式拒绝服务漏洞攻击。
|
||||
k_uDDoSFound = 551,
|
||||
// 未知的服务器错误。当服务器无法定位错误来源时返回。否则应使用 500 Internal Server Error。
|
||||
k_uUnknownServerError = 560
|
||||
};
|
||||
|
||||
|
||||
+2
-1
@@ -10,7 +10,8 @@
|
||||
|
||||
namespace webcc {
|
||||
|
||||
Message::Message() : body_(new Body{}) {
|
||||
//fix core dumped
|
||||
Message::Message() : headers_(), start_line_(), body_(new Body{}) {
|
||||
}
|
||||
|
||||
void Message::SetBody(BodyPtr body, bool set_length) {
|
||||
|
||||
+288
-209
@@ -1,4 +1,4 @@
|
||||
#include "webcc/request_parser.h"
|
||||
#include "webcc/request_parser.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
@@ -9,266 +9,345 @@
|
||||
#include "webcc/string.h"
|
||||
#include "webcc/utility.h"
|
||||
|
||||
namespace webcc {
|
||||
namespace webcc
|
||||
{
|
||||
|
||||
RequestParser::RequestParser() : request_(nullptr) {
|
||||
}
|
||||
RequestParser::RequestParser() : request_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
void RequestParser::Init(Request* request, ViewMatcher view_matcher) {
|
||||
assert(view_matcher);
|
||||
void RequestParser::Init(Request* request, ViewMatcher view_matcher)
|
||||
{
|
||||
assert(view_matcher);
|
||||
|
||||
Parser::Init(request);
|
||||
Parser::Init(request);
|
||||
|
||||
request_ = request;
|
||||
view_matcher_ = view_matcher;
|
||||
}
|
||||
request_ = request;
|
||||
view_matcher_ = view_matcher;
|
||||
header_parsed_ = false; // 【归位】:在 Init 时重置状态,防止对象复用残留
|
||||
}
|
||||
|
||||
bool RequestParser::OnHeadersEnd() {
|
||||
bool matched = view_matcher_(request_->method(), request_->url().path(),
|
||||
&stream_);
|
||||
bool RequestParser::OnHeadersEnd()
|
||||
{
|
||||
header_parsed_ = true; // 【核心位置】:请求头解析完毕,触发标记!
|
||||
|
||||
if (!matched) {
|
||||
LOG_WARN("No view matches the request: %s %s", request_->method().c_str(),
|
||||
request_->url().path().c_str());
|
||||
}
|
||||
bool matched = view_matcher_(request_->method(), request_->url().path(), &stream_, &matched_view_);
|
||||
|
||||
return matched;
|
||||
}
|
||||
if (!matched)
|
||||
{
|
||||
LOG_WARN("No view matches the request: %s %s", request_->method().c_str(), request_->url().path().c_str());
|
||||
}
|
||||
|
||||
bool RequestParser::ParseStartLine(const std::string& line) {
|
||||
std::vector<string_view> parts;
|
||||
Split(line, ' ', true, &parts);
|
||||
return matched;
|
||||
}
|
||||
|
||||
if (parts.size() != 3) {
|
||||
return false;
|
||||
}
|
||||
bool RequestParser::ParseStartLine(const std::string& line)
|
||||
{
|
||||
std::vector<string_view> parts;
|
||||
Split(line, ' ', true, &parts);
|
||||
|
||||
request_->set_method(parts[0]);
|
||||
request_->set_url(Url{ parts[1] });
|
||||
if (parts.size() != 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// HTTP version is ignored.
|
||||
request_->set_method(parts[0]);
|
||||
request_->set_url(Url{ parts[1] });
|
||||
|
||||
return true;
|
||||
}
|
||||
// HTTP version is ignored.
|
||||
|
||||
bool RequestParser::ParseContent(const char* data, std::size_t length) {
|
||||
if (content_type_.multipart()) {
|
||||
return ParseMultipartContent(data, length);
|
||||
} else {
|
||||
return Parser::ParseContent(data, length);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RequestParser::ParseMultipartContent(const char* data,
|
||||
std::size_t length) {
|
||||
pending_data_.append(data, length);
|
||||
bool RequestParser::ParseContent(const char* data, std::size_t length)
|
||||
{
|
||||
if (content_type_.multipart())
|
||||
{
|
||||
return ParseMultipartContent(data, length);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Parser::ParseContent(data, length);
|
||||
}
|
||||
}
|
||||
|
||||
if (!content_length_parsed_ || content_length_ == kInvalidLength) {
|
||||
// Invalid content length (syntax error).
|
||||
return false;
|
||||
}
|
||||
bool RequestParser::ParseMultipartContent(const char* data, std::size_t length)
|
||||
{
|
||||
pending_data_.append(data, length);
|
||||
|
||||
while (true) {
|
||||
if (pending_data_.empty()) {
|
||||
// Wait data from next read.
|
||||
break;
|
||||
}
|
||||
if (!content_length_parsed_ || content_length_ == kInvalidLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (step_ == Step::kStart) {
|
||||
std::string line;
|
||||
if (!GetNextLine(0, &line, true)) {
|
||||
break; // Not enough data
|
||||
}
|
||||
if (!IsBoundary(line, 0, line.size())) {
|
||||
LOG_ERRO("Invalid boundary: %s", line.c_str());
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("Boundary line: %s", line.c_str());
|
||||
// Go to next step.
|
||||
step_ = Step::kBoundaryParsed;
|
||||
continue;
|
||||
}
|
||||
const std::size_t kMaxAllowedMultipartSize = g_max_multipart_size; // 1 GB
|
||||
|
||||
if (step_ == Step::kBoundaryParsed) {
|
||||
if (!part_) {
|
||||
part_.reset(new FormPart{});
|
||||
}
|
||||
bool need_more_data = false;
|
||||
if (ParsePartHeaders(&need_more_data)) {
|
||||
// Go to next step.
|
||||
step_ = Step::kHeadersParsed;
|
||||
LOG_INFO("Part headers just ended");
|
||||
continue;
|
||||
} else {
|
||||
if (need_more_data) {
|
||||
// Need more data from next read.
|
||||
break;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (true)
|
||||
{
|
||||
if (pending_data_.empty())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (step_ == Step::kHeadersParsed) {
|
||||
std::size_t off = 0;
|
||||
std::size_t count = 0;
|
||||
bool ended = false;
|
||||
if (step_ == Step::kStart)
|
||||
{
|
||||
std::string line;
|
||||
if (!GetNextLine(0, &line, true))
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (!IsBoundary(line, 0, line.size()))
|
||||
{
|
||||
LOG_ERRO("Invalid boundary: %s", line.c_str());
|
||||
return false;
|
||||
}
|
||||
step_ = Step::kBoundaryParsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Remember last CRLF position.
|
||||
if (!GetNextBoundaryLine(&off, &count, &ended)) {
|
||||
break;
|
||||
}
|
||||
if (step_ == Step::kBoundaryParsed)
|
||||
{
|
||||
if (!part_)
|
||||
{
|
||||
part_.reset(new FormPart{});
|
||||
if (content_length_ > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
part_->ReserveData(content_length_);
|
||||
}
|
||||
catch (const std::bad_alloc&)
|
||||
{
|
||||
LOG_ERRO("Failed to allocate memory for size: %zu", content_length_);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool need_more_data = false;
|
||||
if (ParsePartHeaders(&need_more_data))
|
||||
{
|
||||
step_ = Step::kHeadersParsed;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (need_more_data)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next boundary found.
|
||||
LOG_INFO("Next boundary found, off=%u", off);
|
||||
if (step_ == Step::kHeadersParsed)
|
||||
{
|
||||
std::string strict_boundary = "\r\n--" + content_type_.boundary();
|
||||
std::size_t pos = pending_data_.find(strict_boundary);
|
||||
|
||||
// This part has ended.
|
||||
if (off >= 2) {
|
||||
// -2 for excluding the CRLF after the data.
|
||||
part_->AppendData(pending_data_.data(), off - 2);
|
||||
if (pos == std::string::npos)
|
||||
{
|
||||
std::size_t reserve_len = strict_boundary.size() + 8;
|
||||
if (pending_data_.size() > reserve_len)
|
||||
{
|
||||
std::size_t safe_len = pending_data_.size() - reserve_len;
|
||||
|
||||
// Erase the data of this part and the next boundary.
|
||||
// +2 for including the CRLF after the boundary.
|
||||
pending_data_.erase(0, off + count + 2);
|
||||
} else {
|
||||
LOG_ERRO("Invalid part data, off=%u", off);
|
||||
return false;
|
||||
}
|
||||
// 【修正点】:使用 part_->data().size() 替代 part_->GetDataSize(),避免触发异常!
|
||||
if (part_->data().size() + safe_len > kMaxAllowedMultipartSize)
|
||||
{
|
||||
LOG_ERRO("Multipart payload size exceeded max limit without boundary!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save this part
|
||||
form_parts_.push_back(part_);
|
||||
part_->AppendData(pending_data_.data(), safe_len);
|
||||
pending_data_.erase(0, safe_len);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset for next part.
|
||||
part_.reset();
|
||||
std::size_t boundary_start = pos;
|
||||
std::size_t line_end = pending_data_.find(kCRLF, boundary_start + 2);
|
||||
if (line_end == std::string::npos)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (ended) {
|
||||
// Go to the end step.
|
||||
step_ = Step::kEnded;
|
||||
break;
|
||||
} else {
|
||||
// Go to next step.
|
||||
step_ = Step::kBoundaryParsed;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::size_t count = line_end - (boundary_start + 2);
|
||||
bool ended = false;
|
||||
|
||||
if (step_ == Step::kEnded) {
|
||||
LOG_INFO("Multipart data has ended");
|
||||
if (!IsBoundary(pending_data_, boundary_start + 2, count, &ended))
|
||||
{
|
||||
std::size_t safe_len = boundary_start + 2;
|
||||
|
||||
// Create a body and set to the request.
|
||||
// 【修正点】:同理替换为 part_->data().size()
|
||||
if (part_->data().size() + safe_len > kMaxAllowedMultipartSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto body = std::make_shared<FormBody>(form_parts_,
|
||||
content_type_.boundary());
|
||||
part_->AppendData(pending_data_.data(), safe_len);
|
||||
pending_data_.erase(0, safe_len);
|
||||
continue;
|
||||
}
|
||||
|
||||
request_->SetBody(body, false); // TODO: set_length?
|
||||
if (boundary_start > 0)
|
||||
{
|
||||
if (part_->data().size() + boundary_start > kMaxAllowedMultipartSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
part_->AppendData(pending_data_.data(), boundary_start);
|
||||
}
|
||||
|
||||
Finish();
|
||||
}
|
||||
pending_data_.erase(0, line_end + 2);
|
||||
|
||||
return true;
|
||||
}
|
||||
form_parts_.push_back(part_);
|
||||
part_.reset();
|
||||
|
||||
bool RequestParser::ParsePartHeaders(bool* need_more_data) {
|
||||
std::size_t off = 0;
|
||||
if (ended)
|
||||
{
|
||||
step_ = Step::kEnded;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
step_ = Step::kBoundaryParsed;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
std::string line;
|
||||
if (!GetNextLine(off, &line, false)) {
|
||||
// Need more data from next read.
|
||||
*need_more_data = true;
|
||||
return false;
|
||||
}
|
||||
if (step_ == Step::kEnded)
|
||||
{
|
||||
LOG_INFO("Multipart data has ended");
|
||||
auto body = std::make_shared<FormBody>(form_parts_, content_type_.boundary());
|
||||
request_->SetBody(body, false);
|
||||
Finish();
|
||||
}
|
||||
|
||||
off = off + line.size() + 2; // +2 for CRLF
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.empty()) {
|
||||
// Headers finished.
|
||||
break;
|
||||
}
|
||||
bool RequestParser::ParsePartHeaders(bool* need_more_data)
|
||||
{
|
||||
std::size_t off = 0;
|
||||
|
||||
Header header;
|
||||
if (!SplitKV(line, ':', true, &header.first, &header.second)) {
|
||||
LOG_ERRO("Invalid part header line: %s", line.c_str());
|
||||
return false;
|
||||
}
|
||||
while (true)
|
||||
{
|
||||
std::string line;
|
||||
if (!GetNextLine(off, &line, false))
|
||||
{
|
||||
// Need more data from next read.
|
||||
*need_more_data = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Part header (%s: %s)", header.first.c_str(),
|
||||
header.second.c_str());
|
||||
off = off + line.size() + 2; // +2 for CRLF
|
||||
|
||||
// Parse Content-Disposition.
|
||||
if (boost::iequals(header.first, headers::kContentDisposition)) {
|
||||
ContentDisposition content_disposition{ header.second };
|
||||
if (!content_disposition.valid()) {
|
||||
LOG_ERRO("Invalid content-disposition header: %s",
|
||||
header.second.c_str());
|
||||
return false;
|
||||
}
|
||||
part_->set_name(content_disposition.name());
|
||||
part_->set_file_name(content_disposition.file_name());
|
||||
LOG_INFO("Content-Disposition (name=%s; filename=%s)",
|
||||
part_->name().c_str(), part_->file_name().c_str());
|
||||
}
|
||||
if (line.empty())
|
||||
{
|
||||
// Headers finished.
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: Parse other headers.
|
||||
}
|
||||
Header header;
|
||||
if (!SplitKV(line, ':', true, &header.first, &header.second))
|
||||
{
|
||||
LOG_ERRO("Invalid part header line: %s", line.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove the data which has just been parsed.
|
||||
pending_data_.erase(0, off);
|
||||
LOG_INFO("Part header (%s: %s)", header.first.c_str(),
|
||||
header.second.c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
// Parse Content-Disposition.
|
||||
if (boost::iequals(header.first, headers::kContentDisposition))
|
||||
{
|
||||
ContentDisposition content_disposition{ header.second };
|
||||
if (!content_disposition.valid())
|
||||
{
|
||||
LOG_ERRO("Invalid content-disposition header: %s",
|
||||
header.second.c_str());
|
||||
return false;
|
||||
}
|
||||
part_->set_name(content_disposition.name());
|
||||
part_->set_file_name(content_disposition.file_name());
|
||||
LOG_INFO("Content-Disposition (name=%s; filename=%s)",
|
||||
part_->name().c_str(), part_->file_name().c_str());
|
||||
}
|
||||
|
||||
bool RequestParser::GetNextBoundaryLine(std::size_t* b_off, std::size_t* b_len,
|
||||
bool* ended) {
|
||||
std::size_t off = 0;
|
||||
// TODO: Parse other headers.
|
||||
}
|
||||
|
||||
while (true) {
|
||||
std::size_t pos = pending_data_.find(kCRLF, off);
|
||||
if (pos == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
// Remove the data which has just been parsed.
|
||||
pending_data_.erase(0, off);
|
||||
|
||||
std::size_t len = pos - off;
|
||||
if (len == 0) {
|
||||
off = pos + 2;
|
||||
continue; // Empty line
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsBoundary(pending_data_, off, len, ended)) {
|
||||
*b_off = off;
|
||||
*b_len = len;
|
||||
return true;
|
||||
}
|
||||
bool RequestParser::GetNextBoundaryLine(std::size_t* b_off, std::size_t* b_len,
|
||||
bool* ended)
|
||||
{
|
||||
std::size_t off = 0;
|
||||
|
||||
off = pos + 2;
|
||||
}
|
||||
while (true)
|
||||
{
|
||||
std::size_t pos = pending_data_.find(kCRLF, off);
|
||||
if (pos == std::string::npos)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
std::size_t len = pos - off;
|
||||
if (len == 0)
|
||||
{
|
||||
off = pos + 2;
|
||||
continue; // Empty line
|
||||
}
|
||||
|
||||
bool RequestParser::IsBoundary(const std::string& str, std::size_t off,
|
||||
std::size_t count, bool* end) const {
|
||||
const std::string& boundary = content_type_.boundary();
|
||||
if (IsBoundary(pending_data_, off, len, ended))
|
||||
{
|
||||
*b_off = off;
|
||||
*b_len = len;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (count != boundary.size() + 2 && count != boundary.size() + 4) {
|
||||
return false;
|
||||
}
|
||||
off = pos + 2;
|
||||
}
|
||||
|
||||
if (str[off] != '-' || str[off + 1] != '-') {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count == boundary.size() + 4) {
|
||||
if (str[off + count - 1] != '-' || str[off + count - 2] != '-') {
|
||||
return false;
|
||||
}
|
||||
if (end != nullptr) {
|
||||
*end = true;
|
||||
}
|
||||
}
|
||||
bool RequestParser::IsBoundary(const std::string& str, std::size_t off,
|
||||
std::size_t count, bool* end) const
|
||||
{
|
||||
const std::string& boundary = content_type_.boundary();
|
||||
|
||||
return strncmp(boundary.c_str(), &str[off + 2], boundary.size()) == 0;
|
||||
}
|
||||
if (count != boundary.size() + 2 && count != boundary.size() + 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (str[off] != '-' || str[off + 1] != '-')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count == boundary.size() + 4)
|
||||
{
|
||||
if (str[off + count - 1] != '-' || str[off + count - 2] != '-')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (end != nullptr)
|
||||
{
|
||||
*end = true;
|
||||
}
|
||||
}
|
||||
|
||||
return strncmp(boundary.c_str(), &str[off + 2], boundary.size()) == 0;
|
||||
}
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
+64
-43
@@ -1,70 +1,91 @@
|
||||
#ifndef WEBCC_REQUEST_PARSER_H_
|
||||
#ifndef WEBCC_REQUEST_PARSER_H_
|
||||
#define WEBCC_REQUEST_PARSER_H_
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "webcc/parser.h"
|
||||
#include "webcc/view.h"
|
||||
|
||||
namespace webcc {
|
||||
namespace webcc
|
||||
{
|
||||
|
||||
using ViewMatcher =
|
||||
std::function<bool(const std::string&, const std::string&, bool*)>;
|
||||
using ViewMatcher =
|
||||
std::function<bool(const std::string&, const std::string&, bool*, ViewPtr*)>;
|
||||
|
||||
class Request;
|
||||
class Request;
|
||||
|
||||
class RequestParser : public Parser {
|
||||
public:
|
||||
RequestParser();
|
||||
class RequestParser : public Parser
|
||||
{
|
||||
public:
|
||||
RequestParser();
|
||||
|
||||
~RequestParser() override = default;
|
||||
~RequestParser() override = default;
|
||||
|
||||
void Init(Request* request, ViewMatcher view_matcher);
|
||||
void Init(Request* request, ViewMatcher view_matcher);
|
||||
|
||||
private:
|
||||
// Override to match the URL against views and check if the matched view
|
||||
// asks for data streaming.
|
||||
bool OnHeadersEnd() override;
|
||||
// 【新增】:检查 Header 是否已解析完毕
|
||||
bool IsHeaderParsed() const
|
||||
{
|
||||
return header_parsed_;
|
||||
}
|
||||
|
||||
bool ParseStartLine(const std::string& line) override;
|
||||
ViewPtr MatchedView() const
|
||||
{
|
||||
return matched_view_;
|
||||
}
|
||||
|
||||
// Override to handle multipart form data which is request only.
|
||||
bool ParseContent(const char* data, std::size_t length) override;
|
||||
private:
|
||||
// Override to match the URL against views and check if the matched view
|
||||
// asks for data streaming.
|
||||
bool OnHeadersEnd() override;
|
||||
|
||||
// Multipart specific parsing helpers.
|
||||
bool ParseStartLine(const std::string& line) override;
|
||||
|
||||
bool ParseMultipartContent(const char* data, std::size_t length);
|
||||
bool ParsePartHeaders(bool* need_more_data);
|
||||
bool GetNextBoundaryLine(std::size_t* b_off, std::size_t* b_len, bool* ended);
|
||||
// Override to handle multipart form data which is request only.
|
||||
bool ParseContent(const char* data, std::size_t length) override;
|
||||
|
||||
// Check if the str.substr(off, count) is a boundary.
|
||||
bool IsBoundary(const std::string& str, std::size_t off,
|
||||
std::size_t count, bool* end = nullptr) const;
|
||||
// Multipart specific parsing helpers.
|
||||
|
||||
private:
|
||||
// The result request message.
|
||||
Request* request_ = nullptr;
|
||||
bool ParseMultipartContent(const char* data, std::size_t length);
|
||||
bool ParsePartHeaders(bool* need_more_data);
|
||||
bool GetNextBoundaryLine(std::size_t* b_off, std::size_t* b_len, bool* ended);
|
||||
|
||||
// A function for matching view once the headers of a request has been
|
||||
// received. The parsing will stop and fail if no view can be matched.
|
||||
ViewMatcher view_matcher_;
|
||||
// Check if the str.substr(off, count) is a boundary.
|
||||
bool IsBoundary(const std::string& str, std::size_t off,
|
||||
std::size_t count, bool* end = nullptr) const;
|
||||
|
||||
// Form data parsing steps.
|
||||
enum class Step {
|
||||
kStart,
|
||||
kBoundaryParsed,
|
||||
kHeadersParsed,
|
||||
kEnded,
|
||||
};
|
||||
private:
|
||||
// The result request message.
|
||||
Request* request_ = nullptr;
|
||||
|
||||
Step step_ = Step::kStart;
|
||||
// A function for matching view once the headers of a request has been
|
||||
// received. The parsing will stop and fail if no view can be matched.
|
||||
ViewMatcher view_matcher_;
|
||||
|
||||
// The current form part being parsed.
|
||||
FormPartPtr part_;
|
||||
// Form data parsing steps.
|
||||
enum class Step
|
||||
{
|
||||
kStart,
|
||||
kBoundaryParsed,
|
||||
kHeadersParsed,
|
||||
kEnded,
|
||||
};
|
||||
|
||||
// All form parts parsed.
|
||||
std::vector<FormPartPtr> form_parts_;
|
||||
};
|
||||
Step step_ = Step::kStart;
|
||||
|
||||
// The current form part being parsed.
|
||||
FormPartPtr part_;
|
||||
|
||||
// All form parts parsed.
|
||||
std::vector<FormPartPtr> form_parts_;
|
||||
|
||||
// 【新增】:Header 解析完成标志
|
||||
bool header_parsed_ = false;
|
||||
|
||||
// View映射(用于请求头检查)
|
||||
ViewPtr matched_view_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
|
||||
+47
-5
@@ -9,34 +9,74 @@ static const std::pair<int, const char*> kTable[] = {
|
||||
{ Status::kSwitchingProtocols, "Switching Protocols" },
|
||||
{ Status::kProcessing, "Processing" },
|
||||
{ Status::kEarlyHints, "Early Hints" },
|
||||
|
||||
{ Status::kOK, "OK" },
|
||||
{ Status::kCreated, "Created" },
|
||||
{ Status::kAccepted, "Accepted" },
|
||||
{ Status::kNonAuthoritativeInformation, "Non-Authoritative Information" },
|
||||
{ Status::kNoContent, "No Content" },
|
||||
{ Status::kResetContent, "Reset Content" },
|
||||
{ Status::kPartialContent, "Partial Content" },
|
||||
{ Status::kMultiStatus, "Multi Status" },
|
||||
{ Status::kAlreadyReported, "Already Reported" },
|
||||
{ Status::kIMUsed, "IM Used" },
|
||||
|
||||
{ Status::kMultipleChoices, "Multiple Choices" },
|
||||
{ Status::kMovedPermanently, "Moved Permanently" },
|
||||
{ Status::kFound, "Found" },
|
||||
{ Status::kSeeOther, "SeeOther" },
|
||||
{ Status::kNotModified, "Not Modified" },
|
||||
{ Status::kUseProxy, "Use Proxy" },
|
||||
{ Status::k__Unused, "Unused" },
|
||||
{ Status::kTemporaryRedirect, "Temporary Reditect" },
|
||||
{ Status::kPermanentRedirect, "Permanent Redirect" },
|
||||
|
||||
{ Status::kBadRequest, "Bad Request" },
|
||||
{ Status::kUnauthorized, "Unauthorized" },
|
||||
{ Status::kPaymentRequired, "Payment Required" },
|
||||
{ Status::kForbidden, "Forbidden" },
|
||||
{ Status::kNotFound, "Not Found" },
|
||||
{ Status::kMethodNotAllowed, "Method Not Allowed" },
|
||||
{ Status::kNotAcceptable, "Not Acceptable" },
|
||||
{ Status::kProxyAuthenticationRequired, "Proxy Authentication Required" },
|
||||
{ Status::kRequestTimeout, "Request Timeout" },
|
||||
{ Status::kConflict, "Conflict" },
|
||||
{ Status::kGone, "Gone" },
|
||||
{ Status::kLocked, "Locked" },
|
||||
{ Status::kLengthRequired, "Length Required" },
|
||||
{ Status::kPreconditionFailed, "Precondition Failed" },
|
||||
{ Status::kContentTooLarge, "Content Too Large" },
|
||||
{ Status::kURITooLong, "URI Too Long" },
|
||||
{ Status::kUnsupportedMediaType, "Unsupported Media Type" },
|
||||
{ Status::kRangeNotSatisfiable, "Range Not Satisfiable" },
|
||||
{ Status::kExpectationFailed, "Expectation Failed" },
|
||||
{ Status::kIamATeapot, "I'm a Teapot" },
|
||||
{ Status::kBadRequest, "Bad Request" },
|
||||
{ Status::kNotFound, "Not Found" },
|
||||
{ Status::kMisdirectedRequest, "Misdirected Request" },
|
||||
{ Status::kUnprocessableContent, "Unprocessable Content" },
|
||||
{ Status::kLocked, "Locked" },
|
||||
{ Status::kFailedDependency, "Failed Dependency" },
|
||||
{ Status::kTooEarly, "TooEarly" },
|
||||
{ Status::kUpgradeRequired, "Upgrade Required" },
|
||||
{ Status::kPreconditionRequired, "Precondition Required" },
|
||||
{ Status::kTooManyRequests, "Too ManyR equests" },
|
||||
{ Status::kRequestHeaderFieldsTooLarge, "Request Header Fields Too Large" },
|
||||
{ Status::kUnavailableForLegalReasons, "Unavailable For Legal Reasons" },
|
||||
|
||||
{ Status::kInternalServerError, "Internal Server Error" },
|
||||
{ Status::kNotImplemented, "Not Implemented" },
|
||||
{ Status::kBadGateway, "Bad Gateway" },
|
||||
{ Status::kServiceUnavailable, "Service Unavailable" },
|
||||
{ Status::kGatewayTimeout, "Gateway Timeout" },
|
||||
{ Status::kHTTPVersionNotSupported, "HTTP Version Not Supported" },
|
||||
{ Status::kVariantAlsoNegotiates, "Variant Also Negotiates" },
|
||||
{ Status::kInsufficientStorage, "Insufficient Storage" },
|
||||
{ Status::kLoopDetected, "Loop Detected" },
|
||||
{ Status::kNotExtended, "Not Extended" },
|
||||
{ Status::kNetworkAuthenticationRequired, "Network Authentication Required" },
|
||||
|
||||
// Not Standard Code By UnknownObject
|
||||
// Not Standard Code By UnknownObject
|
||||
|
||||
{ Status::k_uRequestFormatError, "Request Format Error" },
|
||||
{ Status::k_uRequestInvalid, "Request Invalid" },
|
||||
{ Status::k_uSubProcessFalied, "SubProcess Falied" },
|
||||
{ Status::k_uURLOutOfRange, "URL Out Of Range" },
|
||||
{ Status::k_uInvalidRequestHost, "Invalid Request Host" },
|
||||
{ Status::k_uIPBlocked, "IP Blocked" },
|
||||
@@ -45,6 +85,8 @@ static const std::pair<int, const char*> kTable[] = {
|
||||
{ Status::k_uInvalidFile, "Invalid File" },
|
||||
{ Status::k_uFileTooLarge, "File Too Large" },
|
||||
{ Status::k_uRPSLimited, "RPS Limit" },
|
||||
|
||||
{ Status::k_uSubProcessFalied, "SubProcess Falied" },
|
||||
{ Status::k_uServerHateYou, "Server Hate You" },
|
||||
{ Status::k_uDoSFound, "DoS Found" },
|
||||
{ Status::k_uDDoSFound, "DDoS Found" },
|
||||
|
||||
@@ -17,7 +17,7 @@ public:
|
||||
// NOTE:
|
||||
// Currently, |request| is necessary only when Gzip is enabled and the client
|
||||
// does want to accept Gzip compressed response.
|
||||
explicit ResponseBuilder(RequestPtr request) : request_(request) {
|
||||
explicit ResponseBuilder(RequestPtr request) : request_(request), headers_() {
|
||||
}
|
||||
|
||||
ResponseBuilder(const ResponseBuilder&) = delete;
|
||||
@@ -103,6 +103,24 @@ public:
|
||||
// Add the `Date` header to the response.
|
||||
ResponseBuilder& Date();
|
||||
|
||||
//getter for information logger
|
||||
int GetCode() const
|
||||
{
|
||||
return (int)code_;
|
||||
}
|
||||
|
||||
size_t GetBodySize() const
|
||||
{
|
||||
if(body_ == nullptr)
|
||||
return 0;
|
||||
return body_->GetSize();
|
||||
}
|
||||
|
||||
bool IsBodyEmpty() const
|
||||
{
|
||||
return ((body_ == nullptr) || body_->IsEmpty());
|
||||
}
|
||||
|
||||
#if WEBCC_ENABLE_GZIP
|
||||
ResponseBuilder& Gzip(bool gzip = true) {
|
||||
gzip_ = gzip;
|
||||
|
||||
+120
-91
@@ -1,4 +1,4 @@
|
||||
#include "webcc/router.h"
|
||||
#include "webcc/router.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -7,117 +7,146 @@
|
||||
#include "webcc/logger.h"
|
||||
#include "router.h"
|
||||
|
||||
namespace webcc {
|
||||
namespace webcc
|
||||
{
|
||||
|
||||
bool Router::Route(string_view url, ViewPtr view, const Strings& methods) {
|
||||
assert(view);
|
||||
bool Router::Route(string_view url, ViewPtr view, const Strings& methods)
|
||||
{
|
||||
assert(view);
|
||||
|
||||
// TODO: More error check
|
||||
/*
|
||||
* Repeat Checker by Unknown Object at 2022-02-12
|
||||
*/
|
||||
//------------------RC Start--------------------------
|
||||
int cnt = 0;
|
||||
for (cnt = 0; cnt < routes_.size(); cnt++)
|
||||
if(ToString(routes_[cnt].url) == ToString(url))
|
||||
break;
|
||||
if (cnt != routes_.size()) //repeat found
|
||||
{
|
||||
routes_[cnt].view = view;
|
||||
routes_[cnt].methods = methods;
|
||||
return true;
|
||||
}
|
||||
//-------------------RC End---------------------------
|
||||
// TODO: More error check
|
||||
/*
|
||||
* Repeat Checker by Unknown Object at 2022-02-12
|
||||
*/
|
||||
//------------------RC Start--------------------------
|
||||
int cnt = 0;
|
||||
for (cnt = 0; cnt < routes_.size(); cnt++)
|
||||
if (ToString(routes_[cnt].url) == ToString(url))
|
||||
break;
|
||||
if (cnt != routes_.size()) //repeat found
|
||||
{
|
||||
routes_[cnt].view = view;
|
||||
routes_[cnt].methods = methods;
|
||||
return true;
|
||||
}
|
||||
//-------------------RC End---------------------------
|
||||
|
||||
routes_.push_back({ ToString(url), {}, view, methods });
|
||||
routes_.push_back({ ToString(url), {}, view, methods });
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Router::Route(const UrlRegex& regex_url, ViewPtr view,
|
||||
const Strings& methods) {
|
||||
assert(view);
|
||||
bool Router::Route(const UrlRegex& regex_url, ViewPtr view,
|
||||
const Strings& methods)
|
||||
{
|
||||
assert(view);
|
||||
|
||||
// TODO: More error check
|
||||
// TODO: More error check
|
||||
|
||||
try {
|
||||
routes_.push_back({ "", regex_url(), view, methods });
|
||||
try
|
||||
{
|
||||
routes_.push_back({ "", regex_url(), view, methods });
|
||||
|
||||
} catch (const std::regex_error& e) {
|
||||
LOG_ERRO("Not a valid regular expression: %s", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (const std::regex_error& e)
|
||||
{
|
||||
LOG_ERRO("Not a valid regular expression: %s", e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ViewPtr Router::FindView(const std::string& method, const std::string& url,
|
||||
UrlArgs* args) {
|
||||
assert(args != nullptr);
|
||||
ViewPtr Router::FindView(const std::string& method, const std::string& url,
|
||||
UrlArgs* args)
|
||||
{
|
||||
assert(args != nullptr);
|
||||
|
||||
for (auto& route : routes_) {
|
||||
if (std::find(route.methods.begin(), route.methods.end(), method) ==
|
||||
route.methods.end()) {
|
||||
continue;
|
||||
}
|
||||
for (auto& route : routes_)
|
||||
{
|
||||
if (std::find(route.methods.begin(), route.methods.end(), method) ==
|
||||
route.methods.end())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (route.url.empty()) {
|
||||
std::smatch match;
|
||||
if (route.url.empty())
|
||||
{
|
||||
std::smatch match;
|
||||
|
||||
if (std::regex_match(url, match, route.url_regex)) {
|
||||
// Any sub-matches?
|
||||
// Start from 1 because match[0] is the whole string itself.
|
||||
for (size_t i = 1; i < match.size(); ++i) {
|
||||
args->push_back(match[i].str());
|
||||
}
|
||||
if (std::regex_match(url, match, route.url_regex))
|
||||
{
|
||||
// Any sub-matches?
|
||||
// Start from 1 because match[0] is the whole string itself.
|
||||
for (size_t i = 1; i < match.size(); ++i)
|
||||
{
|
||||
args->push_back(match[i].str());
|
||||
}
|
||||
|
||||
return route.view;
|
||||
}
|
||||
} else {
|
||||
if (boost::iequals(route.url, url)) {
|
||||
return route.view;
|
||||
}
|
||||
}
|
||||
}
|
||||
return route.view;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (boost::iequals(route.url, url))
|
||||
{
|
||||
return route.view;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ViewPtr();
|
||||
}
|
||||
return ViewPtr();
|
||||
}
|
||||
|
||||
bool Router::MatchView(const std::string& method, const std::string& url,
|
||||
bool* stream) {
|
||||
assert(stream != nullptr);
|
||||
*stream = false;
|
||||
bool Router::MatchView(const std::string& method, const std::string& url,
|
||||
bool* stream, ViewPtr* out_view)
|
||||
{
|
||||
assert(stream != nullptr);
|
||||
*stream = false;
|
||||
|
||||
for (auto& route : routes_) {
|
||||
if (std::find(route.methods.begin(), route.methods.end(), method) ==
|
||||
route.methods.end()) {
|
||||
continue;
|
||||
}
|
||||
for (auto& route : routes_)
|
||||
{
|
||||
if (std::find(route.methods.begin(), route.methods.end(), method) ==
|
||||
route.methods.end())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (route.url.empty()) {
|
||||
std::smatch match;
|
||||
if (route.url.empty())
|
||||
{
|
||||
std::smatch match;
|
||||
|
||||
if (std::regex_match(url, match, route.url_regex)) {
|
||||
*stream = route.view->Stream(method);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (boost::iequals(route.url, url)) {
|
||||
*stream = route.view->Stream(method);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (std::regex_match(url, match, route.url_regex))
|
||||
{
|
||||
*stream = route.view->Stream(method);
|
||||
if (out_view != nullptr)
|
||||
*out_view = route.view; // 【新增】
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (boost::iequals(route.url, url))
|
||||
{
|
||||
*stream = route.view->Stream(method);
|
||||
if (out_view != nullptr)
|
||||
*out_view = route.view; // 【新增】
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t Router::GetViewCount() {
|
||||
return routes_.size();
|
||||
}
|
||||
size_t Router::GetViewCount()
|
||||
{
|
||||
return routes_.size();
|
||||
}
|
||||
|
||||
ViewPtr& Router::AccessView(size_t index) {
|
||||
return routes_.at(index).view;
|
||||
}
|
||||
ViewPtr& Router::AccessView(size_t index)
|
||||
{
|
||||
return routes_.at(index).view;
|
||||
}
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ public:
|
||||
// Return if a view is matched or not.
|
||||
// If the view asks for data streaming, |stream| will be set to true.
|
||||
bool MatchView(const std::string& method, const std::string& url,
|
||||
bool* stream);
|
||||
bool* stream, ViewPtr* out_view = nullptr);
|
||||
|
||||
// Direct access for ViewPtr pointers.
|
||||
// This can be used as server-based ip block/condiction checks
|
||||
|
||||
+324
-265
@@ -1,4 +1,4 @@
|
||||
#include "webcc/server.h"
|
||||
#include "webcc/server.h"
|
||||
|
||||
#include <csignal>
|
||||
#include <fstream>
|
||||
@@ -14,7 +14,8 @@
|
||||
using namespace std::placeholders;
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
namespace webcc {
|
||||
namespace webcc
|
||||
{
|
||||
|
||||
// NOTE:
|
||||
// Using `asio::strand` is possible but not necessary:
|
||||
@@ -26,353 +27,411 @@ namespace webcc {
|
||||
// acceptor_(strand_)
|
||||
// The same applies to the sockets.
|
||||
|
||||
Server::Server(boost::asio::ip::tcp protocol, std::uint16_t port,
|
||||
const fs::path& doc_root)
|
||||
: protocol_(protocol),
|
||||
port_(port),
|
||||
doc_root_(doc_root),
|
||||
acceptor_(io_context_),
|
||||
signals_(io_context_) {
|
||||
AddSignals();
|
||||
server_name__ = utility::UserAgent();
|
||||
}
|
||||
Server::Server(boost::asio::ip::tcp protocol, std::uint16_t port,
|
||||
const fs::path& doc_root)
|
||||
: protocol_(protocol),
|
||||
port_(port),
|
||||
doc_root_(doc_root),
|
||||
acceptor_(io_context_),
|
||||
signals_(io_context_)
|
||||
{
|
||||
AddSignals();
|
||||
server_name__ = utility::UserAgent();
|
||||
}
|
||||
|
||||
void Server::Run(std::size_t workers, std::size_t loops) {
|
||||
assert(workers > 0);
|
||||
void Server::Run(std::size_t workers, std::size_t loops)
|
||||
{
|
||||
assert(workers > 0);
|
||||
|
||||
#if WEBCC_STUDY_SERVER_THREADING
|
||||
LOG_USER("Run(workers:%u, loops:%u)", workers, loops);
|
||||
LOG_USER("Run(workers:%u, loops:%u)", workers, loops);
|
||||
#endif
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{ state_mutex_ };
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{ state_mutex_ };
|
||||
|
||||
assert(worker_threads_.empty());
|
||||
assert(worker_threads_.empty());
|
||||
|
||||
if (IsRunning()) {
|
||||
LOG_WARN("Server is already running");
|
||||
return;
|
||||
}
|
||||
if (IsRunning())
|
||||
{
|
||||
LOG_WARN("Server is already running");
|
||||
return;
|
||||
}
|
||||
|
||||
running_ = true;
|
||||
io_context_.restart();
|
||||
running_ = true;
|
||||
io_context_.restart();
|
||||
|
||||
if (!Listen(port_)) {
|
||||
LOG_ERRO("Server is NOT going to run");
|
||||
return;
|
||||
}
|
||||
if (!Listen(port_))
|
||||
{
|
||||
LOG_ERRO("Server is NOT going to run");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Server is going to run");
|
||||
LOG_INFO("Server is going to run");
|
||||
|
||||
AsyncWaitSignals();
|
||||
AsyncWaitSignals();
|
||||
|
||||
AsyncAccept();
|
||||
AsyncAccept();
|
||||
|
||||
// Create worker threads.
|
||||
for (std::size_t i = 0; i < workers; ++i) {
|
||||
worker_threads_.emplace_back(&Server::WorkerRoutine, this);
|
||||
}
|
||||
}
|
||||
// Create worker threads.
|
||||
for (std::size_t i = 0; i < workers; ++i)
|
||||
{
|
||||
worker_threads_.emplace_back(&Server::WorkerRoutine, this);
|
||||
}
|
||||
}
|
||||
|
||||
// Start the event loop.
|
||||
// The io_context::run() call will block until all asynchronous operations
|
||||
// have finished. While the server is running, there is always at least one
|
||||
// asynchronous operation outstanding: the asynchronous accept call waiting
|
||||
// for new incoming connections.
|
||||
// Start the event loop.
|
||||
// The io_context::run() call will block until all asynchronous operations
|
||||
// have finished. While the server is running, there is always at least one
|
||||
// asynchronous operation outstanding: the asynchronous accept call waiting
|
||||
// for new incoming connections.
|
||||
|
||||
LOG_INFO("Loop is running in %u thread(s)", loops);
|
||||
LOG_INFO("Loop is running in %u thread(s)", loops);
|
||||
|
||||
if (loops == 1) {
|
||||
// Run the loop in current thread.
|
||||
io_context_.run();
|
||||
} else {
|
||||
std::vector<std::thread> loop_threads;
|
||||
for (std::size_t i = 0; i < loops; ++i) {
|
||||
loop_threads.emplace_back(&boost::asio::io_context::run, &io_context_);
|
||||
}
|
||||
// Join the threads for blocking.
|
||||
for (std::size_t i = 0; i < loops; ++i) {
|
||||
loop_threads[i].join();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (loops == 1)
|
||||
{
|
||||
// Run the loop in current thread.
|
||||
io_context_.run();
|
||||
}
|
||||
else
|
||||
{
|
||||
std::vector<std::thread> loop_threads;
|
||||
for (std::size_t i = 0; i < loops; ++i)
|
||||
{
|
||||
loop_threads.emplace_back(&boost::asio::io_context::run, &io_context_);
|
||||
}
|
||||
// Join the threads for blocking.
|
||||
for (std::size_t i = 0; i < loops; ++i)
|
||||
{
|
||||
loop_threads[i].join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Server::Stop() {
|
||||
std::lock_guard<std::mutex> lock{ state_mutex_ };
|
||||
void Server::Stop()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{ state_mutex_ };
|
||||
|
||||
DoStop();
|
||||
}
|
||||
DoStop();
|
||||
}
|
||||
|
||||
bool Server::IsRunning() const {
|
||||
return running_ && !io_context_.stopped();
|
||||
}
|
||||
bool Server::IsRunning() const
|
||||
{
|
||||
return running_ && !io_context_.stopped();
|
||||
}
|
||||
|
||||
void Server::AddSignals() {
|
||||
signals_.add(SIGINT); // Ctrl+C
|
||||
signals_.add(SIGTERM);
|
||||
void Server::AddSignals()
|
||||
{
|
||||
signals_.add(SIGINT); // Ctrl+C
|
||||
signals_.add(SIGTERM);
|
||||
|
||||
#if defined(SIGQUIT)
|
||||
signals_.add(SIGQUIT);
|
||||
signals_.add(SIGQUIT);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void Server::AsyncWaitSignals() {
|
||||
signals_.async_wait(
|
||||
[this](boost::system::error_code, int signo) {
|
||||
// The server is stopped by canceling all outstanding asynchronous
|
||||
// operations. Once all operations have finished the io_context::run()
|
||||
// call will exit.
|
||||
LOG_INFO("On signal %d, stop the server", signo);
|
||||
void Server::AsyncWaitSignals()
|
||||
{
|
||||
signals_.async_wait(
|
||||
[this] (boost::system::error_code, int signo)
|
||||
{
|
||||
// The server is stopped by canceling all outstanding asynchronous
|
||||
// operations. Once all operations have finished the io_context::run()
|
||||
// call will exit.
|
||||
LOG_INFO("On signal %d, stop the server", signo);
|
||||
|
||||
DoStop();
|
||||
});
|
||||
}
|
||||
DoStop();
|
||||
});
|
||||
}
|
||||
|
||||
bool Server::Listen(std::uint16_t port) {
|
||||
boost::system::error_code ec;
|
||||
bool Server::Listen(std::uint16_t port)
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
|
||||
tcp::endpoint endpoint{ protocol_, port };
|
||||
tcp::endpoint endpoint{ protocol_, port };
|
||||
|
||||
// Open the acceptor.
|
||||
acceptor_.open(endpoint.protocol(), ec);
|
||||
if (ec) {
|
||||
LOG_ERRO("Acceptor open error (%s)", ec.message().c_str());
|
||||
return false;
|
||||
}
|
||||
// Open the acceptor.
|
||||
acceptor_.open(endpoint.protocol(), ec);
|
||||
if (ec)
|
||||
{
|
||||
LOG_ERRO("Acceptor open error (%s)", ec.message().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set option SO_REUSEADDR on.
|
||||
// When SO_REUSEADDR is set, multiple servers can listen on the same port.
|
||||
// This is necessary for restarting the server on the same port.
|
||||
// More details:
|
||||
// - https://stackoverflow.com/a/3233022
|
||||
// - http://www.andy-pearce.com/blog/posts/2013/Feb/so_reuseaddr-on-windows/
|
||||
acceptor_.set_option(tcp::acceptor::reuse_address(true));
|
||||
// Set option SO_REUSEADDR on.
|
||||
// When SO_REUSEADDR is set, multiple servers can listen on the same port.
|
||||
// This is necessary for restarting the server on the same port.
|
||||
// More details:
|
||||
// - https://stackoverflow.com/a/3233022
|
||||
// - http://www.andy-pearce.com/blog/posts/2013/Feb/so_reuseaddr-on-windows/
|
||||
acceptor_.set_option(tcp::acceptor::reuse_address(true));
|
||||
|
||||
// Bind to the server address.
|
||||
acceptor_.bind(endpoint, ec);
|
||||
if (ec) {
|
||||
LOG_ERRO("Acceptor bind error (%s)", ec.message().c_str());
|
||||
return false;
|
||||
}
|
||||
// Bind to the server address.
|
||||
acceptor_.bind(endpoint, ec);
|
||||
if (ec)
|
||||
{
|
||||
LOG_ERRO("Acceptor bind error (%s)", ec.message().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start listening for connections.
|
||||
// After listen, the client is able to connect to the server even the server
|
||||
// has not started to accept the connection yet.
|
||||
acceptor_.listen(boost::asio::socket_base::max_listen_connections, ec);
|
||||
if (ec) {
|
||||
LOG_ERRO("Acceptor listen error (%s)", ec.message().c_str());
|
||||
return false;
|
||||
}
|
||||
// Start listening for connections.
|
||||
// After listen, the client is able to connect to the server even the server
|
||||
// has not started to accept the connection yet.
|
||||
acceptor_.listen(boost::asio::socket_base::max_listen_connections, ec);
|
||||
if (ec)
|
||||
{
|
||||
LOG_ERRO("Acceptor listen error (%s)", ec.message().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Server::AsyncAccept() {
|
||||
void Server::AsyncAccept()
|
||||
{
|
||||
#if WEBCC_STUDY_SERVER_THREADING
|
||||
LOG_USER("AsyncAccept");
|
||||
LOG_USER("AsyncAccept");
|
||||
#endif
|
||||
|
||||
acceptor_.async_accept(
|
||||
[this](boost::system::error_code ec, tcp::socket socket) {
|
||||
acceptor_.async_accept(
|
||||
[this] (boost::system::error_code ec, tcp::socket socket)
|
||||
{
|
||||
#if WEBCC_STUDY_SERVER_THREADING
|
||||
LOG_USER("Accept handler");
|
||||
LOG_USER("Accept handler");
|
||||
#endif
|
||||
|
||||
// Check whether the server was stopped by a signal before this
|
||||
// completion handler had a chance to run.
|
||||
if (!acceptor_.is_open()) {
|
||||
return;
|
||||
}
|
||||
// Check whether the server was stopped by a signal before this
|
||||
// completion handler had a chance to run.
|
||||
if (!acceptor_.is_open())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ec) {
|
||||
LOG_INFO("Accepted a connection");
|
||||
if (!ec)
|
||||
{
|
||||
LOG_INFO("Accepted a connection");
|
||||
|
||||
auto view_matcher = std::bind(&Server::MatchViewOrStatic, this, _1,
|
||||
_2, _3);
|
||||
auto view_matcher = std::bind(&Server::MatchViewOrStatic, this, _1,
|
||||
_2, _3, _4);
|
||||
|
||||
auto connection = std::make_shared<Connection>(
|
||||
std::move(socket), &pool_, &queue_, std::move(view_matcher),
|
||||
buffer_size_);
|
||||
auto connection = std::make_shared<Connection>(
|
||||
std::move(socket), &pool_, &queue_, std::move(view_matcher),
|
||||
buffer_size_);
|
||||
|
||||
pool_.Start(connection);
|
||||
}
|
||||
pool_.Start(connection);
|
||||
}
|
||||
|
||||
AsyncAccept();
|
||||
});
|
||||
}
|
||||
AsyncAccept();
|
||||
});
|
||||
}
|
||||
|
||||
void Server::DoStop() {
|
||||
// Stop accepting new connections.
|
||||
acceptor_.close();
|
||||
void Server::DoStop()
|
||||
{
|
||||
// Stop accepting new connections.
|
||||
acceptor_.close();
|
||||
|
||||
// Stop worker threads.
|
||||
// This might take some time if the threads are still processing.
|
||||
StopWorkers();
|
||||
// Stop worker threads.
|
||||
// This might take some time if the threads are still processing.
|
||||
StopWorkers();
|
||||
|
||||
// Close all pending connections.
|
||||
pool_.Clear();
|
||||
// Close all pending connections.
|
||||
pool_.Clear();
|
||||
|
||||
// Finally, stop the event processing loop.
|
||||
// This function does not block, but instead simply signals the io_context to
|
||||
// stop. All invocations of its run() or run_one() member functions should
|
||||
// return as soon as possible.
|
||||
io_context_.stop();
|
||||
// Finally, stop the event processing loop.
|
||||
// This function does not block, but instead simply signals the io_context to
|
||||
// stop. All invocations of its run() or run_one() member functions should
|
||||
// return as soon as possible.
|
||||
io_context_.stop();
|
||||
|
||||
running_ = false;
|
||||
}
|
||||
running_ = false;
|
||||
}
|
||||
|
||||
void Server::WorkerRoutine() {
|
||||
LOG_INFO("Worker is running");
|
||||
void Server::WorkerRoutine()
|
||||
{
|
||||
LOG_INFO("Worker is running");
|
||||
|
||||
for (;;) {
|
||||
auto connection = queue_.PopOrWait();
|
||||
for (;;)
|
||||
{
|
||||
auto connection = queue_.PopOrWait();
|
||||
|
||||
if (!connection) {
|
||||
LOG_INFO("Worker is going to stop");
|
||||
if (!connection)
|
||||
{
|
||||
LOG_INFO("Worker is going to stop");
|
||||
|
||||
// For stopping next worker.
|
||||
queue_.Push({});
|
||||
// For stopping next worker.
|
||||
queue_.Push({});
|
||||
|
||||
// Stop this worker.
|
||||
break;
|
||||
}
|
||||
// Stop this worker.
|
||||
break;
|
||||
}
|
||||
|
||||
Handle(connection);
|
||||
}
|
||||
}
|
||||
Handle(connection);
|
||||
}
|
||||
}
|
||||
|
||||
void Server::StopWorkers() {
|
||||
LOG_INFO("Stop workers");
|
||||
void Server::StopWorkers()
|
||||
{
|
||||
LOG_INFO("Stop workers");
|
||||
|
||||
// Clear/drop pending connections.
|
||||
// The connections will be closed later (see DoStop).
|
||||
// Alternatively, we can wait for the pending connections to be handled.
|
||||
if (queue_.Size() != 0) {
|
||||
LOG_INFO("Clear pending connections");
|
||||
queue_.Clear();
|
||||
}
|
||||
// Clear/drop pending connections.
|
||||
// The connections will be closed later (see DoStop).
|
||||
// Alternatively, we can wait for the pending connections to be handled.
|
||||
if (queue_.Size() != 0)
|
||||
{
|
||||
LOG_INFO("Clear pending connections");
|
||||
queue_.Clear();
|
||||
}
|
||||
|
||||
// Enqueue a null connection to trigger the first worker to stop.
|
||||
queue_.Push(ConnectionPtr());
|
||||
// Enqueue a null connection to trigger the first worker to stop.
|
||||
queue_.Push(ConnectionPtr());
|
||||
|
||||
// Wait for worker threads to finish.
|
||||
for (auto& t : worker_threads_) {
|
||||
if (t.joinable()) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
// Wait for worker threads to finish.
|
||||
for (auto& t : worker_threads_)
|
||||
{
|
||||
if (t.joinable())
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup worker threads.
|
||||
worker_threads_.clear();
|
||||
// Cleanup worker threads.
|
||||
worker_threads_.clear();
|
||||
|
||||
// Clear the queue because it has a remaining null connection pushed by the
|
||||
// last worker thread.
|
||||
queue_.Clear();
|
||||
// Clear the queue because it has a remaining null connection pushed by the
|
||||
// last worker thread.
|
||||
queue_.Clear();
|
||||
|
||||
LOG_INFO("Workers stopped");
|
||||
}
|
||||
LOG_INFO("Workers stopped");
|
||||
}
|
||||
|
||||
// UnknownObject at 2022-09-04:
|
||||
// 1. Updated method error code to 405 (old code is 400)
|
||||
// 2. Add function to change 'Server' header
|
||||
void Server::Handle(ConnectionPtr connection) {
|
||||
auto request = connection->request();
|
||||
// UnknownObject at 2022-09-04:
|
||||
// 1. Updated method error code to 405 (old code is 400)
|
||||
// 2. Add function to change 'Server' header
|
||||
void Server::Handle(ConnectionPtr connection)
|
||||
{
|
||||
auto request = connection->request();
|
||||
|
||||
const Url& url = request->url();
|
||||
LOG_INFO("Request URL path: %s", url.path().c_str());
|
||||
const Url& url = request->url();
|
||||
LOG_INFO("Request URL path: %s", url.path().c_str());
|
||||
|
||||
UrlArgs args;
|
||||
auto view = FindView(request->method(), url.path(), &args);
|
||||
UrlArgs args;
|
||||
auto view = FindView(request->method(), url.path(), &args);
|
||||
|
||||
if (!view) {
|
||||
LOG_WARN("No view matches the request: %s %s", request->method().c_str(),
|
||||
url.path().c_str());
|
||||
if (!view)
|
||||
{
|
||||
LOG_WARN("No view matches the request: %s %s", request->method().c_str(),
|
||||
url.path().c_str());
|
||||
|
||||
if (request->method() == methods::kGet) {
|
||||
// Try to serve static files for GET request.
|
||||
auto response = ServeStatic(request);
|
||||
response->SetHeader(headers::kServer, server_name__);
|
||||
if (!response) {
|
||||
// Static file not found.
|
||||
connection->SendResponse(Status::kNotFound, server_name__);
|
||||
} else {
|
||||
connection->SendResponse(response);
|
||||
}
|
||||
} else {
|
||||
connection->SendResponse(Status::kMethodNotAllowed, server_name__);
|
||||
}
|
||||
if (request->method() == methods::kGet)
|
||||
{
|
||||
// Try to serve static files for GET request.
|
||||
auto response = ServeStatic(request);
|
||||
response->SetHeader(headers::kServer, server_name__);
|
||||
if (!response)
|
||||
{
|
||||
// Static file not found.
|
||||
connection->SendResponse(Status::kNotFound, server_name__);
|
||||
}
|
||||
else
|
||||
{
|
||||
connection->SendResponse(response);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
connection->SendResponse(Status::kMethodNotAllowed, server_name__);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the (regex matched) URL args to request object.
|
||||
request->set_args(std::move(args));
|
||||
// Save the (regex matched) URL args to request object.
|
||||
request->set_args(std::move(args));
|
||||
|
||||
// Ask the matched view to process the request.
|
||||
ResponsePtr response = view->Handle(request);
|
||||
// Ask the matched view to process the request.
|
||||
ResponsePtr response = view->Handle(request);
|
||||
|
||||
// Send the response back.
|
||||
if (response) {
|
||||
if (!response->HasHeader(headers::kServer))
|
||||
response->SetHeader(headers::kServer, server_name__);
|
||||
connection->SendResponse(response);
|
||||
} else {
|
||||
connection->SendResponse(Status::kBadRequest, server_name__);
|
||||
}
|
||||
}
|
||||
// Send the response back.
|
||||
if (response)
|
||||
{
|
||||
if (!response->HasHeader(headers::kServer))
|
||||
response->SetHeader(headers::kServer, server_name__);
|
||||
connection->SendResponse(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
connection->SendResponse(Status::kBadRequest, server_name__);
|
||||
}
|
||||
}
|
||||
|
||||
bool Server::MatchViewOrStatic(const std::string& method,
|
||||
const std::string& url, bool* stream) {
|
||||
if (Router::MatchView(method, url, stream)) {
|
||||
return true;
|
||||
}
|
||||
bool Server::MatchViewOrStatic(const std::string& method,
|
||||
const std::string& url, bool* stream, ViewPtr* out_view)
|
||||
{
|
||||
if (Router::MatchView(method, url, stream, out_view))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to match a static file.
|
||||
if (method == methods::kGet && !doc_root_.empty()) {
|
||||
fs::path path = doc_root_ / url;
|
||||
// 如果没有匹配到 View,确保 out_view 被清空
|
||||
if (out_view != nullptr)
|
||||
{
|
||||
*out_view = nullptr;
|
||||
}
|
||||
|
||||
fs::error_code ec;
|
||||
if (!fs::is_directory(path, ec) && fs::exists(path, ec)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try to match a static file.
|
||||
if (method == methods::kGet && !doc_root_.empty())
|
||||
{
|
||||
fs::path path = doc_root_ / url;
|
||||
|
||||
return false;
|
||||
}
|
||||
fs::error_code ec;
|
||||
if (!fs::is_directory(path, ec) && fs::exists(path, ec))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ResponsePtr Server::ServeStatic(RequestPtr request) {
|
||||
assert(request->method() == methods::kGet);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (doc_root_.empty()) {
|
||||
LOG_INFO("The doc root was not specified");
|
||||
return {};
|
||||
}
|
||||
ResponsePtr Server::ServeStatic(RequestPtr request)
|
||||
{
|
||||
assert(request->method() == methods::kGet);
|
||||
|
||||
fs::path path = doc_root_ / request->url().path();
|
||||
if (doc_root_.empty())
|
||||
{
|
||||
LOG_INFO("The doc root was not specified");
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
// NOTE: FileBody might throw Error::kFileError.
|
||||
auto body = std::make_shared<FileBody>(path, file_chunk_size_);
|
||||
fs::path path = doc_root_ / request->url().path();
|
||||
|
||||
auto response = std::make_shared<Response>(Status::kOK);
|
||||
try
|
||||
{
|
||||
// NOTE: FileBody might throw Error::kFileError.
|
||||
auto body = std::make_shared<FileBody>(path, file_chunk_size_);
|
||||
|
||||
std::string extension = path.extension().string();
|
||||
response->SetContentType(media_types::FromExtension(extension), "");
|
||||
auto response = std::make_shared<Response>(Status::kOK);
|
||||
|
||||
// NOTE: Gzip compression is not supported.
|
||||
response->SetBody(body, true);
|
||||
std::string extension = path.extension().string();
|
||||
response->SetContentType(media_types::FromExtension(extension), "");
|
||||
|
||||
return response;
|
||||
// NOTE: Gzip compression is not supported.
|
||||
response->SetBody(body, true);
|
||||
|
||||
} catch (const Error& error) {
|
||||
LOG_ERRO("File error: %s", error.message().c_str());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return response;
|
||||
|
||||
void Server::SetDefaultServerName(std::string server_name) {
|
||||
server_name__ = server_name;
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (const Error& error)
|
||||
{
|
||||
LOG_ERRO("File error: %s", error.message().c_str());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void Server::SetDefaultServerName(std::string server_name)
|
||||
{
|
||||
server_name__ = server_name;
|
||||
return;
|
||||
}
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
+106
-99
@@ -16,138 +16,145 @@
|
||||
#include "webcc/router.h"
|
||||
#include "webcc/url.h"
|
||||
|
||||
namespace webcc {
|
||||
namespace webcc
|
||||
{
|
||||
|
||||
class Server : public Router {
|
||||
public:
|
||||
Server(boost::asio::ip::tcp protocol, std::uint16_t port,
|
||||
const fs::path& doc_root = {});
|
||||
using HeaderValidator = std::function<bool(const RequestPtr&)>;
|
||||
|
||||
Server(const Server&) = delete;
|
||||
Server& operator=(const Server&) = delete;
|
||||
class Server : public Router
|
||||
{
|
||||
public:
|
||||
Server(boost::asio::ip::tcp protocol, std::uint16_t port,
|
||||
const fs::path& doc_root = {});
|
||||
|
||||
~Server() = default;
|
||||
Server(const Server&) = delete;
|
||||
Server& operator=(const Server&) = delete;
|
||||
|
||||
void set_buffer_size(std::size_t buffer_size) {
|
||||
if (buffer_size > 0) {
|
||||
buffer_size_ = buffer_size;
|
||||
}
|
||||
}
|
||||
~Server() = default;
|
||||
|
||||
void set_file_chunk_size(std::size_t file_chunk_size) {
|
||||
assert(file_chunk_size > 0);
|
||||
file_chunk_size_ = file_chunk_size;
|
||||
}
|
||||
void set_buffer_size(std::size_t buffer_size)
|
||||
{
|
||||
if (buffer_size > 0)
|
||||
{
|
||||
buffer_size_ = buffer_size;
|
||||
}
|
||||
}
|
||||
|
||||
// Start and run the server.
|
||||
// This method is blocking so will not return until Stop() is called (from
|
||||
// another thread) or a signal like SIGINT is caught.
|
||||
// When the request of a connection has been read, the connection is put into
|
||||
// a queue waiting for some worker thread to process. Normally, the more
|
||||
// |workers| you have, the more concurrency you gain (the concurrency also
|
||||
// depends on the number of CPU cores). The worker thread pops connections
|
||||
// from the queue one by one, prepares the response by the user provided View,
|
||||
// then sends it back to the client.
|
||||
// Meanwhile, the (event) loop, i.e., io_context, is also running in a number
|
||||
// (|loops|) of threads. Normally, one thread for the loop is good enough, but
|
||||
// it could be more than that.
|
||||
void Run(std::size_t workers = 1, std::size_t loops = 1);
|
||||
void set_file_chunk_size(std::size_t file_chunk_size)
|
||||
{
|
||||
assert(file_chunk_size > 0);
|
||||
file_chunk_size_ = file_chunk_size;
|
||||
}
|
||||
|
||||
// Stop the server.
|
||||
// This should be called from another thread since the Run() is blocking.
|
||||
void Stop();
|
||||
// Start and run the server.
|
||||
// This method is blocking so will not return until Stop() is called (from
|
||||
// another thread) or a signal like SIGINT is caught.
|
||||
// When the request of a connection has been read, the connection is put into
|
||||
// a queue waiting for some worker thread to process. Normally, the more
|
||||
// |workers| you have, the more concurrency you gain (the concurrency also
|
||||
// depends on the number of CPU cores). The worker thread pops connections
|
||||
// from the queue one by one, prepares the response by the user provided View,
|
||||
// then sends it back to the client.
|
||||
// Meanwhile, the (event) loop, i.e., io_context, is also running in a number
|
||||
// (|loops|) of threads. Normally, one thread for the loop is good enough, but
|
||||
// it could be more than that.
|
||||
void Run(std::size_t workers = 1, std::size_t loops = 1);
|
||||
|
||||
// Is the server running?
|
||||
bool IsRunning() const;
|
||||
// Stop the server.
|
||||
// This should be called from another thread since the Run() is blocking.
|
||||
void Stop();
|
||||
|
||||
// For High-Level api deleloper: to set the default server name insteaed of
|
||||
// webcc Added by UnknownObject at 2022-09-04
|
||||
void SetDefaultServerName(std::string server_name);
|
||||
// Is the server running?
|
||||
bool IsRunning() const;
|
||||
|
||||
private:
|
||||
// Register signals which indicate when the server should exit.
|
||||
void AddSignals();
|
||||
// For High-Level api deleloper: to set the default server name insteaed of
|
||||
// webcc Added by UnknownObject at 2022-09-04
|
||||
void SetDefaultServerName(std::string server_name);
|
||||
|
||||
// Wait for a signal to stop the server.
|
||||
void AsyncWaitSignals();
|
||||
private:
|
||||
// Register signals which indicate when the server should exit.
|
||||
void AddSignals();
|
||||
|
||||
// Listen on the given port.
|
||||
bool Listen(std::uint16_t port);
|
||||
// Wait for a signal to stop the server.
|
||||
void AsyncWaitSignals();
|
||||
|
||||
// Accept connections asynchronously.
|
||||
void AsyncAccept();
|
||||
// Listen on the given port.
|
||||
bool Listen(std::uint16_t port);
|
||||
|
||||
// Stop acceptor and worker threads, close all pending connections, and
|
||||
// finally stop the event loop.
|
||||
void DoStop();
|
||||
// Accept connections asynchronously.
|
||||
void AsyncAccept();
|
||||
|
||||
// Worker thread routine.
|
||||
void WorkerRoutine();
|
||||
// Stop acceptor and worker threads, close all pending connections, and
|
||||
// finally stop the event loop.
|
||||
void DoStop();
|
||||
|
||||
// Clear pending connections from the queue and stop worker threads.
|
||||
void StopWorkers();
|
||||
// Worker thread routine.
|
||||
void WorkerRoutine();
|
||||
|
||||
// Handle a connection (or more precisely, the request inside it).
|
||||
// Get the request from the connection, process it, prepare the response,
|
||||
// then send the response back to the client.
|
||||
// The connection will keep alive if it's a persistent connection. When next
|
||||
// request comes, this connection will be put back to the queue again.
|
||||
virtual void Handle(ConnectionPtr connection);
|
||||
// Clear pending connections from the queue and stop worker threads.
|
||||
void StopWorkers();
|
||||
|
||||
// Match the view by HTTP method and URL (path).
|
||||
// Return if a view or static file is matched or not.
|
||||
// If the view asks for data streaming, |stream| will be set to true.
|
||||
bool MatchViewOrStatic(const std::string& method, const std::string& url,
|
||||
bool* stream);
|
||||
// Handle a connection (or more precisely, the request inside it).
|
||||
// Get the request from the connection, process it, prepare the response,
|
||||
// then send the response back to the client.
|
||||
// The connection will keep alive if it's a persistent connection. When next
|
||||
// request comes, this connection will be put back to the queue again.
|
||||
virtual void Handle(ConnectionPtr connection);
|
||||
|
||||
// Serve static files from the doc root.
|
||||
ResponsePtr ServeStatic(RequestPtr request);
|
||||
// Match the view by HTTP method and URL (path).
|
||||
// Return if a view or static file is matched or not.
|
||||
// If the view asks for data streaming, |stream| will be set to true.
|
||||
bool MatchViewOrStatic(const std::string& method, const std::string& url,
|
||||
bool* stream, ViewPtr* out_view = nullptr);
|
||||
|
||||
private:
|
||||
// tcp::v4() or tcp::v6()
|
||||
boost::asio::ip::tcp protocol_;
|
||||
// Serve static files from the doc root.
|
||||
ResponsePtr ServeStatic(RequestPtr request);
|
||||
|
||||
// Port number.
|
||||
std::uint16_t port_ = 0;
|
||||
private:
|
||||
// tcp::v4() or tcp::v6()
|
||||
boost::asio::ip::tcp protocol_;
|
||||
|
||||
// The directory with the static files to be served.
|
||||
fs::path doc_root_;
|
||||
// Port number.
|
||||
std::uint16_t port_ = 0;
|
||||
|
||||
// The size of the buffer for reading request.
|
||||
std::size_t buffer_size_ = kBufferSize;
|
||||
// The directory with the static files to be served.
|
||||
fs::path doc_root_;
|
||||
|
||||
// The size of the chunk loaded into memory each time when serving a
|
||||
// static file.
|
||||
std::size_t file_chunk_size_ = 1024;
|
||||
// The size of the buffer for reading request.
|
||||
std::size_t buffer_size_ = kBufferSize;
|
||||
|
||||
// Is the server running?
|
||||
bool running_ = false;
|
||||
// The size of the chunk loaded into memory each time when serving a
|
||||
// static file.
|
||||
std::size_t file_chunk_size_ = 1024;
|
||||
|
||||
// The mutex for guarding the state of the server.
|
||||
std::mutex state_mutex_;
|
||||
// Is the server running?
|
||||
bool running_ = false;
|
||||
|
||||
// The io_context used to perform asynchronous operations.
|
||||
boost::asio::io_context io_context_;
|
||||
// The mutex for guarding the state of the server.
|
||||
std::mutex state_mutex_;
|
||||
|
||||
// Acceptor used to listen for incoming connections.
|
||||
boost::asio::ip::tcp::acceptor acceptor_;
|
||||
// The io_context used to perform asynchronous operations.
|
||||
boost::asio::io_context io_context_;
|
||||
|
||||
// The connection pool which owns all live connections.
|
||||
ConnectionPool pool_;
|
||||
// Acceptor used to listen for incoming connections.
|
||||
boost::asio::ip::tcp::acceptor acceptor_;
|
||||
|
||||
// The signals for processing termination notifications.
|
||||
boost::asio::signal_set signals_;
|
||||
// The connection pool which owns all live connections.
|
||||
ConnectionPool pool_;
|
||||
|
||||
// Worker threads.
|
||||
std::vector<std::thread> worker_threads_;
|
||||
// The signals for processing termination notifications.
|
||||
boost::asio::signal_set signals_;
|
||||
|
||||
// The queue with connection waiting for the workers to process.
|
||||
Queue<ConnectionPtr> queue_;
|
||||
// Worker threads.
|
||||
std::vector<std::thread> worker_threads_;
|
||||
|
||||
// For High-Level api deleloper: to set the default server name insteaed of webcc
|
||||
// Added by UnknownObject at 2022-09-04
|
||||
std::string server_name__;
|
||||
};
|
||||
// The queue with connection waiting for the workers to process.
|
||||
Queue<ConnectionPtr> queue_;
|
||||
|
||||
// For High-Level api deleloper: to set the default server name insteaed of webcc
|
||||
// Added by UnknownObject at 2022-09-04
|
||||
std::string server_name__;
|
||||
};
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
|
||||
@@ -51,6 +51,12 @@ public:
|
||||
port_ = ToString(port);
|
||||
}
|
||||
|
||||
// Force Set
|
||||
void ForceSet_Path(string_view path)
|
||||
{
|
||||
path_ = path;
|
||||
}
|
||||
|
||||
// Append a piece of path.
|
||||
void AppendPath(string_view piece, bool encode = false);
|
||||
|
||||
|
||||
+9
-1
@@ -1,4 +1,4 @@
|
||||
#ifndef WEBCC_VIEW_H_
|
||||
#ifndef WEBCC_VIEW_H_
|
||||
#define WEBCC_VIEW_H_
|
||||
|
||||
#include <memory>
|
||||
@@ -25,6 +25,14 @@ public:
|
||||
virtual bool Stream(const std::string& /*method*/) {
|
||||
return false; // No streaming by default
|
||||
}
|
||||
|
||||
// 【核心新增】:请求头预校验虚函数
|
||||
// 当 Header 刚解析完毕、尚未接收 Body 时触发。
|
||||
// 返回 true 继续接收数据;返回 false 强行 RST 切断连接。
|
||||
virtual bool ValidateHeader(RequestPtr /*request*/)
|
||||
{
|
||||
return true; // 默认不校验,全放行
|
||||
}
|
||||
};
|
||||
|
||||
using ViewPtr = std::shared_ptr<View>;
|
||||
|
||||
Reference in New Issue
Block a user