условно работающие статические файлы и "динамический" контент
This commit is contained in:
commit
9502debfee
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.idea/
|
||||||
|
cmake-build-*
|
||||||
|
|
55
CMakeLists.txt
Normal file
55
CMakeLists.txt
Normal 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
12
README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Terminal web server
|
||||||
|
|
||||||
|
Сервис, запускаемый на терминале как веб-морда.
|
||||||
|
|
||||||
|
# Зависимости
|
||||||
|
|
||||||
|
По идее только libboost
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo apt-get install libboost-all-dev
|
||||||
|
```
|
||||||
|
|
98
src/main.cpp
Normal file
98
src/main.cpp
Normal 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
84
src/server/connection.cpp
Normal 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
76
src/server/connection.hpp
Normal 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
|
32
src/server/connection_manager.cpp
Normal file
32
src/server/connection_manager.cpp
Normal 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
|
46
src/server/connection_manager.hpp
Normal file
46
src/server/connection_manager.hpp
Normal 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
25
src/server/header.hpp
Normal 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
55
src/server/mime_types.cpp
Normal 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
40
src/server/mime_types.hpp
Normal 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
225
src/server/reply.cpp
Normal 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
63
src/server/reply.hpp
Normal 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
32
src/server/request.hpp
Normal 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
|
241
src/server/request_parser.cpp
Normal file
241
src/server/request_parser.cpp
Normal 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
|
92
src/server/request_parser.hpp
Normal file
92
src/server/request_parser.hpp
Normal 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
66
src/server/resource.cpp
Normal 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
60
src/server/resource.h
Normal 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
95
src/server/server.cpp
Normal 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
51
src/server/server.hpp
Normal 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
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
23
static/js/modules/header.js
Normal file
23
static/js/modules/header.js
Normal 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
11932
static/js/vue.js
Normal file
File diff suppressed because it is too large
Load Diff
42
static/login.html
Normal file
42
static/login.html
Normal 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
0
static/style.css
Normal file
Loading…
x
Reference in New Issue
Block a user