diff --git a/console/embedded/explore_css.cpp b/console/embedded/explore_css.cpp index 750b68079..ae3dd116c 100644 --- a/console/embedded/explore_css.cpp +++ b/console/embedded/explore_css.cpp @@ -21,7 +21,13 @@ namespace libbitcoin { namespace server { -DEFINE_EMBEDDED_PAGE(explore_pages, char, css, "") +// Simple test css for embedded page, links in font. +DEFINE_EMBEDDED_PAGE(explore_pages, char, css, +R"(@font-face +{ + font-family: 'Boston'; + src: url('boston.woff2'); +})") } // namespace server } // namespace libbitcoin diff --git a/console/embedded/explore_ecma.cpp b/console/embedded/explore_ecma.cpp index b66e06145..81b4d3250 100644 --- a/console/embedded/explore_ecma.cpp +++ b/console/embedded/explore_ecma.cpp @@ -21,7 +21,12 @@ namespace libbitcoin { namespace server { -DEFINE_EMBEDDED_PAGE(explore_pages, char, ecma, "") +// Simple test ecma script for embedded page. +DEFINE_EMBEDDED_PAGE(explore_pages, char, ecma, +R"(document.addEventListener('DOMContentLoaded', function() +{ + console.log('pong'); +});)") } // namespace server } // namespace libbitcoin diff --git a/console/embedded/explore_font.cpp b/console/embedded/explore_font.cpp index 75c755454..552880e5f 100644 --- a/console/embedded/explore_font.cpp +++ b/console/embedded/explore_font.cpp @@ -21,7 +21,17 @@ namespace libbitcoin { namespace server { -DEFINE_EMBEDDED_PAGE(explore_pages, char, font, "") +DEFINE_EMBEDDED_PAGE(explore_pages, uint8_t, font, +{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}) } // namespace server } // namespace libbitcoin diff --git a/console/embedded/explore_html.cpp b/console/embedded/explore_html.cpp index 93a288d18..f15a62e14 100644 --- a/console/embedded/explore_html.cpp +++ b/console/embedded/explore_html.cpp @@ -21,8 +21,22 @@ namespace libbitcoin { namespace server { -// Empty page disabled embedded size. -DEFINE_EMBEDDED_PAGE(explore_pages, char, html, "") +// Simple test html for embedded page, links in css and page icon. +DEFINE_EMBEDDED_PAGE(explore_pages, char, html, +R"( + + Libbitcoin Block Explorer + + + + + + + + +

Hello world!

+ +)") } // namespace server } // namespace libbitcoin diff --git a/console/embedded/explore_icon.cpp b/console/embedded/explore_icon.cpp index 6d0efef1d..35b939569 100644 --- a/console/embedded/explore_icon.cpp +++ b/console/embedded/explore_icon.cpp @@ -21,7 +21,17 @@ namespace libbitcoin { namespace server { -DEFINE_EMBEDDED_PAGE(explore_pages, char, icon, "") +DEFINE_EMBEDDED_PAGE(explore_pages, uint8_t, icon, +{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}) } // namespace server } // namespace libbitcoin diff --git a/console/main.cpp b/console/main.cpp index 0900a4ca2..f0decbf52 100644 --- a/console/main.cpp +++ b/console/main.cpp @@ -86,10 +86,9 @@ int bc::system::main(int argc, char* argv[]) std::ios_base::sync_with_stdio(false); set_utf8_stdio(); - // HACK: web_server used for both! const server::web_pages web_server{}; const server::explore_pages block_explorer{}; - parser metadata(chain::selection::mainnet, web_server, web_server); + parser metadata(chain::selection::mainnet, block_explorer, web_server); const auto& args = const_cast(argv); @@ -100,11 +99,11 @@ int bc::system::main(int argc, char* argv[]) symbols_path = metadata.configured.log.symbols; #endif -// requires _WIN32_WINNT set to 0x0602 (defaults 0x0602 in vc++ 2022). -#if defined(HAVE_MSC) && defined(MEMORY_PRIORITY_INFORMATION) +// Requires _WIN32_WINNT set to 0x0602 (defaults 0x0602 in vc++ 2022). +#if defined(HAVE_MSC) && (_WIN32_WINNT >= _WIN32_WINNT_WIN8) - // Set low memory priority on the current process. - const MEMORY_PRIORITY_INFORMATION priority{ MEMORY_PRIORITY_LOW }; + // Set low memory priority on the current process (testing). + MEMORY_PRIORITY_INFORMATION priority{ MEMORY_PRIORITY_LOW }; SetProcessInformation( GetCurrentProcess(), ProcessMemoryPriority, diff --git a/include/bitcoin/node/protocols/protocol_explore.hpp b/include/bitcoin/node/protocols/protocol_explore.hpp index 49c3fd8a7..376e7c81a 100644 --- a/include/bitcoin/node/protocols/protocol_explore.hpp +++ b/include/bitcoin/node/protocols/protocol_explore.hpp @@ -51,6 +51,10 @@ class BCN_API protocol_explore /// Receivers. void handle_receive_get(const code& ec, const network::http::method::get& request) NOEXCEPT override; + + /// Dispatch. + virtual bool dispatch_object( + const network::http::request& request) NOEXCEPT; }; } // namespace node diff --git a/include/bitcoin/node/protocols/protocol_html.hpp b/include/bitcoin/node/protocols/protocol_html.hpp index df4258825..aea4b7efc 100644 --- a/include/bitcoin/node/protocols/protocol_html.hpp +++ b/include/bitcoin/node/protocols/protocol_html.hpp @@ -51,6 +51,10 @@ class BCN_API protocol_html void handle_receive_get(const code& ec, const network::http::method::get& request) NOEXCEPT override; + /// Dispatch. + virtual bool dispatch_embedded( + const network::http::request& request) NOEXCEPT; + /// Senders. virtual void send_json(const network::http::request& request, boost::json::value&& model, size_t size_hint) NOEXCEPT; @@ -67,6 +71,8 @@ class BCN_API protocol_html /// Utilities. bool is_allowed_origin(const network::http::fields& fields, size_t version) const NOEXCEPT; + std::filesystem::path to_path( + const std::string& target = "/") const NOEXCEPT; std::filesystem::path to_local_path( const std::string& target = "/") const NOEXCEPT; diff --git a/src/protocols/protocol_explore.cpp b/src/protocols/protocol_explore.cpp index eb8ded214..7d332fb3a 100644 --- a/src/protocols/protocol_explore.cpp +++ b/src/protocols/protocol_explore.cpp @@ -34,87 +34,11 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) // Handle get method. // ---------------------------------------------------------------------------- -// TODO: performance timing header. -// TODO: formatted error responses. -// TODO: formatted error responses. -// TODO: priority accept media type sort and dispatch. -// TODO: URI path parse (see API doc). - -/// TODO: move to own source. -/// Pagination and filtering are via query string. -enum targets -{ - /// /v[]/block/hash/[bkhash] {1} - /// /v[]/block/height/[height] {1} - block, - - /// /v[]/block/hash/[bkhash]/filter {1} - /// /v[]/block/height/[height]/filter {1} - filter, - - /// /v[]/block/hash/[bkhash]/header {1} - /// /v[]/block/height/[height]/header {1} - header, - - /// /v[]/transaction/hash/[txhash] {1} - /// /v[]/block/hash/[bkhash]/transaction/position/[position] {1} - /// /v[]/block/height/[height]/transaction/position/[position] {1} - transaction, - - /// /v[]/block/hash/[bkhash]/transactions {all txs in the block} - /// /v[]/block/height/[height]/transactions {all txs in the block} - transactions, - - // -------------------------------------------------------------------- - - /// /v[]/input/[txhash]/[index] {1} - input, - - /// /v[]/inputs/[txhash] {all inputs in the tx} - inputs, - - /// /v[]/input/[txhash]/[index]/script {1} - input_script, - - /// /v[]/input/[txhash]/scripts {all input scripts in the tx} - input_scripts, - - /// /v[]/input/[txhash]/[index]/witness {1} - input_witness, - - /// /v[]/input/[txhash]/witnesses {all witnesses in the tx} - input_witnesses, - - // -------------------------------------------------------------------- - - /// /v[]/output/[txhash]/[index] {1} - output, - - /// /v[]/outputs/[txhash] {all outputs in the tx} - outputs, - - /// /v[]/output/[txhash]/[index]/script {1} - output_script, - - /// /v[]/output/[txhash]/scripts {all output scripts in the tx} - output_scripts, - - /// /v[]/output/[txhash]/[index]/spender {1 - confirmed} - output_spender, - - /// /v[]/output/[txhash]/spenders {all} - output_spenders, - - // -------------------------------------------------------------------- - - /// /v[]/address/[output-script-hash] {all} - address -}; void protocol_explore::handle_receive_get(const code& ec, const method::get& request) NOEXCEPT { - BC_ASSERT_MSG(stranded(), "strand"); + BC_ASSERT(stranded()); if (stopped(ec)) return; @@ -133,207 +57,194 @@ void protocol_explore::handle_receive_get(const code& ec, return; } - const auto target = request->target(); - if (!is_origin_form(target)) - { - send_bad_target(*request); + // Dispatch object with specified encoding. + if (dispatch_object(*request)) return; - } - wallet::uri uri{}; - if (!uri.decode(target)) + // Embedded page site. + if (dispatch_embedded(*request)) + return; + + // Empty path implies malformed target (terminal). + auto path = to_local_path(request->target()); + if (path.empty()) { send_bad_target(*request); return; } - if (const auto parts = split(uri.path(), "/"); - parts.size() == two) + // If no file extension it's REST on the single/default html page. + if (!path.has_extension()) { - constexpr auto data = mime_type::application_octet_stream; - constexpr auto json = mime_type::application_json; - constexpr auto text = mime_type::text_plain; - - const auto hd = parts.front() == "header" || parts.front() == "hd"; - const auto bk = parts.front() == "block" || parts.front() == "bk"; - const auto tx = parts.front() == "transaction" || parts.front() == "tx"; - if (!hd && !bk && !tx) - { - send_bad_target(*request); - return; - } + path = to_local_path(); - auto params = uri.decode_query(); - const auto format = params["format"]; - const auto accepts = to_mime_types((*request)[field::accept]); - const auto is_json = contains(accepts, json) || format == "json"; - const auto is_text = contains(accepts, text) || format == "text"; - const auto is_data = contains(accepts, data) || format == "data"; - const auto wit = params["witness"] != "false"; - const auto hex = parts.back(); - - hash_digest hash{}; - if ((is_json || is_text || is_data) && !decode_hash(hash, hex)) + // Default html page (e.g. index.html) is not configured. + if (path.empty()) { - send_bad_target(*request); + send_not_implemented(*request); return; } + } - const auto& query = archive(); + // Get the single/default or explicitly requested page. + auto file = get_file_body(path); + if (!file.is_open()) + { + send_not_found(*request); + return; + } - if (is_json) + send_file(*request, std::move(file), + file_mime_type(path, mime_type::application_octet_stream)); +} + +// Dispatch. +// ---------------------------------------------------------------------------- + +bool protocol_explore::dispatch_object( + const network::http::request& request) NOEXCEPT +{ + const auto target = request.target(); + if (!is_origin_form(target)) + { + send_bad_target(request); + return true; + } + + wallet::uri uri{}; + if (!uri.decode(target)) + { + send_bad_target(request); + return true; + } + + const auto parts = split(uri.path(), "/"); + if (parts.size() != two) + return false; + + const auto hd = parts.front() == "header" || parts.front() == "hd"; + const auto bk = parts.front() == "block" || parts.front() == "bk"; + const auto tx = parts.front() == "transaction" || parts.front() == "tx"; + if (!hd && !bk && !tx) + { + send_bad_target(request); + return true; + } + + auto params = uri.decode_query(); + const auto format = params["format"]; + constexpr auto text = mime_type::text_plain; + constexpr auto json = mime_type::application_json; + constexpr auto data = mime_type::application_octet_stream; + const auto accepts = to_mime_types((request)[field::accept]); + const auto is_json = contains(accepts, json) || format == "json"; + const auto is_text = contains(accepts, text) || format == "text"; + const auto is_data = contains(accepts, data) || format == "data"; + if (!is_json && !is_text && !is_data) + return false; + + hash_digest hash{}; + if (!decode_hash(hash, parts.back())) + { + send_bad_target(request); + return true; + } + + const auto& query = archive(); + const auto wit = params["witness"] != "false"; + + if (is_json) + { + if (hd) { - if (hd) - { - if (const auto ptr = query.get_header(query.to_header(hash))) - { - send_json(*request, value_from(ptr), ptr->serialized_size()); - return; - } - } - else if (bk) - { - if (const auto ptr = query.get_block(query.to_header(hash), wit)) - { - send_json(*request, value_from(ptr), ptr->serialized_size(wit)); - return; - } - } - else + if (const auto ptr = query.get_header(query.to_header(hash))) { - if (const auto ptr = query.get_transaction(query.to_tx(hash), wit)) - { - send_json(*request, value_from(ptr), ptr->serialized_size(wit)); - return; - } + send_json(request, value_from(ptr), ptr->serialized_size()); + return true; } - - send_not_found(*request); - return; } - else if (is_text) + else if (bk) { - if (hd) - { - if (const auto ptr = query.get_header(query.to_header(hash))) - { - send_text(*request, encode_base16(ptr->to_data())); - return; - } - } - else if (bk) + if (const auto ptr = query.get_block(query.to_header(hash), wit)) { - if (const auto ptr = query.get_block(query.to_header(hash), wit)) - { - send_text(*request, encode_base16(ptr->to_data(wit))); - return; - } + send_json(request, value_from(ptr), ptr->serialized_size(wit)); + return true; } - else + } + else + { + if (const auto ptr = query.get_transaction(query.to_tx(hash), wit)) { - if (const auto ptr = query.get_transaction(query.to_tx(hash), wit)) - { - send_text(*request, encode_base16(ptr->to_data(wit))); - return; - } + send_json(request, value_from(ptr), ptr->serialized_size(wit)); + return true; } - - send_not_found(*request); - return; } - else if (is_data) + + send_not_found(request); + return true; + } + + if (is_text) + { + if (hd) { - if (hd) + if (const auto ptr = query.get_header(query.to_header(hash))) { - if (const auto ptr = query.get_header(query.to_header(hash))) - { - send_data(*request, ptr->to_data()); - return; - } + send_text(request, encode_base16(ptr->to_data())); + return true; } - else if (bk) + } + else if (bk) + { + if (const auto ptr = query.get_block(query.to_header(hash), wit)) { - if (const auto ptr = query.get_block(query.to_header(hash), wit)) - { - send_data(*request, ptr->to_data(wit)); - return; - } + send_text(request, encode_base16(ptr->to_data(wit))); + return true; } - else + } + else + { + if (const auto ptr = query.get_transaction(query.to_tx(hash), wit)) { - if (const auto ptr = query.get_transaction(query.to_tx(hash), wit)) - { - send_data(*request, ptr->to_data(wit)); - return; - } + send_text(request, encode_base16(ptr->to_data(wit))); + return true; } - - send_not_found(*request); - return; } + + send_not_found(request); + return true; } - // Embedded page site. - if (config().server.explore.pages.enabled()) + ////if (is_data) { - const auto& pages = config().server.explore.pages; - const auto mime = extension_mime_type(to_extension(request->target())); - switch (mime) + if (hd) { - case mime_type::text_css: - send_span(*request, pages.css(), mime); - break; - case mime_type::text_html: - send_span(*request, pages.html(), mime); - break; - case mime_type::application_javascript: - send_span(*request, pages.ecma(), mime); - break; - case mime_type::font_woff: - case mime_type::font_woff2: - send_span(*request, pages.font(), mime); - return; - case mime_type::image_png: - case mime_type::image_gif: - case mime_type::image_jpeg: - case mime_type::image_x_icon: - case mime_type::image_svg_xml: - send_span(*request, pages.icon(), mime); - break; - default: - send_not_implemented(*request); - return; + if (const auto ptr = query.get_header(query.to_header(hash))) + { + send_data(request, ptr->to_data()); + return true; + } } - } - - // Empty path implies malformed target (terminal). - auto path = to_local_path(request->target()); - if (path.empty()) - { - send_bad_target(*request); - return; - } - - if (!path.has_extension()) - { - // Empty implies default page invalid or not configured (terminal). - path = to_local_path(); - if (path.empty()) + else if (bk) { - send_not_implemented(*request); - return; + if (const auto ptr = query.get_block(query.to_header(hash), wit)) + { + send_data(request, ptr->to_data(wit)); + return true; + } + } + else + { + if (const auto ptr = query.get_transaction(query.to_tx(hash), wit)) + { + send_data(request, ptr->to_data(wit)); + return true; + } } - } - // Not open implies file not found (non-terminal). - auto file = get_file_body(path); - if (!file.is_open()) - { - send_not_found(*request); - return; + send_not_found(request); + return true; } - - send_file(*request, std::move(file), file_mime_type(path)); } BC_POP_WARNING() diff --git a/src/protocols/protocol_html.cpp b/src/protocols/protocol_html.cpp index d6b82a90e..22bcb125d 100644 --- a/src/protocols/protocol_html.cpp +++ b/src/protocols/protocol_html.cpp @@ -36,7 +36,7 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) void protocol_html::handle_receive_get(const code& ec, const method::get& request) NOEXCEPT { - BC_ASSERT_MSG(stranded(), "strand"); + BC_ASSERT(stranded()); if (stopped(ec)) return; @@ -55,10 +55,16 @@ void protocol_html::handle_receive_get(const code& ec, return; } + // Embedded page site. + if (dispatch_embedded(*request)) + return; + // Empty path implies malformed target (terminal). const auto path = to_local_path(request->target()); if (path.empty()) { + // TODO: split out sanitize from canonicalize so that this can return + // send_not_found() when the request is sanitary but not found. send_bad_target(*request); return; } @@ -71,8 +77,43 @@ void protocol_html::handle_receive_get(const code& ec, return; } - const auto default_type = mime_type::application_octet_stream; - send_file(*request, std::move(file), file_mime_type(path, default_type)); + send_file(*request, std::move(file), + file_mime_type(path, mime_type::application_octet_stream)); +} + +// Dispatch. +// ---------------------------------------------------------------------------- + +bool protocol_html::dispatch_embedded(const request& request) NOEXCEPT +{ + // False only if not enabled, otherwise handled below. + if (!options_.pages.enabled()) + return false; + + const auto& pages = config().server.explore.pages; + switch (const auto mime = file_mime_type(to_path(request.target()))) + { + case mime_type::text_css: + send_span(request, pages.css(), mime); + return true; + case mime_type::text_html: + send_span(request, pages.html(), mime); + return true; + case mime_type::application_javascript: + send_span(request, pages.ecma(), mime); + return true; + case mime_type::font_woff: + case mime_type::font_woff2: + send_span(request, pages.font(), mime); + return true; + case mime_type::image_png: + case mime_type::image_gif: + case mime_type::image_jpeg: + send_span(request, pages.icon(), mime); + return true; + default: + return false; + } } // Senders. @@ -85,7 +126,7 @@ constexpr auto text = mime_type::text_plain; void protocol_html::send_json(const request& request, boost::json::value&& model, size_t size_hint) NOEXCEPT { - BC_ASSERT_MSG(stranded(), "strand"); + BC_ASSERT(stranded()); response response{ status::ok, request.version() }; add_common_headers(response, request); response.set(field::content_type, from_mime_type(json)); @@ -97,7 +138,7 @@ void protocol_html::send_json(const request& request, void protocol_html::send_text(const request& request, std::string&& hexidecimal) NOEXCEPT { - BC_ASSERT_MSG(stranded(), "strand"); + BC_ASSERT(stranded()); response response{ status::ok, request.version() }; add_common_headers(response, request); response.set(field::content_type, from_mime_type(text)); @@ -109,7 +150,7 @@ void protocol_html::send_text(const request& request, void protocol_html::send_data(const request& request, system::data_chunk&& bytes) NOEXCEPT { - BC_ASSERT_MSG(stranded(), "strand"); + BC_ASSERT(stranded()); response response{ status::ok, request.version() }; add_common_headers(response, request); response.set(field::content_type, from_mime_type(data)); @@ -121,7 +162,7 @@ void protocol_html::send_data(const request& request, void protocol_html::send_file(const request& request, file&& file, mime_type type) NOEXCEPT { - BC_ASSERT_MSG(stranded(), "strand"); + BC_ASSERT(stranded()); BC_ASSERT_MSG(file.is_open(), "sending closed file handle"); response response{ status::ok, request.version() }; add_common_headers(response, request); @@ -134,7 +175,7 @@ void protocol_html::send_file(const request& request, file&& file, void protocol_html::send_span(const request& request, span_body::value_type&& span, mime_type type) NOEXCEPT { - BC_ASSERT_MSG(stranded(), "strand"); + BC_ASSERT(stranded()); response response{ status::ok, request.version() }; add_common_headers(response, request); response.set(field::content_type, from_mime_type(type)); @@ -159,11 +200,16 @@ bool protocol_html::is_allowed_origin(const fields& fields, network::config::to_normal_host(origin, default_port())); } +std::filesystem::path protocol_html::to_path( + const std::string& target) const NOEXCEPT +{ + return target == "/" ? target + options_.default_ : target; +} + std::filesystem::path protocol_html::to_local_path( const std::string& target) const NOEXCEPT { - return sanitize_origin(options_.path, - target == "/" ? target + options_.default_ : target); + return sanitize_origin(options_.path, to_path(target).string()); } BC_POP_WARNING()