условно работающие статические файлы и "динамический" контент

This commit is contained in:
Vladislav Ostapov 2024-10-29 15:55:47 +03:00
commit 9502debfee
25 changed files with 13448 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea/
cmake-build-*

55
CMakeLists.txt Normal file
View File

@ -0,0 +1,55 @@
cmake_minimum_required(VERSION 3.28)
project(terminal_web_server)
set(CMAKE_CXX_STANDARD 17)
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Release")
message(STATUS "Build type is release. Optimization for speed, without debug info")
add_compile_options(-Ofast -s)
elseif ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
message(STATUS "Minimal optimization, debug info included")
add_compile_options(-g)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0")
add_definitions(-DUSE_DEBUG)
else()
message(FATAL_ERROR "You must set build type \"Debug\" or \"Release\". Another build types not supported!")
endif()
add_compile_options(-Wall -Wextra -Wsign-conversion)
add_executable(terminal-web-server
src/server/mime_types.hpp
src/server/mime_types.cpp
src/server/request_parser.hpp
src/server/request.hpp
src/server/connection.hpp
src/server/request_parser.cpp
src/server/server.cpp
src/server/header.hpp
src/server/connection_manager.cpp
src/server/connection_manager.hpp
src/server/reply.hpp
src/server/reply.cpp
src/server/connection.cpp
src/main.cpp
src/server/server.hpp
src/server/resource.cpp
src/server/resource.h
)
find_package(Boost 1.53.0 COMPONENTS system thread filesystem url log log_setup REQUIRED)
target_link_libraries(terminal-web-server ${Boost_LIBRARIES})
target_include_directories(terminal-web-server PRIVATE ${Boost_INCLUDE_DIR})
#find_package(OpenSSL)
#if(OPENSSL_FOUND)
# target_compile_definitions(simple-web-server INTERFACE HAVE_OPENSSL)
# target_link_libraries(simple-web-server INTERFACE ${OPENSSL_LIBRARIES})
# target_include_directories(simple-web-server INTERFACE ${OPENSSL_INCLUDE_DIR})
#
# add_executable(https_examples https_examples.cpp)
# target_link_libraries(https_examples simple-web-server)
# target_link_libraries(https_examples ${Boost_LIBRARIES})
# target_include_directories(https_examples PRIVATE ${Boost_INCLUDE_DIR})
#endif()

12
README.md Normal file
View File

@ -0,0 +1,12 @@
# Terminal web server
Сервис, запускаемый на терминале как веб-морда.
# Зависимости
По идее только libboost
```shell
sudo apt-get install libboost-all-dev
```

98
src/main.cpp Normal file
View File

@ -0,0 +1,98 @@
#include <iostream>
#include <string>
#include <sys/prctl.h>
#include <boost/asio.hpp>
#include "server/server.hpp"
#include <boost/log/trivial.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/utility/setup/common_attributes.hpp>
#include <boost/log/utility/setup/console.hpp>
#include <boost/log/utility/setup/file.hpp>
#include <boost/log/utility/setup/formatter_parser.hpp>
namespace mime_types = http::server::mime_types;
void init_logging() {
namespace log = boost::log;
namespace keywords = log::keywords;
namespace expressions = log::expressions;
namespace attributes = log::attributes;
log::register_simple_formatter_factory<log::trivial::severity_level, char>("Severity");
// #ifdef USE_DEBUG
// log::add_console_log(std::clog, keywords::format = "%TimeStamp%: [%Severity%] %Message% [%ThreadID%]");
// #else
// log::add_file_log(
// keywords::file_name = "/home/root/manager_orlik_%N.log",
// keywords::rotation_size = 10 * 1024 * 1024,
// keywords::time_based_rotation = log::sinks::file::rotation_at_time_point(0, 0, 0),
// keywords::format = expressions::format("%1% [%2%] [%3%] <%4%> [%5%]")
// % expressions::format_date_time<boost::posix_time::ptime>("TimeStamp", "%Y-%m-%d, %H:%M:%S.%f")
// % expressions::format_named_scope("Scope", keywords::format = "%n (%f:%l)")
// % expressions::attr<log::trivial::severity_level>("Severity")
// % expressions::message % expressions::attr<attributes::current_thread_id::value_type>("ThreadID"),
// keywords::open_mode = std::ios_base::app,
// keywords::auto_flush = true
// );
// #endif
log::add_console_log(std::clog, keywords::format = "%TimeStamp%: [%Severity%] %Message% [%ThreadID%]");
log::core::get()->set_filter(log::trivial::severity >= log::trivial::info);
log::add_common_attributes();
}
int main(int argc, char *argv[]) {
try {
// Check command line arguments.
if (argc != 3) {
std::cerr << "Usage: http_server <address> <port>\n";
std::cerr << " For IPv4, try:\n";
std::cerr << " receiver 0.0.0.0 80\n";
std::cerr << " For IPv6, try:\n";
std::cerr << " receiver 0::0 80\n";
return 1;
}
prctl(PR_SET_NAME, "main", 0, 0, 0);
init_logging();
boost::log::core::get()->add_thread_attribute("Scope", boost::log::attributes::named_scope());
#ifdef USE_DEBUG
BOOST_LOG_TRIVIAL(info) << "Starting DEBUG " << argv[0];
#else
BOOST_LOG_TRIVIAL(info) << "Starting RELEASE build" << argv[0];
#endif
// Initialise the server.
http::server::server s(argv[1], argv[2]);
s.resources.emplace_back(std::make_unique<http::resource::StaticFileResource>("/", "static/login.html", mime_types::text_html));
s.resources.emplace_back(std::make_unique<http::resource::StaticFileResource>("/favicon.ico", "static/favicon.png", mime_types::image_png));
s.resources.emplace_back(std::make_unique<http::resource::StaticFileResource>("/js/vue.js", "static/js/vue.js", mime_types::javascript));
s.resources.emplace_back(std::make_unique<http::resource::GenericResource>("/api/statistics", [](const auto& req, auto& rep) {
if (req.method != "GET") {
http::server::stock_reply(http::server::bad_request, rep);
}
rep.status = http::server::ok;
rep.headers.clear();
rep.headers.push_back({.name = "Content-Type", .value = to_string(mime_types::json)});
const char* json = R"({"key":"value"})";
rep.content.insert(rep.content.end(), json, json + strlen(json));
}));
// Run the server until stopped.
s.run();
} catch (std::exception &e) {
BOOST_LOG_TRIVIAL(error) << e.what() << std::endl;
return -1;
}
return 0;
}

