commit 9502debfee80ca4137528dfeb66a679663fe06bf Author: Vladislav Ostapov Date: Tue Oct 29 15:55:47 2024 +0300 условно работающие статические файлы и "динамический" контент diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af3eb46 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +cmake-build-* + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3d613bc --- /dev/null +++ b/CMakeLists.txt @@ -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() + diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c9eeaa --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Terminal web server + +Сервис, запускаемый на терминале как веб-морда. + +# Зависимости + +По идее только libboost + +```shell +sudo apt-get install libboost-all-dev +``` + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..2f6426e --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,98 @@ +#include +#include +#include +#include +#include "server/server.hpp" +#include +#include +#include +#include +#include +#include + + +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("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("TimeStamp", "%Y-%m-%d, %H:%M:%S.%f") +// % expressions::format_named_scope("Scope", keywords::format = "%n (%f:%l)") +// % expressions::attr("Severity") +// % expressions::message % expressions::attr("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
\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("/", "static/login.html", mime_types::text_html)); + s.resources.emplace_back(std::make_unique("/favicon.ico", "static/favicon.png", mime_types::image_png)); + s.resources.emplace_back(std::make_unique("/js/vue.js", "static/js/vue.js", mime_types::javascript)); + + s.resources.emplace_back(std::make_unique("/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; +} + + diff --git a/src/server/connection.cpp b/src/server/connection.cpp new file mode 100644 index 0000000..241ded4 --- /dev/null +++ b/src/server/connection.cpp @@ -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 +#include +#include + +#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 diff --git a/src/server/connection.hpp b/src/server/connection.hpp new file mode 100644 index 0000000..29328e1 --- /dev/null +++ b/src/server/connection.hpp @@ -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 +#include +#include +#include "reply.hpp" +#include "request.hpp" +#include "request_parser.hpp" + + +namespace http::server { + using request_handler = std::function; + + class connection_manager; + /// Represents a single connection from a client. + class connection + : public std::enable_shared_from_this { + 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 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_ptr; +} // namespace http::server + + +#endif // HTTP_CONNECTION_HPP diff --git a/src/server/connection_manager.cpp b/src/server/connection_manager.cpp new file mode 100644 index 0000000..9bacaeb --- /dev/null +++ b/src/server/connection_manager.cpp @@ -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 diff --git a/src/server/connection_manager.hpp b/src/server/connection_manager.hpp new file mode 100644 index 0000000..838bdd8 --- /dev/null +++ b/src/server/connection_manager.hpp @@ -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 +#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 connections_; + }; +} // namespace http::server + + +#endif // HTTP_CONNECTION_MANAGER_HPP diff --git a/src/server/header.hpp b/src/server/header.hpp new file mode 100644 index 0000000..53f961b --- /dev/null +++ b/src/server/header.hpp @@ -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 + + +namespace http::server { + struct header { + std::string name; + std::string value; + }; +} // namespace http::server + + +#endif // HTTP_HEADER_HPP diff --git a/src/server/mime_types.cpp b/src/server/mime_types.cpp new file mode 100644 index 0000000..84bc7a3 --- /dev/null +++ b/src/server/mime_types.cpp @@ -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"; + } +} diff --git a/src/server/mime_types.hpp b/src/server/mime_types.hpp new file mode 100644 index 0000000..70c0678 --- /dev/null +++ b/src/server/mime_types.hpp @@ -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 + + +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 diff --git a/src/server/reply.cpp b/src/server/reply.cpp new file mode 100644 index 0000000..23e99c7 --- /dev/null +++ b/src/server/reply.cpp @@ -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 + +#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 reply::to_buffers() const { + std::vector 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[] = + "" + "Created" + "

201 Created

" + ""; + constexpr char accepted[] = + "" + "Accepted" + "

202 Accepted

" + ""; + constexpr char no_content[] = + "" + "No Content" + "

204 Content

" + ""; + constexpr char multiple_choices[] = + "" + "Multiple Choices" + "

300 Multiple Choices

" + ""; + constexpr char moved_permanently[] = + "" + "Moved Permanently" + "

301 Moved Permanently

" + ""; + constexpr char moved_temporarily[] = + "" + "Moved Temporarily" + "

302 Moved Temporarily

" + ""; + constexpr char not_modified[] = + "" + "Not Modified" + "

304 Not Modified

" + ""; + constexpr char bad_request[] = + "" + "Bad Request" + "

400 Bad Request

" + ""; + constexpr char unauthorized[] = + "" + "Unauthorized" + "

401 Unauthorized

" + ""; + constexpr char forbidden[] = + "" + "Forbidden" + "

403 Forbidden

" + ""; + constexpr char not_found[] = + "" + "Not Found" + "

404 Not Found

" + ""; + constexpr char internal_server_error[] = + "" + "Internal Server Error" + "

500 Internal Server Error

" + ""; + constexpr char not_implemented[] = + "" + "Not Implemented" + "

501 Not Implemented

" + ""; + constexpr char bad_gateway[] = + "" + "Bad Gateway" + "

502 Bad Gateway

" + ""; + constexpr char service_unavailable[] = + "" + "Service Unavailable" + "

503 Service Unavailable