84
src/server/connection.cpp Normal file
View File

@ -0,0 +1,84 @@
//
// connection.cpp
// ~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "connection.hpp"
#include <utility>
#include <boost/log/trivial.hpp>
#include <boost/beast.hpp>
#include "connection_manager.hpp"
namespace http::server {
connection::connection(boost::asio::ip::tcp::socket socket,
connection_manager &manager, request_handler handler)
: socket_(std::move(socket)),
connection_manager_(manager),
request_handler_(std::move(handler)), request_(), reply_() {
}
void connection::start() {
do_read();
}
void connection::stop() {
socket_.close();
}
void connection::do_read() {
request_parser_.reset();
request_.headers.clear();
request_.method.clear();
request_.uri.clear();
auto self(shared_from_this());
socket_.async_read_some(boost::asio::buffer(buffer_), [this, self](boost::system::error_code ec, std::size_t bytes_transferred) {
if (!ec) {
request_parser::result_type result;
std::tie(result, std::ignore) = request_parser_.parse(
request_, buffer_.data(), buffer_.data() + bytes_transferred);
if (result == request_parser::good) {
request_handler_(request_, reply_);
do_write();
} else if (result == request_parser::bad) {
stock_reply(bad_request, reply_);
do_write();
} else {
do_read();
}
} else {
connection_manager_.stop(shared_from_this());
}
});
}
void connection::do_write() {
reply_.headers.push_back({.name = "Server", .value = "TerminalWebServer v0.1"});
if (!reply_.content.empty()) {
reply_.headers.push_back({.name = "Content-Length", .value = std::to_string(reply_.content.size())});
}
if (request_.http_version_major == 1) {
reply_.headers.push_back({.name = "Connection", .value = "keep-alive"});
}
BOOST_LOG_TRIVIAL(info) << "HTTP query " << reply_.status << " " << request_.method << " " << request_.uri;
auto self(shared_from_this());
async_write(socket_, reply_.to_buffers(), [this, self](boost::system::error_code ec, std::size_t) {
if (!ec) {
// keep alive connection
do_read();
} else {
connection_manager_.stop(shared_from_this());
}
});
}
} // namespace http::server

76
src/server/connection.hpp Normal file
View File

@ -0,0 +1,76 @@
//
// connection.hpp
// ~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#ifndef HTTP_CONNECTION_HPP
#define HTTP_CONNECTION_HPP
#include <boost/asio.hpp>
#include <array>
#include <memory>
#include "reply.hpp"
#include "request.hpp"
#include "request_parser.hpp"
namespace http::server {
using request_handler = std::function<void(const request &req, reply &rep)>;
class connection_manager;
/// Represents a single connection from a client.
class connection
: public std::enable_shared_from_this<connection> {
public:
connection(const connection &) = delete;
connection &operator=(const connection &) = delete;
/// Construct a connection with the given socket.
explicit connection(boost::asio::ip::tcp::socket socket, connection_manager &manager, request_handler handler);
/// Start the first asynchronous operation for the connection.
void start();
/// Stop all asynchronous operations associated with the connection.
void stop();
private:
/// Perform an asynchronous read operation.
void do_read();
/// Perform an asynchronous write operation.
void do_write();
/// Socket for the connection.
boost::asio::ip::tcp::socket socket_;
/// The manager for this connection.
connection_manager &connection_manager_;
/// The handler used to process the incoming request.
request_handler request_handler_;
/// Buffer for incoming data.
std::array<char, 8192> buffer_{};
/// The incoming request.
request request_;
/// The parser for the incoming request.
request_parser request_parser_;
/// The reply to be sent back to the client.
reply reply_;
};
typedef std::shared_ptr<connection> connection_ptr;
} // namespace http::server
#endif // HTTP_CONNECTION_HPP

View File