" + ""; + + std::vector 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& 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 diff --git a/src/server/reply.hpp b/src/server/reply.hpp new file mode 100644 index 0000000..74bc90b --- /dev/null +++ b/src/server/reply.hpp @@ -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 +#include +#include +#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
headers; + + /// The content to be sent in the reply. + std::vector 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 to_buffers() const; + }; + + /// Get a stock reply. + void stock_reply(status_type status, reply& rep); + +} // namespace http::server + + +#endif // HTTP_REPLY_HPP diff --git a/src/server/request.hpp b/src/server/request.hpp new file mode 100644 index 0000000..a6b0622 --- /dev/null +++ b/src/server/request.hpp @@ -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 +#include +#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
headers; + }; +} // namespace http::server + + +#endif // HTTP_REQUEST_HPP diff --git a/src/server/request_parser.cpp b/src/server/request_parser.cpp new file mode 100644 index 0000000..edeb32c --- /dev/null +++ b/src/server/request_parser.cpp @@ -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 diff --git a/src/server/request_parser.hpp b/src/server/request_parser.hpp new file mode 100644 index 0000000..1d7fb53 --- /dev/null +++ b/src/server/request_parser.hpp @@ -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 +#include + +#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 + std::tuple 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 diff --git a/src/server/resource.cpp b/src/server/resource.cpp new file mode 100644 index 0000000..b565634 --- /dev/null +++ b/src/server/resource.cpp @@ -0,0 +1,66 @@ +#include "resource.h" +#include +#include +#include + +static void loadFile(const std::string& path, std::vector& 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; + + diff --git a/src/server/resource.h b/src/server/resource.h new file mode 100644 index 0000000..5a60663 --- /dev/null +++ b/src/server/resource.h @@ -0,0 +1,60 @@ +#ifndef RESOURCE_H +#define RESOURCE_H +#include +#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 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; + + /** + * Класс ресурса для 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 diff --git a/src/server/server.cpp b/src/server/server.cpp new file mode 100644 index 0000000..d46d52e --- /dev/null +++ b/src/server/server.cpp @@ -0,0 +1,95 @@ +#include "server.hpp" +#include +#include + + +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( + 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 diff --git a/src/server/server.hpp b/src/server/server.hpp new file mode 100644 index 0000000..ba3c1e0 --- /dev/null +++ b/src/server/server.hpp @@ -0,0 +1,51 @@ +#ifndef HTTP_SERVER_HPP +#define HTTP_SERVER_HPP + +#include +#include +#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> 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 diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000..f9081bf Binary files /dev/null and b/static/favicon.png differ diff --git a/static/js/modules/header.js b/static/js/modules/header.js new file mode 100644 index 0000000..370aa51 --- /dev/null +++ b/static/js/modules/header.js @@ -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: `
Счётчик: {{ count }}
` +} \ No newline at end of file diff --git a/static/js/vue.js b/static/js/vue.js new file mode 100644 index 0000000..bafa6b2 --- /dev/null +++ b/static/js/vue.js @@ -0,0 +1,11932 @@ +/*! + * Vue.js v2.7.16 + * (c) 2014-2023 Evan You + * Released under the MIT License. + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Vue = factory()); +})(this, (function () { 'use strict'; + + var emptyObject = Object.freeze({}); + var isArray = Array.isArray; + // These helpers produce better VM code in JS engines due to their + // explicitness and function inlining. + function isUndef(v) { + return v === undefined || v === null; + } + function isDef(v) { + return v !== undefined && v !== null; + } + function isTrue(v) { + return v === true; + } + function isFalse(v) { + return v === false; + } + /** + * Check if value is primitive. + */ + function isPrimitive(value) { + return (typeof value === 'string' || + typeof value === 'number' || + // $flow-disable-line + typeof value === 'symbol' || + typeof value === 'boolean'); + } + function isFunction(value) { + return typeof value === 'function'; + } + /** + * Quick object check - this is primarily used to tell + * objects from primitive values when we know the value + * is a JSON-compliant type. + */ + function isObject(obj) { + return obj !== null && typeof obj === 'object'; + } + /** + * Get the raw type string of a value, e.g., [object Object]. + */ + var _toString = Object.prototype.toString; + function toRawType(value) { + return _toString.call(value).slice(8, -1); + } + /** + * Strict object type check. Only returns true + * for plain JavaScript objects. + */ + function isPlainObject(obj) { + return _toString.call(obj) === '[object Object]'; + } + function isRegExp(v) { + return _toString.call(v) === '[object RegExp]'; + } + /** + * Check if val is a valid array index. + */ + function isValidArrayIndex(val) { + var n = parseFloat(String(val)); + return n >= 0 && Math.floor(n) === n && isFinite(val); + } + function isPromise(val) { + return (isDef(val) && + typeof val.then === 'function' && + typeof val.catch === 'function'); + } + /** + * Convert a value to a string that is actually rendered. + */ + function toString(val) { + return val == null + ? '' + : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) + ? JSON.stringify(val, replacer, 2) + : String(val); + } + function replacer(_key, val) { + // avoid circular deps from v3 + if (val && val.__v_isRef) { + return val.value; + } + return val; + } + /** + * Convert an input value to a number for persistence. + * If the conversion fails, return original string. + */ + function toNumber(val) { + var n = parseFloat(val); + return isNaN(n) ? val : n; + } + /** + * Make a map and return a function for checking if a key + * is in that map. + */ + function makeMap(str, expectsLowerCase) { + var map = Object.create(null); + var list = str.split(','); + for (var i = 0; i < list.length; i++) { + map[list[i]] = true; + } + return expectsLowerCase ? function (val) { return map[val.toLowerCase()]; } : function (val) { return map[val]; }; + } + /** + * Check if a tag is a built-in tag. + */ + var isBuiltInTag = makeMap('slot,component', true); + /** + * Check if an attribute is a reserved attribute. + */ + var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); + /** + * Remove an item from an array. + */ + function remove$2(arr, item) { + var len = arr.length; + if (len) { + // fast path for the only / last item + if (item === arr[len - 1]) { + arr.length = len - 1; + return; + } + var index = arr.indexOf(item); + if (index > -1) { + return arr.splice(index, 1); + } + } + } + /** + * Check whether an object has the property. + */ + var hasOwnProperty = Object.prototype.hasOwnProperty; + function hasOwn(obj, key) { + return hasOwnProperty.call(obj, key); + } + /** + * Create a cached version of a pure function. + */ + function cached(fn) { + var cache = Object.create(null); + return function cachedFn(str) { + var hit = cache[str]; + return hit || (cache[str] = fn(str)); + }; + } + /** + * Camelize a hyphen-delimited string. + */ + var camelizeRE = /-(\w)/g; + var camelize = cached(function (str) { + return str.replace(camelizeRE, function (_, c) { return (c ? c.toUpperCase() : ''); }); + }); + /** + * Capitalize a string. + */ + var capitalize = cached(function (str) { + return str.charAt(0).toUpperCase() + str.slice(1); + }); + /** + * Hyphenate a camelCase string. + */ + var hyphenateRE = /\B([A-Z])/g; + var hyphenate = cached(function (str) { + return str.replace(hyphenateRE, '-$1').toLowerCase(); + }); + /** + * Simple bind polyfill for environments that do not support it, + * e.g., PhantomJS 1.x. Technically, we don't need this anymore + * since native bind is now performant enough in most browsers. + * But removing it would mean breaking code that was able to run in + * PhantomJS 1.x, so this must be kept for backward compatibility. + */ + /* istanbul ignore next */ + function polyfillBind(fn, ctx) { + function boundFn(a) { + var l = arguments.length; + return l + ? l > 1 + ? fn.apply(ctx, arguments) + : fn.call(ctx, a) + : fn.call(ctx); + } + boundFn._length = fn.length; + return boundFn; + } + function nativeBind(fn, ctx) { + return fn.bind(ctx); + } + // @ts-expect-error bind cannot be `undefined` + var bind$1 = Function.prototype.bind ? nativeBind : polyfillBind; + /** + * Convert an Array-like object to a real Array. + */ + function toArray(list, start) { + start = start || 0; + var i = list.length - start; + var ret = new Array(i); + while (i--) { + ret[i] = list[i + start]; + } + return ret; + } + /** + * Mix properties into target object. + */ + function extend(to, _from) { + for (var key in _from) { + to[key] = _from[key]; + } + return to; + } + /** + * Merge an Array of Objects into a single Object. + */ + function toObject(arr) { + var res = {}; + for (var i = 0; i < arr.length; i++) { + if (arr[i]) { + extend(res, arr[i]); + } + } + return res; + } + /* eslint-disable no-unused-vars */ + /** + * Perform no operation. + * Stubbing args to make Flow happy without leaving useless transpiled code + * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/). + */ + function noop(a, b, c) { } + /** + * Always return false. + */ + var no = function (a, b, c) { return false; }; + /* eslint-enable no-unused-vars */ + /** + * Return the same value. + */ + var identity = function (_) { return _; }; + /** + * Generate a string containing static keys from compiler modules. + */ + function genStaticKeys$1(modules) { + return modules + .reduce(function (keys, m) { return keys.concat(m.staticKeys || []); }, []) + .join(','); + } + /** + * Check if two values are loosely equal - that is, + * if they are plain objects, do they have the same shape? + */ + function looseEqual(a, b) { + if (a === b) + return true; + var isObjectA = isObject(a); + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + var isArrayA = Array.isArray(a); + var isArrayB = Array.isArray(b); + if (isArrayA && isArrayB) { + return (a.length === b.length && + a.every(function (e, i) { + return looseEqual(e, b[i]); + })); + } + else if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime(); + } + else if (!isArrayA && !isArrayB) { + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return (keysA.length === keysB.length && + keysA.every(function (key) { + return looseEqual(a[key], b[key]); + })); + } + else { + /* istanbul ignore next */ + return false; + } + } + catch (e) { + /* istanbul ignore next */ + return false; + } + } + else if (!isObjectA && !isObjectB) { + return String(a) === String(b); + } + else { + return false; + } + } + /** + * Return the first index at which a loosely equal value can be + * found in the array (if value is a plain object, the array must + * contain an object of the same shape), or -1 if it is not present. + */ + function looseIndexOf(arr, val) { + for (var i = 0; i < arr.length; i++) { + if (looseEqual(arr[i], val)) + return i; + } + return -1; + } + /** + * Ensure a function is called only once. + */ + function once(fn) { + var called = false; + return function () { + if (!called) { + called = true; + fn.apply(this, arguments); + } + }; + } + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#polyfill + function hasChanged(x, y) { + if (x === y) { + return x === 0 && 1 / x !== 1 / y; + } + else { + return x === x || y === y; + } + } + + var SSR_ATTR = 'data-server-rendered'; + var ASSET_TYPES = ['component', 'directive', 'filter']; + var LIFECYCLE_HOOKS = [ + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'beforeDestroy', + 'destroyed', + 'activated', + 'deactivated', + 'errorCaptured', + 'serverPrefetch', + 'renderTracked', + 'renderTriggered' + ]; + + var config = { + /** + * Option merge strategies (used in core/util/options) + */ + // $flow-disable-line + optionMergeStrategies: Object.create(null), + /** + * Whether to suppress warnings. + */ + silent: false, + /** + * Show production mode tip message on boot? + */ + productionTip: true, + /** + * Whether to enable devtools + */ + devtools: true, + /** + * Whether to record perf + */ + performance: false, + /** + * Error handler for watcher errors + */ + errorHandler: null, + /** + * Warn handler for watcher warns + */ + warnHandler: null, + /** + * Ignore certain custom elements + */ + ignoredElements: [], + /** + * Custom user key aliases for v-on + */ + // $flow-disable-line + keyCodes: Object.create(null), + /** + * Check if a tag is reserved so that it cannot be registered as a + * component. This is platform-dependent and may be overwritten. + */ + isReservedTag: no, + /** + * Check if an attribute is reserved so that it cannot be used as a component + * prop. This is platform-dependent and may be overwritten. + */ + isReservedAttr: no, + /** + * Check if a tag is an unknown element. + * Platform-dependent. + */ + isUnknownElement: no, + /** + * Get the namespace of an element + */ + getTagNamespace: noop, + /** + * Parse the real tag name for the specific platform. + */ + parsePlatformTagName: identity, + /** + * Check if an attribute must be bound using property, e.g. value + * Platform-dependent. + */ + mustUseProp: no, + /** + * Perform updates asynchronously. Intended to be used by Vue Test Utils + * This will significantly reduce performance if set to false. + */ + async: true, + /** + * Exposed for legacy reasons + */ + _lifecycleHooks: LIFECYCLE_HOOKS + }; + + /** + * unicode letters used for parsing html tags, component names and property paths. + * using https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname + * skipping \u10000-\uEFFFF due to it freezing up PhantomJS + */ + var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; + /** + * Check if a string starts with $ or _ + */ + function isReserved(str) { + var c = (str + '').charCodeAt(0); + return c === 0x24 || c === 0x5f; + } + /** + * Define a property. + */ + function def(obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true + }); + } + /** + * Parse simple path. + */ + var bailRE = new RegExp("[^".concat(unicodeRegExp.source, ".$_\\d]")); + function parsePath(path) { + if (bailRE.test(path)) { + return; + } + var segments = path.split('.'); + return function (obj) { + for (var i = 0; i < segments.length; i++) { + if (!obj) + return; + obj = obj[segments[i]]; + } + return obj; + }; + } + + // can we use __proto__? + var hasProto = '__proto__' in {}; + // Browser environment sniffing + var inBrowser = typeof window !== 'undefined'; + var UA = inBrowser && window.navigator.userAgent.toLowerCase(); + var isIE = UA && /msie|trident/.test(UA); + var isIE9 = UA && UA.indexOf('msie 9.0') > 0; + var isEdge = UA && UA.indexOf('edge/') > 0; + UA && UA.indexOf('android') > 0; + var isIOS = UA && /iphone|ipad|ipod|ios/.test(UA); + UA && /chrome\/\d+/.test(UA) && !isEdge; + UA && /phantomjs/.test(UA); + var isFF = UA && UA.match(/firefox\/(\d+)/); + // Firefox has a "watch" function on Object.prototype... + // @ts-expect-error firebox support + var nativeWatch = {}.watch; + var supportsPassive = false; + if (inBrowser) { + try { + var opts = {}; + Object.defineProperty(opts, 'passive', { + get: function () { + /* istanbul ignore next */ + supportsPassive = true; + } + }); // https://github.com/facebook/flow/issues/285 + window.addEventListener('test-passive', null, opts); + } + catch (e) { } + } + // this needs to be lazy-evaled because vue may be required before + // vue-server-renderer can set VUE_ENV + var _isServer; + var isServerRendering = function () { + if (_isServer === undefined) { + /* istanbul ignore if */ + if (!inBrowser && typeof global !== 'undefined') { + // detect presence of vue-server-renderer and avoid + // Webpack shimming the process + _isServer = + global['process'] && global['process'].env.VUE_ENV === 'server'; + } + else { + _isServer = false; + } + } + return _isServer; + }; + // detect devtools + var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + /* istanbul ignore next */ + function isNative(Ctor) { + return typeof Ctor === 'function' && /native code/.test(Ctor.toString()); + } + var hasSymbol = typeof Symbol !== 'undefined' && + isNative(Symbol) && + typeof Reflect !== 'undefined' && + isNative(Reflect.ownKeys); + var _Set; // $flow-disable-line + /* istanbul ignore if */ if (typeof Set !== 'undefined' && isNative(Set)) { + // use native Set when available. + _Set = Set; + } + else { + // a non-standard Set polyfill that only works with primitive keys. + _Set = /** @class */ (function () { + function Set() { + this.set = Object.create(null); + } + Set.prototype.has = function (key) { + return this.set[key] === true; + }; + Set.prototype.add = function (key) { + this.set[key] = true; + }; + Set.prototype.clear = function () { + this.set = Object.create(null); + }; + return Set; + }()); + } + + var currentInstance = null; + /** + * This is exposed for compatibility with v3 (e.g. some functions in VueUse + * relies on it). Do not use this internally, just use `currentInstance`. + * + * @internal this function needs manual type declaration because it relies + * on previously manually authored types from Vue 2 + */ + function getCurrentInstance() { + return currentInstance && { proxy: currentInstance }; + } + /** + * @internal + */ + function setCurrentInstance(vm) { + if (vm === void 0) { vm = null; } + if (!vm) + currentInstance && currentInstance._scope.off(); + currentInstance = vm; + vm && vm._scope.on(); + } + + /** + * @internal + */ + var VNode = /** @class */ (function () { + function VNode(tag, data, children, text, elm, context, componentOptions, asyncFactory) { + this.tag = tag; + this.data = data; + this.children = children; + this.text = text; + this.elm = elm; + this.ns = undefined; + this.context = context; + this.fnContext = undefined; + this.fnOptions = undefined; + this.fnScopeId = undefined; + this.key = data && data.key; + this.componentOptions = componentOptions; + this.componentInstance = undefined; + this.parent = undefined; + this.raw = false; + this.isStatic = false; + this.isRootInsert = true; + this.isComment = false; + this.isCloned = false; + this.isOnce = false; + this.asyncFactory = asyncFactory; + this.asyncMeta = undefined; + this.isAsyncPlaceholder = false; + } + Object.defineProperty(VNode.prototype, "child", { + // DEPRECATED: alias for componentInstance for backwards compat. + /* istanbul ignore next */ + get: function () { + return this.componentInstance; + }, + enumerable: false, + configurable: true + }); + return VNode; + }()); + var createEmptyVNode = function (text) { + if (text === void 0) { text = ''; } + var node = new VNode(); + node.text = text; + node.isComment = true; + return node; + }; + function createTextVNode(val) { + return new VNode(undefined, undefined, undefined, String(val)); + } + // optimized shallow clone + // used for static nodes and slot nodes because they may be reused across + // multiple renders, cloning them avoids errors when DOM manipulations rely + // on their elm reference. + function cloneVNode(vnode) { + var cloned = new VNode(vnode.tag, vnode.data, + // #7975 + // clone children array to avoid mutating original in case of cloning + // a child. + vnode.children && vnode.children.slice(), vnode.text, vnode.elm, vnode.context, vnode.componentOptions, vnode.asyncFactory); + cloned.ns = vnode.ns; + cloned.isStatic = vnode.isStatic; + cloned.key = vnode.key; + cloned.isComment = vnode.isComment; + cloned.fnContext = vnode.fnContext; + cloned.fnOptions = vnode.fnOptions; + cloned.fnScopeId = vnode.fnScopeId; + cloned.asyncMeta = vnode.asyncMeta; + cloned.isCloned = true; + return cloned; + } + + /* not type checking this file because flow doesn't play well with Proxy */ + var initProxy; + { + var allowedGlobals_1 = makeMap('Infinity,undefined,NaN,isFinite,isNaN,' + + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,' + + 'require' // for Webpack/Browserify + ); + var warnNonPresent_1 = function (target, key) { + warn$2("Property or method \"".concat(key, "\" is not defined on the instance but ") + + 'referenced during render. Make sure that this property is reactive, ' + + 'either in the data option, or for class-based components, by ' + + 'initializing the property. ' + + 'See: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', target); + }; + var warnReservedPrefix_1 = function (target, key) { + warn$2("Property \"".concat(key, "\" must be accessed with \"$data.").concat(key, "\" because ") + + 'properties starting with "$" or "_" are not proxied in the Vue instance to ' + + 'prevent conflicts with Vue internals. ' + + 'See: https://v2.vuejs.org/v2/api/#data', target); + }; + var hasProxy_1 = typeof Proxy !== 'undefined' && isNative(Proxy); + if (hasProxy_1) { + var isBuiltInModifier_1 = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact'); + config.keyCodes = new Proxy(config.keyCodes, { + set: function (target, key, value) { + if (isBuiltInModifier_1(key)) { + warn$2("Avoid overwriting built-in modifier in config.keyCodes: .".concat(key)); + return false; + } + else { + target[key] = value; + return true; + } + } + }); + } + var hasHandler_1 = { + has: function (target, key) { + var has = key in target; + var isAllowed = allowedGlobals_1(key) || + (typeof key === 'string' && + key.charAt(0) === '_' && + !(key in target.$data)); + if (!has && !isAllowed) { + if (key in target.$data) + warnReservedPrefix_1(target, key); + else + warnNonPresent_1(target, key); + } + return has || !isAllowed; + } + }; + var getHandler_1 = { + get: function (target, key) { + if (typeof key === 'string' && !(key in target)) { + if (key in target.$data) + warnReservedPrefix_1(target, key); + else + warnNonPresent_1(target, key); + } + return target[key]; + } + }; + initProxy = function initProxy(vm) { + if (hasProxy_1) { + // determine which proxy handler to use + var options = vm.$options; + var handlers = options.render && options.render._withStripped ? getHandler_1 : hasHandler_1; + vm._renderProxy = new Proxy(vm, handlers); + } + else { + vm._renderProxy = vm; + } + }; + } + + /****************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + + var __assign = function() { + __assign = Object.assign || function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); + }; + + typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { + var e = new Error(message); + return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; + }; + + var uid$2 = 0; + var pendingCleanupDeps = []; + var cleanupDeps = function () { + for (var i = 0; i < pendingCleanupDeps.length; i++) { + var dep = pendingCleanupDeps[i]; + dep.subs = dep.subs.filter(function (s) { return s; }); + dep._pending = false; + } + pendingCleanupDeps.length = 0; + }; + /** + * A dep is an observable that can have multiple + * directives subscribing to it. + * @internal + */ + var Dep = /** @class */ (function () { + function Dep() { + // pending subs cleanup + this._pending = false; + this.id = uid$2++; + this.subs = []; + } + Dep.prototype.addSub = function (sub) { + this.subs.push(sub); + }; + Dep.prototype.removeSub = function (sub) { + // #12696 deps with massive amount of subscribers are extremely slow to + // clean up in Chromium + // to workaround this, we unset the sub for now, and clear them on + // next scheduler flush. + this.subs[this.subs.indexOf(sub)] = null; + if (!this._pending) { + this._pending = true; + pendingCleanupDeps.push(this); + } + }; + Dep.prototype.depend = function (info) { + if (Dep.target) { + Dep.target.addDep(this); + if (info && Dep.target.onTrack) { + Dep.target.onTrack(__assign({ effect: Dep.target }, info)); + } + } + }; + Dep.prototype.notify = function (info) { + // stabilize the subscriber list first + var subs = this.subs.filter(function (s) { return s; }); + if (!config.async) { + // subs aren't sorted in scheduler if not running async + // we need to sort them now to make sure they fire in correct + // order + subs.sort(function (a, b) { return a.id - b.id; }); + } + for (var i = 0, l = subs.length; i < l; i++) { + var sub = subs[i]; + if (info) { + sub.onTrigger && + sub.onTrigger(__assign({ effect: subs[i] }, info)); + } + sub.update(); + } + }; + return Dep; + }()); + // The current target watcher being evaluated. + // This is globally unique because only one watcher + // can be evaluated at a time. + Dep.target = null; + var targetStack = []; + function pushTarget(target) { + targetStack.push(target); + Dep.target = target; + } + function popTarget() { + targetStack.pop(); + Dep.target = targetStack[targetStack.length - 1]; + } + + /* + * not type checking this file because flow doesn't play well with + * dynamically accessing methods on Array prototype + */ + var arrayProto = Array.prototype; + var arrayMethods = Object.create(arrayProto); + var methodsToPatch = [ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse' + ]; + /** + * Intercept mutating methods and emit events + */ + methodsToPatch.forEach(function (method) { + // cache original method + var original = arrayProto[method]; + def(arrayMethods, method, function mutator() { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + var result = original.apply(this, args); + var ob = this.__ob__; + var inserted; + switch (method) { + case 'push': + case 'unshift': + inserted = args; + break; + case 'splice': + inserted = args.slice(2); + break; + } + if (inserted) + ob.observeArray(inserted); + // notify change + { + ob.dep.notify({ + type: "array mutation" /* TriggerOpTypes.ARRAY_MUTATION */, + target: this, + key: method + }); + } + return result; + }); + }); + + var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + var NO_INITIAL_VALUE = {}; + /** + * In some cases we may want to disable observation inside a component's + * update computation. + */ + var shouldObserve = true; + function toggleObserving(value) { + shouldObserve = value; + } + // ssr mock dep + var mockDep = { + notify: noop, + depend: noop, + addSub: noop, + removeSub: noop + }; + /** + * Observer class that is attached to each observed + * object. Once attached, the observer converts the target + * object's property keys into getter/setters that + * collect dependencies and dispatch updates. + */ + var Observer = /** @class */ (function () { + function Observer(value, shallow, mock) { + if (shallow === void 0) { shallow = false; } + if (mock === void 0) { mock = false; } + this.value = value; + this.shallow = shallow; + this.mock = mock; + // this.value = value + this.dep = mock ? mockDep : new Dep(); + this.vmCount = 0; + def(value, '__ob__', this); + if (isArray(value)) { + if (!mock) { + if (hasProto) { + value.__proto__ = arrayMethods; + /* eslint-enable no-proto */ + } + else { + for (var i = 0, l = arrayKeys.length; i < l; i++) { + var key = arrayKeys[i]; + def(value, key, arrayMethods[key]); + } + } + } + if (!shallow) { + this.observeArray(value); + } + } + else { + /** + * Walk through all properties and convert them into + * getter/setters. This method should only be called when + * value type is Object. + */ + var keys = Object.keys(value); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock); + } + } + } + /** + * Observe a list of Array items. + */ + Observer.prototype.observeArray = function (value) { + for (var i = 0, l = value.length; i < l; i++) { + observe(value[i], false, this.mock); + } + }; + return Observer; + }()); + // helpers + /** + * Attempt to create an observer instance for a value, + * returns the new observer if successfully observed, + * or the existing observer if the value already has one. + */ + function observe(value, shallow, ssrMockReactivity) { + if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { + return value.__ob__; + } + if (shouldObserve && + (ssrMockReactivity || !isServerRendering()) && + (isArray(value) || isPlainObject(value)) && + Object.isExtensible(value) && + !value.__v_skip /* ReactiveFlags.SKIP */ && + !isRef(value) && + !(value instanceof VNode)) { + return new Observer(value, shallow, ssrMockReactivity); + } + } + /** + * Define a reactive property on an Object. + */ + function defineReactive(obj, key, val, customSetter, shallow, mock, observeEvenIfShallow) { + if (observeEvenIfShallow === void 0) { observeEvenIfShallow = false; } + var dep = new Dep(); + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + return; + } + // cater for pre-defined getter/setters + var getter = property && property.get; + var setter = property && property.set; + if ((!getter || setter) && + (val === NO_INITIAL_VALUE || arguments.length === 2)) { + val = obj[key]; + } + var childOb = shallow ? val && val.__ob__ : observe(val, false, mock); + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter() { + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + { + dep.depend({ + target: obj, + type: "get" /* TrackOpTypes.GET */, + key: key + }); + } + if (childOb) { + childOb.dep.depend(); + if (isArray(value)) { + dependArray(value); + } + } + } + return isRef(value) && !shallow ? value.value : value; + }, + set: function reactiveSetter(newVal) { + var value = getter ? getter.call(obj) : val; + if (!hasChanged(value, newVal)) { + return; + } + if (customSetter) { + customSetter(); + } + if (setter) { + setter.call(obj, newVal); + } + else if (getter) { + // #7981: for accessor properties without setter + return; + } + else if (!shallow && isRef(value) && !isRef(newVal)) { + value.value = newVal; + return; + } + else { + val = newVal; + } + childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock); + { + dep.notify({ + type: "set" /* TriggerOpTypes.SET */, + target: obj, + key: key, + newValue: newVal, + oldValue: value + }); + } + } + }); + return dep; + } + function set(target, key, val) { + if ((isUndef(target) || isPrimitive(target))) { + warn$2("Cannot set reactive property on undefined, null, or primitive value: ".concat(target)); + } + if (isReadonly(target)) { + warn$2("Set operation on key \"".concat(key, "\" failed: target is readonly.")); + return; + } + var ob = target.__ob__; + if (isArray(target) && isValidArrayIndex(key)) { + target.length = Math.max(target.length, key); + target.splice(key, 1, val); + // when mocking for SSR, array methods are not hijacked + if (ob && !ob.shallow && ob.mock) { + observe(val, false, true); + } + return val; + } + if (key in target && !(key in Object.prototype)) { + target[key] = val; + return val; + } + if (target._isVue || (ob && ob.vmCount)) { + warn$2('Avoid adding reactive properties to a Vue instance or its root $data ' + + 'at runtime - declare it upfront in the data option.'); + return val; + } + if (!ob) { + target[key] = val; + return val; + } + defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock); + { + ob.dep.notify({ + type: "add" /* TriggerOpTypes.ADD */, + target: target, + key: key, + newValue: val, + oldValue: undefined + }); + } + return val; + } + function del(target, key) { + if ((isUndef(target) || isPrimitive(target))) { + warn$2("Cannot delete reactive property on undefined, null, or primitive value: ".concat(target)); + } + if (isArray(target) && isValidArrayIndex(key)) { + target.splice(key, 1); + return; + } + var ob = target.__ob__; + if (target._isVue || (ob && ob.vmCount)) { + warn$2('Avoid deleting properties on a Vue instance or its root $data ' + + '- just set it to null.'); + return; + } + if (isReadonly(target)) { + warn$2("Delete operation on key \"".concat(key, "\" failed: target is readonly.")); + return; + } + if (!hasOwn(target, key)) { + return; + } + delete target[key]; + if (!ob) { + return; + } + { + ob.dep.notify({ + type: "delete" /* TriggerOpTypes.DELETE */, + target: target, + key: key + }); + } + } + /** + * Collect dependencies on array elements when the array is touched, since + * we cannot intercept array element access like property getters. + */ + function dependArray(value) { + for (var e = void 0, i = 0, l = value.length; i < l; i++) { + e = value[i]; + if (e && e.__ob__) { + e.__ob__.dep.depend(); + } + if (isArray(e)) { + dependArray(e); + } + } + } + + function reactive(target) { + makeReactive(target, false); + return target; + } + /** + * Return a shallowly-reactive copy of the original object, where only the root + * level properties are reactive. It also does not auto-unwrap refs (even at the + * root level). + */ + function shallowReactive(target) { + makeReactive(target, true); + def(target, "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */, true); + return target; + } + function makeReactive(target, shallow) { + // if trying to observe a readonly proxy, return the readonly version. + if (!isReadonly(target)) { + { + if (isArray(target)) { + warn$2("Avoid using Array as root value for ".concat(shallow ? "shallowReactive()" : "reactive()", " as it cannot be tracked in watch() or watchEffect(). Use ").concat(shallow ? "shallowRef()" : "ref()", " instead. This is a Vue-2-only limitation.")); + } + var existingOb = target && target.__ob__; + if (existingOb && existingOb.shallow !== shallow) { + warn$2("Target is already a ".concat(existingOb.shallow ? "" : "non-", "shallow reactive object, and cannot be converted to ").concat(shallow ? "" : "non-", "shallow.")); + } + } + var ob = observe(target, shallow, isServerRendering() /* ssr mock reactivity */); + if (!ob) { + if (target == null || isPrimitive(target)) { + warn$2("value cannot be made reactive: ".concat(String(target))); + } + if (isCollectionType(target)) { + warn$2("Vue 2 does not support reactive collection types such as Map or Set."); + } + } + } + } + function isReactive(value) { + if (isReadonly(value)) { + return isReactive(value["__v_raw" /* ReactiveFlags.RAW */]); + } + return !!(value && value.__ob__); + } + function isShallow(value) { + return !!(value && value.__v_isShallow); + } + function isReadonly(value) { + return !!(value && value.__v_isReadonly); + } + function isProxy(value) { + return isReactive(value) || isReadonly(value); + } + function toRaw(observed) { + var raw = observed && observed["__v_raw" /* ReactiveFlags.RAW */]; + return raw ? toRaw(raw) : observed; + } + function markRaw(value) { + // non-extensible objects won't be observed anyway + if (Object.isExtensible(value)) { + def(value, "__v_skip" /* ReactiveFlags.SKIP */, true); + } + return value; + } + /** + * @internal + */ + function isCollectionType(value) { + var type = toRawType(value); + return (type === 'Map' || type === 'WeakMap' || type === 'Set' || type === 'WeakSet'); + } + + /** + * @internal + */ + var RefFlag = "__v_isRef"; + function isRef(r) { + return !!(r && r.__v_isRef === true); + } + function ref$1(value) { + return createRef(value, false); + } + function shallowRef(value) { + return createRef(value, true); + } + function createRef(rawValue, shallow) { + if (isRef(rawValue)) { + return rawValue; + } + var ref = {}; + def(ref, RefFlag, true); + def(ref, "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */, shallow); + def(ref, 'dep', defineReactive(ref, 'value', rawValue, null, shallow, isServerRendering())); + return ref; + } + function triggerRef(ref) { + if (!ref.dep) { + warn$2("received object is not a triggerable ref."); + } + { + ref.dep && + ref.dep.notify({ + type: "set" /* TriggerOpTypes.SET */, + target: ref, + key: 'value' + }); + } + } + function unref(ref) { + return isRef(ref) ? ref.value : ref; + } + function proxyRefs(objectWithRefs) { + if (isReactive(objectWithRefs)) { + return objectWithRefs; + } + var proxy = {}; + var keys = Object.keys(objectWithRefs); + for (var i = 0; i < keys.length; i++) { + proxyWithRefUnwrap(proxy, objectWithRefs, keys[i]); + } + return proxy; + } + function proxyWithRefUnwrap(target, source, key) { + Object.defineProperty(target, key, { + enumerable: true, + configurable: true, + get: function () { + var val = source[key]; + if (isRef(val)) { + return val.value; + } + else { + var ob = val && val.__ob__; + if (ob) + ob.dep.depend(); + return val; + } + }, + set: function (value) { + var oldValue = source[key]; + if (isRef(oldValue) && !isRef(value)) { + oldValue.value = value; + } + else { + source[key] = value; + } + } + }); + } + function customRef(factory) { + var dep = new Dep(); + var _a = factory(function () { + { + dep.depend({ + target: ref, + type: "get" /* TrackOpTypes.GET */, + key: 'value' + }); + } + }, function () { + { + dep.notify({ + target: ref, + type: "set" /* TriggerOpTypes.SET */, + key: 'value' + }); + } + }), get = _a.get, set = _a.set; + var ref = { + get value() { + return get(); + }, + set value(newVal) { + set(newVal); + } + }; + def(ref, RefFlag, true); + return ref; + } + function toRefs(object) { + if (!isReactive(object)) { + warn$2("toRefs() expects a reactive object but received a plain one."); + } + var ret = isArray(object) ? new Array(object.length) : {}; + for (var key in object) { + ret[key] = toRef(object, key); + } + return ret; + } + function toRef(object, key, defaultValue) { + var val = object[key]; + if (isRef(val)) { + return val; + } + var ref = { + get value() { + var val = object[key]; + return val === undefined ? defaultValue : val; + }, + set value(newVal) { + object[key] = newVal; + } + }; + def(ref, RefFlag, true); + return ref; + } + + var rawToReadonlyFlag = "__v_rawToReadonly"; + var rawToShallowReadonlyFlag = "__v_rawToShallowReadonly"; + function readonly(target) { + return createReadonly(target, false); + } + function createReadonly(target, shallow) { + if (!isPlainObject(target)) { + { + if (isArray(target)) { + warn$2("Vue 2 does not support readonly arrays."); + } + else if (isCollectionType(target)) { + warn$2("Vue 2 does not support readonly collection types such as Map or Set."); + } + else { + warn$2("value cannot be made readonly: ".concat(typeof target)); + } + } + return target; + } + if (!Object.isExtensible(target)) { + warn$2("Vue 2 does not support creating readonly proxy for non-extensible object."); + } + // already a readonly object + if (isReadonly(target)) { + return target; + } + // already has a readonly proxy + var existingFlag = shallow ? rawToShallowReadonlyFlag : rawToReadonlyFlag; + var existingProxy = target[existingFlag]; + if (existingProxy) { + return existingProxy; + } + var proxy = Object.create(Object.getPrototypeOf(target)); + def(target, existingFlag, proxy); + def(proxy, "__v_isReadonly" /* ReactiveFlags.IS_READONLY */, true); + def(proxy, "__v_raw" /* ReactiveFlags.RAW */, target); + if (isRef(target)) { + def(proxy, RefFlag, true); + } + if (shallow || isShallow(target)) { + def(proxy, "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */, true); + } + var keys = Object.keys(target); + for (var i = 0; i < keys.length; i++) { + defineReadonlyProperty(proxy, target, keys[i], shallow); + } + return proxy; + } + function defineReadonlyProperty(proxy, target, key, shallow) { + Object.defineProperty(proxy, key, { + enumerable: true, + configurable: true, + get: function () { + var val = target[key]; + return shallow || !isPlainObject(val) ? val : readonly(val); + }, + set: function () { + warn$2("Set operation on key \"".concat(key, "\" failed: target is readonly.")); + } + }); + } + /** + * Returns a reactive-copy of the original object, where only the root level + * properties are readonly, and does NOT unwrap refs nor recursively convert + * returned properties. + * This is used for creating the props proxy object for stateful components. + */ + function shallowReadonly(target) { + return createReadonly(target, true); + } + + function computed(getterOrOptions, debugOptions) { + var getter; + var setter; + var onlyGetter = isFunction(getterOrOptions); + if (onlyGetter) { + getter = getterOrOptions; + setter = function () { + warn$2('Write operation failed: computed value is readonly'); + } + ; + } + else { + getter = getterOrOptions.get; + setter = getterOrOptions.set; + } + var watcher = isServerRendering() + ? null + : new Watcher(currentInstance, getter, noop, { lazy: true }); + if (watcher && debugOptions) { + watcher.onTrack = debugOptions.onTrack; + watcher.onTrigger = debugOptions.onTrigger; + } + var ref = { + // some libs rely on the presence effect for checking computed refs + // from normal refs, but the implementation doesn't matter + effect: watcher, + get value() { + if (watcher) { + if (watcher.dirty) { + watcher.evaluate(); + } + if (Dep.target) { + if (Dep.target.onTrack) { + Dep.target.onTrack({ + effect: Dep.target, + target: ref, + type: "get" /* TrackOpTypes.GET */, + key: 'value' + }); + } + watcher.depend(); + } + return watcher.value; + } + else { + return getter(); + } + }, + set value(newVal) { + setter(newVal); + } + }; + def(ref, RefFlag, true); + def(ref, "__v_isReadonly" /* ReactiveFlags.IS_READONLY */, onlyGetter); + return ref; + } + + var mark; + var measure; + { + var perf_1 = inBrowser && window.performance; + /* istanbul ignore if */ + if (perf_1 && + // @ts-ignore + perf_1.mark && + // @ts-ignore + perf_1.measure && + // @ts-ignore + perf_1.clearMarks && + // @ts-ignore + perf_1.clearMeasures) { + mark = function (tag) { return perf_1.mark(tag); }; + measure = function (name, startTag, endTag) { + perf_1.measure(name, startTag, endTag); + perf_1.clearMarks(startTag); + perf_1.clearMarks(endTag); + // perf.clearMeasures(name) + }; + } + } + + var normalizeEvent = cached(function (name) { + var passive = name.charAt(0) === '&'; + name = passive ? name.slice(1) : name; + var once = name.charAt(0) === '~'; // Prefixed last, checked first + name = once ? name.slice(1) : name; + var capture = name.charAt(0) === '!'; + name = capture ? name.slice(1) : name; + return { + name: name, + once: once, + capture: capture, + passive: passive + }; + }); + function createFnInvoker(fns, vm) { + function invoker() { + var fns = invoker.fns; + if (isArray(fns)) { + var cloned = fns.slice(); + for (var i = 0; i < cloned.length; i++) { + invokeWithErrorHandling(cloned[i], null, arguments, vm, "v-on handler"); + } + } + else { + // return handler return value for single handlers + return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler"); + } + } + invoker.fns = fns; + return invoker; + } + function updateListeners(on, oldOn, add, remove, createOnceHandler, vm) { + var name, cur, old, event; + for (name in on) { + cur = on[name]; + old = oldOn[name]; + event = normalizeEvent(name); + if (isUndef(cur)) { + warn$2("Invalid handler for event \"".concat(event.name, "\": got ") + String(cur), vm); + } + else if (isUndef(old)) { + if (isUndef(cur.fns)) { + cur = on[name] = createFnInvoker(cur, vm); + } + if (isTrue(event.once)) { + cur = on[name] = createOnceHandler(event.name, cur, event.capture); + } + add(event.name, cur, event.capture, event.passive, event.params); + } + else if (cur !== old) { + old.fns = cur; + on[name] = old; + } + } + for (name in oldOn) { + if (isUndef(on[name])) { + event = normalizeEvent(name); + remove(event.name, oldOn[name], event.capture); + } + } + } + + function mergeVNodeHook(def, hookKey, hook) { + if (def instanceof VNode) { + def = def.data.hook || (def.data.hook = {}); + } + var invoker; + var oldHook = def[hookKey]; + function wrappedHook() { + hook.apply(this, arguments); + // important: remove merged hook to ensure it's called only once + // and prevent memory leak + remove$2(invoker.fns, wrappedHook); + } + if (isUndef(oldHook)) { + // no existing hook + invoker = createFnInvoker([wrappedHook]); + } + else { + /* istanbul ignore if */ + if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { + // already a merged invoker + invoker = oldHook; + invoker.fns.push(wrappedHook); + } + else { + // existing plain hook + invoker = createFnInvoker([oldHook, wrappedHook]); + } + } + invoker.merged = true; + def[hookKey] = invoker; + } + + function extractPropsFromVNodeData(data, Ctor, tag) { + // we are only extracting raw values here. + // validation and default values are handled in the child + // component itself. + var propOptions = Ctor.options.props; + if (isUndef(propOptions)) { + return; + } + var res = {}; + var attrs = data.attrs, props = data.props; + if (isDef(attrs) || isDef(props)) { + for (var key in propOptions) { + var altKey = hyphenate(key); + { + var keyInLowerCase = key.toLowerCase(); + if (key !== keyInLowerCase && attrs && hasOwn(attrs, keyInLowerCase)) { + tip("Prop \"".concat(keyInLowerCase, "\" is passed to component ") + + "".concat(formatComponentName( + // @ts-expect-error tag is string + tag || Ctor), ", but the declared prop name is") + + " \"".concat(key, "\". ") + + "Note that HTML attributes are case-insensitive and camelCased " + + "props need to use their kebab-case equivalents when using in-DOM " + + "templates. You should probably use \"".concat(altKey, "\" instead of \"").concat(key, "\".")); + } + } + checkProp(res, props, key, altKey, true) || + checkProp(res, attrs, key, altKey, false); + } + } + return res; + } + function checkProp(res, hash, key, altKey, preserve) { + if (isDef(hash)) { + if (hasOwn(hash, key)) { + res[key] = hash[key]; + if (!preserve) { + delete hash[key]; + } + return true; + } + else if (hasOwn(hash, altKey)) { + res[key] = hash[altKey]; + if (!preserve) { + delete hash[altKey]; + } + return true; + } + } + return false; + } + + // The template compiler attempts to minimize the need for normalization by + // statically analyzing the template at compile time. + // + // For plain HTML markup, normalization can be completely skipped because the + // generated render function is guaranteed to return Array. There are + // two cases where extra normalization is needed: + // 1. When the children contains components - because a functional component + // may return an Array instead of a single root. In this case, just a simple + // normalization is needed - if any child is an Array, we flatten the whole + // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep + // because functional components already normalize their own children. + function simpleNormalizeChildren(children) { + for (var i = 0; i < children.length; i++) { + if (isArray(children[i])) { + return Array.prototype.concat.apply([], children); + } + } + return children; + } + // 2. When the children contains constructs that always generated nested Arrays, + // e.g.