@ -0,0 +1,32 @@
//
// connection_manager.cpp
// ~~~~~~~~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "connection_manager.hpp"
namespace http::server {
connection_manager::connection_manager() = default;
void connection_manager::start(connection_ptr c) {
connections_.insert(c);
c->start();
}
void connection_manager::stop(connection_ptr c) {
connections_.erase(c);
c->stop();
}
void connection_manager::stop_all() {
for (auto& c: connections_)
c->stop();
connections_.clear();
}
} // namespace http::server

View File

@ -0,0 +1,46 @@
//
// connection_manager.hpp
// ~~~~~~~~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#ifndef HTTP_CONNECTION_MANAGER_HPP
#define HTTP_CONNECTION_MANAGER_HPP
#include <set>
#include "connection.hpp"
namespace http::server {
/// Manages open connections so that they may be cleanly stopped when the server
/// needs to shut down.
class connection_manager {
public:
connection_manager(const connection_manager &) = delete;
connection_manager &operator=(const connection_manager &) = delete;
/// Construct a connection manager.
connection_manager();
/// Add the specified connection to the manager and start it.
void start(connection_ptr c);
/// Stop the specified connection.
void stop(connection_ptr c);
/// Stop all connections.
void stop_all();
private:
/// The managed connections.
std::set<connection_ptr> connections_;
};
} // namespace http::server
#endif // HTTP_CONNECTION_MANAGER_HPP

25
src/server/header.hpp Normal file
View File

@ -0,0 +1,25 @@
//
// header.hpp
// ~~~~~~~~~~
//
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#ifndef HTTP_HEADER_HPP
#define HTTP_HEADER_HPP
#include <string>
namespace http::server {
struct header {
std::string name;
std::string value;
};
} // namespace http::server
#endif // HTTP_HEADER_HPP

55
src/server/mime_types.cpp Normal file
View File

@ -0,0 +1,55 @@
//
// mime_types.cpp
// ~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "mime_types.hpp"
namespace http::server::mime_types {
struct mapping {
const char *extension;
const char *mime_type;
} mappings[] =
{
{"gif", "image/gif"},
{"htm", "text/html"},
{"html", "text/html"},
{"jpg", "image/jpeg"},
{"png", "image/png"}
};
std::string extension_to_type(const std::string &extension) {
for (mapping m: mappings) {
if (m.extension == extension) {
return m.mime_type;
}
}
return "text/plain";
}
} // namespace http::server::mime_types
std::string http::server::mime_types::to_string(Mime m) {
switch (m) {
case image_gif: return "image/gif";
case image_png: return "image/png";
case image_jpeg: return "image/jpeg";
case image_svg: return "image/svg+xml";
case image_webp: return "image/webp";
case image_ico: return "image/vnd.microsoft.icon";
case text_plain: return "text/plain";
case text_html: return "text/html";
case text_css: return "text/css";
case json: return "application/json";
case javascript: return "application/javascript";
case blob:
default:
return "application/octet-stream";
}
}

40
src/server/mime_types.hpp Normal file
View File

@ -0,0 +1,40 @@
//
// mime_types.hpp
// ~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#ifndef HTTP_MIME_TYPES_HPP
#define HTTP_MIME_TYPES_HPP
#include <string>
namespace http::server::mime_types {
enum Mime {
image_gif, // image/gif
image_png, // image/png
image_jpeg, // image/jpeg
image_svg, // image/svg+xml
image_webp, // image/webp
image_ico, // image/vnd.microsoft.icon
text_plain, // text/plain
text_html, // text/html
text_css, // text/css
json, // application/json
javascript, // application/javascript
blob // application/octet-stream
};
std::string to_string(Mime m);
/// Convert a file extension into a MIME type.
std::string extension_to_type(const std::string &extension);
} // namespace http::server::mime_types
#endif // HTTP_MIME_TYPES_HPP

225
src/server/reply.cpp Normal file
View File

@ -0,0 +1,225 @@
//
// reply.cpp
// ~~~~~~~~~
//
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "reply.hpp"
#include <string>
#include "mime_types.hpp"
namespace http::server {
namespace status_strings {
const std::string ok = "HTTP/1.1 200 OK\r\n";
const std::string created = "HTTP/1.1 201 Created\r\n";
const std::string accepted = "HTTP/1.1 202 Accepted\r\n";
const std::string no_content = "HTTP/1.1 204 No Content\r\n";
const std::string multiple_choices = "HTTP/1.1 300 Multiple Choices\r\n";
const std::string moved_permanently = "HTTP/1.1 301 Moved Permanently\r\n";
const std::string moved_temporarily = "HTTP/1.1 302 Moved Temporarily\r\n";
const std::string not_modified = "HTTP/1.1 304 Not Modified\r\n";
const std::string bad_request = "HTTP/1.1 400 Bad Request\r\n";
const std::string unauthorized = "HTTP/1.1 401 Unauthorized\r\n";
const std::string forbidden = "HTTP/1.1 403 Forbidden\r\n";
const std::string not_found = "HTTP/1.1 404 Not Found\r\n";
const std::string internal_server_error = "HTTP/1.1 500 Internal Server Error\r\n";
const std::string not_implemented = "HTTP/1.1 501 Not Implemented\r\n";
const std::string bad_gateway = "HTTP/1.1 502 Bad Gateway\r\n";
const std::string service_unavailable = "HTTP/1.1 503 Service Unavailable\r\n";
boost::asio::const_buffer to_buffer(status_type status) {
switch (status) {
case status_type::ok:
return boost::asio::buffer(ok);
case status_type::created:
return boost::asio::buffer(created);
case status_type::accepted:
return boost::asio::buffer(accepted);
case status_type::no_content:
return boost::asio::buffer(no_content);
case status_type::multiple_choices:
return boost::asio::buffer(multiple_choices);
case status_type::moved_permanently:
return boost::asio::buffer(moved_permanently);
case status_type::moved_temporarily:
return boost::asio::buffer(moved_temporarily);
case status_type::not_modified:
return boost::asio::buffer(not_modified);
case status_type::bad_request:
return boost::asio::buffer(bad_request);
case status_type::unauthorized:
return boost::asio::buffer(unauthorized);
case status_type::forbidden:
return boost::asio::buffer(forbidden);
case status_type::not_found:
return boost::asio::buffer(not_found);
case status_type::internal_server_error:
return boost::asio::buffer(internal_server_error);
case status_type::not_implemented:
return boost::asio::buffer(not_implemented);
case status_type::bad_gateway:
return boost::asio::buffer(bad_gateway);
case status_type::service_unavailable:
return boost::asio::buffer(service_unavailable);
default:
return boost::asio::buffer(internal_server_error);
}
}
} // namespace status_strings
namespace misc_strings {
const char name_value_separator[] = {':', ' '};
const char crlf[] = {'\r', '\n'};
} // namespace misc_strings
std::vector<boost::asio::const_buffer> reply::to_buffers() const {
std::vector<boost::asio::const_buffer> buffers;
buffers.push_back(status_strings::to_buffer(status));
for (const auto & h : headers) {
buffers.emplace_back(boost::asio::buffer(h.name));
buffers.push_back(boost::asio::buffer(misc_strings::name_value_separator));
buffers.emplace_back(boost::asio::buffer(h.value));
buffers.push_back(boost::asio::buffer(misc_strings::crlf));
}
buffers.emplace_back(boost::asio::buffer(misc_strings::crlf));
buffers.emplace_back(boost::asio::buffer(content));
return buffers;
}
namespace stock_replies {
constexpr char ok[] = "";
constexpr char created[] =
"<html>"
"<head><title>Created</title></head>"
"<body><h1>201 Created</h1></body>"
"</html>";
constexpr char accepted[] =
"<html>"
"<head><title>Accepted</title></head>"
"<body><h1>202 Accepted</h1></body>"
"</html>";
constexpr char no_content[] =
"<html>"
"<head><title>No Content</title></head>"
"<body><h1>204 Content</h1></body>"
"</html>";
constexpr char multiple_choices[] =
"<html>"
"<head><title>Multiple Choices</title></head>"
"<body><h1>300 Multiple Choices</h1></body>"
"</html>";
constexpr char moved_permanently[] =
"<html>"
"<head><title>Moved Permanently</title></head>"
"<body><h1>301 Moved Permanently</h1></body>"
"</html>";
constexpr char moved_temporarily[] =
"<html>"
"<head><title>Moved Temporarily</title></head>"
"<body><h1>302 Moved Temporarily</h1></body>"
"</html>";
constexpr char not_modified[] =
"<html>"
"<head><title>Not Modified</title></head>"
"<body><h1>304 Not Modified</h1></body>"
"</html>";
constexpr char bad_request[] =
"<html>"
"<head><title>Bad Request</title></head>"
"<body><h1>400 Bad Request</h1></body>"
"</html>";
constexpr char unauthorized[] =
"<html>"
"<head><title>Unauthorized</title></head>"
"<body><h1>401 Unauthorized</h1></body>"
"</html>";
constexpr char forbidden[] =
"<html>"
"<head><title>Forbidden</title></head>"
"<body><h1>403 Forbidden</h1></body>"
"</html>";
constexpr char not_found[] =
"<html>"
"<head><title>Not Found</title></head>"
"<body><h1>404 Not Found</h1></body>"
"</html>";
constexpr char internal_server_error[] =
"<html>"
"<head><title>Internal Server Error</title></head>"
"<body><h1>500 Internal Server Error</h1></body>"
"</html>";
constexpr char not_implemented[] =
"<html>"
"<head><title>Not Implemented</title></head>"
"<body><h1>501 Not Implemented</h1></body>"
"</html>";
constexpr char bad_gateway[] =
"<html>"
"<head><title>Bad Gateway</title></head>"
"<body><h1>502 Bad Gateway</h1></body>"
"</html>";
constexpr char service_unavailable[] =
"<html>"
"<head><title>Service Unavailable</title></head>"
"<body><h1>503 Service Unavailable</h1></body>"
"</html>";
std::vector<char> as_content(status_type status) {
switch (status) {
case status_type::ok: return {ok, ok + (sizeof(ok) - 1)};
case status_type::created: return {created, created + (sizeof(created) - 1)};
case status_type::accepted: return {accepted, accepted + (sizeof(accepted) - 1)};
case status_type::no_content: return {no_content, no_content + (sizeof(no_content) - 1)};
case status_type::multiple_choices: return {multiple_choices, multiple_choices + (sizeof(multiple_choices) - 1)};
case status_type::moved_permanently: return {moved_permanently, moved_permanently + (sizeof(moved_permanently) - 1)};
case status_type::moved_temporarily: return {moved_temporarily, moved_temporarily + (sizeof(moved_temporarily) - 1)};
case status_type::not_modified: return {not_modified, not_modified + (sizeof(not_modified) - 1)};
case status_type::bad_request: return {bad_request, bad_request + (sizeof(bad_request) - 1)};
case status_type::unauthorized: return {unauthorized, unauthorized + (sizeof(unauthorized) - 1)};
case status_type::forbidden: return {forbidden, forbidden + (sizeof(forbidden) - 1)};
case status_type::not_found: return {not_found, not_found + (sizeof(not_found) - 1)};
case status_type::not_implemented: return {not_implemented, not_implemented + (sizeof(not_implemented) - 1)};
case status_type::bad_gateway: return {bad_gateway, bad_gateway + (sizeof(bad_gateway) - 1)};
case status_type::service_unavailable: return {service_unavailable, service_unavailable + (sizeof(service_unavailable) - 1)};
case status_type::internal_server_error:
default: return {internal_server_error, internal_server_error + (sizeof(internal_server_error) - 1)};
}
}
void as_content(status_type status, std::vector<char>& dest) {
dest.clear();
switch (status) {
case status_type::ok: dest.insert(dest.end(), ok, ok + sizeof(ok) - 1); return;
case status_type::created: dest.insert(dest.end(), created, created + sizeof(created) - 1); return;
case status_type::accepted: dest.insert(dest.end(), accepted, accepted + sizeof(accepted) - 1); return;
case status_type::no_content: dest.insert(dest.end(), no_content, no_content + sizeof(no_content) - 1); return;
case status_type::multiple_choices: dest.insert(dest.end(), multiple_choices, multiple_choices + sizeof(multiple_choices) - 1); return;
case status_type::moved_permanently: dest.insert(dest.end(), moved_permanently, moved_permanently + sizeof(moved_permanently) - 1); return;
case status_type::moved_temporarily: dest.insert(dest.end(), moved_temporarily, moved_temporarily + sizeof(moved_temporarily) - 1); return;
case status_type::not_modified: dest.insert(dest.end(), not_modified, not_modified + sizeof(not_modified) - 1); return;
case status_type::bad_request: dest.insert(dest.end(), bad_request, bad_request + sizeof(bad_request) - 1); return;
case status_type::unauthorized: dest.insert(dest.end(), unauthorized, unauthorized + sizeof(unauthorized) - 1); return;
case status_type::forbidden: dest.insert(dest.end(), forbidden, forbidden + sizeof(forbidden) - 1); return;
case status_type::not_found: dest.insert(dest.end(), not_found, not_found + sizeof(not_found) - 1); return;
case status_type::not_implemented: dest.insert(dest.end(), not_implemented, not_implemented + sizeof(not_implemented) - 1); return;
case status_type::bad_gateway: dest.insert(dest.end(), bad_gateway, bad_gateway + sizeof(bad_gateway) - 1); return;
case status_type::service_unavailable: dest.insert(dest.end(), service_unavailable, service_unavailable + sizeof(service_unavailable) - 1); return;
case status_type::internal_server_error:
default: dest.insert(dest.end(), internal_server_error, internal_server_error + sizeof(internal_server_error) - 1);
}
}
} // namespace stock_replies
void stock_reply(status_type status, reply& rep) {
rep.status = status;
rep.headers.clear();
rep.headers.push_back({.name = "Content-Type", .value = to_string(mime_types::text_html)});
stock_replies::as_content(status, rep.content);
}
} // namespace http::server

63
src/server/reply.hpp Normal file
View File

@ -0,0 +1,63 @@
//
// reply.hpp
// ~~~~~~~~~
//
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#ifndef HTTP_REPLY_HPP
#define HTTP_REPLY_HPP
#include <string>
#include <vector>
#include <boost/asio.hpp>
#include "header.hpp"
namespace http::server {
/// The status of the reply.
enum status_type {
ok = 200,
created = 201,
accepted = 202,
no_content = 204,
multiple_choices = 300,
moved_permanently = 301,
moved_temporarily = 302,
not_modified = 304,
bad_request = 400,
unauthorized = 401,
forbidden = 403,
not_found = 404,
internal_server_error = 500,
not_implemented = 501,
bad_gateway = 502,
service_unavailable = 503
};
/// A reply to be sent to a client.
struct reply {
status_type status;
/// The headers to be included in the reply.
std::vector<header> headers;
/// The content to be sent in the reply.
std::vector<char> content;
/// Convert the reply into a vector of buffers. The buffers do not own the
/// underlying memory blocks, therefore the reply object must remain valid and
/// not be changed until the write operation has completed.
std::vector<boost::asio::const_buffer> to_buffers() const;
};
/// Get a stock reply.
void stock_reply(status_type status, reply& rep);
} // namespace http::server
#endif // HTTP_REPLY_HPP

32
src/server/request.hpp Normal file
View File

@ -0,0 +1,32 @@
//
// request.hpp
// ~~~~~~~~~~~
//
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#ifndef HTTP_REQUEST_HPP
#define HTTP_REQUEST_HPP
#include <string>
#include <vector>
#include "header.hpp"
namespace http::server {
/// A request received from a client.
struct request {
std::string method;
std::string uri;
bool is_keep_alive;
int http_version_major;
int http_version_minor;
std::vector<header> headers;
};
} // namespace http::server
#endif // HTTP_REQUEST_HPP

View File

@ -0,0 +1,241 @@
//
// request_parser.cpp
// ~~~~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "request_parser.hpp"
#include "request.hpp"
namespace http::server {
request_parser::request_parser()
: state_(method_start) {
}
void request_parser::reset() {
state_ = method_start;
}
request_parser::result_type request_parser::consume(request &req, char input) {
switch (state_) {
case method_start:
if (!is_char(input) || is_ctl(input) || is_tspecial(input)) {
return bad;
} else {
state_ = method;
req.method.push_back(input);
return indeterminate;
}
case method:
if (input == ' ') {
state_ = uri;
return indeterminate;
} else if (!is_char(input) || is_ctl(input) || is_tspecial(input)) {
return bad;
} else {
req.method.push_back(input);
return indeterminate;
}
case uri:
if (input == ' ') {
state_ = http_version_h;
return indeterminate;
} else if (is_ctl(input)) {
return bad;
} else {
req.uri.push_back(input);
return indeterminate;
}
case http_version_h:
if (input == 'H') {
state_ = http_version_t_1;
return indeterminate;
} else {
return bad;
}
case http_version_t_1:
if (input == 'T') {
state_ = http_version_t_2;
return indeterminate;
} else {
return bad;
}
case http_version_t_2:
if (input == 'T') {
state_ = http_version_p;
return indeterminate;
} else {
return bad;
}
case http_version_p:
if (input == 'P') {
state_ = http_version_slash;
return indeterminate;
} else {
return bad;
}
case http_version_slash:
if (input == '/') {
req.http_version_major = 0;
req.http_version_minor = 0;
state_ = http_version_major_start;
return indeterminate;
} else {
return bad;
}
case http_version_major_start:
if (is_digit(input)) {
req.http_version_major = req.http_version_major * 10 + input - '0';
state_ = http_version_major;
return indeterminate;
} else {
return bad;
}
case http_version_major:
if (input == '.') {
state_ = http_version_minor_start;
return indeterminate;
} else if (is_digit(input)) {
req.http_version_major = req.http_version_major * 10 + input - '0';
return indeterminate;
} else {
return bad;
}
case http_version_minor_start:
if (is_digit(input)) {
req.http_version_minor = req.http_version_minor * 10 + input - '0';
state_ = http_version_minor;
return indeterminate;
} else {
return bad;
}
case http_version_minor:
if (input == '\r') {
state_ = expecting_newline_1;
return indeterminate;
} else if (is_digit(input)) {
req.http_version_minor = req.http_version_minor * 10 + input - '0';
return indeterminate;
} else {
return bad;
}
case expecting_newline_1:
if (input == '\n') {
state_ = header_line_start;
return indeterminate;
} else {
return bad;
}
case header_line_start:
if (input == '\r') {
state_ = expecting_newline_3;
return indeterminate;
} else if (!req.headers.empty() && (input == ' ' || input == '\t')) {
state_ = header_lws;
return indeterminate;
} else if (!is_char(input) || is_ctl(input) || is_tspecial(input)) {
return bad;
} else {
req.headers.push_back(header());
req.headers.back().name.push_back(input);
state_ = header_name;
return indeterminate;
}
case header_lws:
if (input == '\r') {
state_ = expecting_newline_2;
return indeterminate;
} else if (input == ' ' || input == '\t') {
return indeterminate;
} else if (is_ctl(input)) {
return bad;
} else {
state_ = header_value;
req.headers.back().value.push_back(input);
return indeterminate;
}
case header_name:
if (input == ':') {
state_ = space_before_header_value;
return indeterminate;
} else if (!is_char(input) || is_ctl(input) || is_tspecial(input)) {
return bad;
} else {
req.headers.back().name.push_back(input);
return indeterminate;
}
case space_before_header_value:
if (input == ' ') {
state_ = header_value;
return indeterminate;
} else {
return bad;
}
case header_value:
if (input == '\r') {
state_ = expecting_newline_2;
return indeterminate;
} else if (is_ctl(input)) {
return bad;
} else {
req.headers.back().value.push_back(input);
return indeterminate;
}
case expecting_newline_2:
if (input == '\n') {
state_ = header_line_start;
return indeterminate;
} else {
return bad;
}
case expecting_newline_3:
return (input == '\n') ? good : bad;
default:
return bad;
}
}
bool request_parser::is_char(int c) {
return c >= 0 && c <= 127;
}
bool request_parser::is_ctl(int c) {
return (c >= 0 && c <= 31) || (c == 127);
}
bool request_parser::is_tspecial(int c) {
switch (c) {
case '(':
case ')':
case '<':
case '>':
case '@':
case ',':
case ';':
case ':':
case '\\':
case '"':
case '/':
case '[':
case ']':
case '?':
case '=':
case '{':
case '}':
case ' ':
case '\t':
return true;
default:
return false;
}
}
bool request_parser::is_digit(int c) {
return c >= '0' && c <= '9';
}
} // namespace http::server

View File

@ -0,0 +1,92 @@
//
// request_parser.hpp
// ~~~~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#ifndef HTTP_REQUEST_PARSER_HPP
#define HTTP_REQUEST_PARSER_HPP
#include <tuple>
#include <boost/algorithm/string.hpp>
#include "request.hpp"
namespace http::server {
struct request;
/// Parser for incoming requests.
class request_parser {
public:
/// Construct ready to parse the request method.
request_parser();
/// Reset to initial parser state.
void reset();
/// Result of parse.
enum result_type { good, bad, indeterminate };
/// Parse some data. The enum return value is good when a complete request has
/// been parsed, bad if the data is invalid, indeterminate when more data is
/// required. The InputIterator return value indicates how much of the input
/// has been consumed.
template<typename InputIterator>
std::tuple<result_type, InputIterator> parse(request &req, InputIterator begin, InputIterator end) {
while (begin != end) {
result_type result = consume(req, *begin++);
if (result == good || result == bad)
return std::make_tuple(result, begin);
}
return std::make_tuple(indeterminate, begin);
}
private:
/// Handle the next character of input.
result_type consume(request &req, char input);
/// Check if a byte is an HTTP character.
static bool is_char(int c);
/// Check if a byte is an HTTP control character.
static bool is_ctl(int c);
/// Check if a byte is defined as an HTTP tspecial character.
static bool is_tspecial(int c);
/// Check if a byte is a digit.
static bool is_digit(int c);
/// The current state of the parser.
enum state {
method_start,
method,
uri,
http_version_h,
http_version_t_1,
http_version_t_2,
http_version_p,
http_version_slash,
http_version_major_start,
http_version_major,
http_version_minor_start,
http_version_minor,
expecting_newline_1,
header_line_start,
header_lws,
header_name,
space_before_header_value,
header_value,
expecting_newline_2,
expecting_newline_3
} state_;
};
} // namespace http::server
#endif // HTTP_REQUEST_PARSER_HPP

66
src/server/resource.cpp Normal file
View File

@ -0,0 +1,66 @@
#include "resource.h"
#include <boost/log/trivial.hpp>
#include <boost/asio/buffer.hpp>
#include <fstream>
static void loadFile(const std::string& path, std::vector<char>& content) {
std::ifstream is(path, std::ios::in | std::ios::binary);
if (!is) {
throw std::runtime_error("File not found");
}
content.clear();
for (;;) {
char buf[512];
auto len = is.read(buf, sizeof(buf)).gcount();
if (len <= 0) {
break;
}
content.insert(content.end(), buf, buf + len);
}
}
http::resource::StaticFileResource::StaticFileResource(const std::string &path, const std::string &filePath, server::mime_types::Mime type): type(type) {
this->path = path;
#ifdef USE_DEBUG
BOOST_LOG_TRIVIAL(info) << "Skip loading file " << filePath << " (http path: " << path << ")";
this->filePath = filePath;
#else
BOOST_LOG_TRIVIAL(info) << "Load file " << filePath << " (http path: " << path << ")";
loadFile(filePath, this->content);
#endif
}
void http::resource::StaticFileResource::handle(const server::request &req, server::reply &rep) {
if (req.method != "GET") {
stock_reply(server::bad_request, rep);
return;
}
// TODO сделать поддержку range
#ifdef USE_DEBUG
BOOST_LOG_TRIVIAL(debug) << "Reload file " << filePath << " (http path: " << path << ")";
loadFile(this->filePath, rep.content);
#else
rep.content.clear();
rep.content.insert(rep.content.end(), this->content.begin(), this->content.end());
#endif
rep.status = server::ok;
rep.headers.clear();
// TODO сделать cache control
rep.headers.push_back({.name = "Content-Type", .value = to_string(this->type)});
}
http::resource::StaticFileResource::~StaticFileResource() = default;
http::resource::GenericResource::GenericResource(const std::string &path, const respGenerator &generator): generator_(generator) {
this->path = path;
}
void http::resource::GenericResource::handle(const server::request &req, server::reply &rep) {
this->generator_(req, rep);
}
http::resource::GenericResource::~GenericResource() = default;

60
src/server/resource.h Normal file
View File

@ -0,0 +1,60 @@
#ifndef RESOURCE_H
#define RESOURCE_H
#include <string>
#include "mime_types.hpp"
#include "reply.hpp"
#include "request.hpp"
namespace http::resource {
/**
* Абстрактный ресурс
*/
class BasicResource {
public:
std::string path;
virtual void handle(const server::request &req, server::reply &rep) = 0;
virtual ~BasicResource() = default;
};
/**
* Класс ресурса статического файла
*/
class StaticFileResource final : public BasicResource {
private:
server::mime_types::Mime type;
#ifdef USE_DEBUG
std::string filePath;
#else
std::vector<char> content;
#endif
public:
StaticFileResource(const std::string& path, const std::string& filePath, server::mime_types::Mime type);
void handle(const server::request &req, server::reply &rep) override;
~StaticFileResource() override;
};
using respGenerator = std::function<void(const server::request &req, server::reply &rep)>;
/**
* Класс ресурса для POST-запросов
*/
class GenericResource final : public BasicResource {
private:
respGenerator generator_;
public:
GenericResource(const std::string& path, const respGenerator& generator);
void handle(const server::request &req, server::reply &rep) override;
~GenericResource() override;
};
}
#endif //RESOURCE_H

95
src/server/server.cpp Normal file
View File

@ -0,0 +1,95 @@
#include "server.hpp"
#include <utility>
#include <boost/url/url_view.hpp>
namespace http::server {
server::server(const std::string &address, const std::string &port)
: io_context_(1), signals_(io_context_), acceptor_(io_context_) {
// Register to handle the signals that indicate when the server should exit.
// It is safe to register for the same signal multiple times in a program,
// provided all registration for the specified signal is made through Asio.
signals_.add(SIGINT);
signals_.add(SIGTERM);
#if defined(SIGQUIT)
signals_.add(SIGQUIT);
#endif // defined(SIGQUIT)
do_await_stop();
// Open the acceptor with the option to reuse the address (i.e. SO_REUSEADDR).
boost::asio::ip::tcp::resolver resolver(io_context_);
boost::asio::ip::tcp::endpoint endpoint =
*resolver.resolve(address, port).begin();
acceptor_.open(endpoint.protocol());
acceptor_.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
acceptor_.bind(endpoint);
acceptor_.listen(128);
do_accept();
}
void server::run() {
// 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.
io_context_.run();
}
void server::do_accept() {
acceptor_.async_accept(
[this](boost::system::error_code ec, boost::asio::ip::tcp::socket socket) {
// 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) {
connection_manager_.start(std::make_shared<connection>(
std::move(socket), connection_manager_, [this](const auto& req, auto& rep) { this->handle_request(req, rep); }));
}
do_accept();
});
}
void server::do_await_stop() {
signals_.async_wait(
[this](boost::system::error_code /*ec*/, int /*signo*/) {
// The server is stopped by cancelling all outstanding asynchronous
// operations. Once all operations have finished the io_context::run()
// call will exit.
acceptor_.close();
connection_manager_.stop_all();
});
}
void server::handle_request(const request &req, reply &rep) {
boost::urls::url_view url(req.uri);
const auto path = url.path();
// Request path must be absolute and not contain "..".
if (path.empty() || path[0] != '/' || path.find("..") != std::string::npos) {
stock_reply(bad_request, rep);
return;
}
rep.status = ok;
rep.headers.clear();
rep.content.clear();
for (auto& res: resources) {
if (res->path != path) {
continue;
}
res->handle(req, rep);
return;
}
stock_reply(not_found, rep);
}
} // namespace http::server

51
src/server/server.hpp Normal file
View File

@ -0,0 +1,51 @@
#ifndef HTTP_SERVER_HPP
#define HTTP_SERVER_HPP
#include <boost/asio.hpp>
#include <string>
#include "connection_manager.hpp"
#include "resource.h"
namespace http::server {
/// The top-level class of the HTTP server.
class server {
public:
server(const server &) = delete;
server &operator=(const server &) = delete;
/// Construct the server to listen on the specified TCP address and port
explicit server(const std::string &address, const std::string &port);
std::vector<std::unique_ptr<resource::BasicResource>> resources;
/// Run the server's io_context loop.
void run();
private:
/// Perform an asynchronous accept operation.
void do_accept();
/// Wait for a request to stop the server.
void do_await_stop();
/// The io_context used to perform asynchronous operations.
boost::asio::io_context io_context_;
/// The signal_set is used to register for process termination notifications.
boost::asio::signal_set signals_;
/// Acceptor used to listen for incoming connections.
boost::asio::ip::tcp::acceptor acceptor_;
/// The connection manager which owns all live connections.
connection_manager connection_manager_;
/// Handle a request and produce a reply.
void handle_request(const request &req, reply &rep);
};
} // namespace http::server
#endif // HTTP_SERVER_HPP

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,23 @@
import { ref } from 'vue'
export default {
el: '#status-header',
data: {
message: 'Hello Vue!',
now: new Date()
},
methods: {
updateDate() {
this.now = new Date();
}
},
mounted() {
setInterval(() => {
this.updateDate();
}, 1000);
},
setup() {
const count = ref(0)
return { count }
},
template: `<div>Счётчик: {{ count }}</div>`
}

11932
static/js/vue.js Normal file

File diff suppressed because it is too large Load Diff

42
static/login.html Normal file
View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<header>
<div id="app">
<h1>{{ message }}</h1>
<p>{{ now }}</p>
</div>
<div id="status-header"></div>
<!-- Версия для разработки включает в себя возможность вывода в консоль полезных уведомлений -->
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!',
now: new Date()
},
methods: {
updateDate() {
this.now = new Date();
}
},
mounted() {
setInterval(() => {
this.updateDate();
}, 1000);
}
})
// import MyComponent from './modules/header'
// const sh = new Vue(MyComponent)
</script>
</header>
</body>
</html>

0
static/style.css Normal file
View File