434 auto accept = [
this](
auto& args,
const nlohmann::json& params) {
443 HTTP_STATUS_INTERNAL_SERVER_ERROR,
444 ccf::errors::InternalError,
445 "Target node should be part of network to accept new nodes.");
451 auto this_startup_seqno =
454 in.startup_seqno.has_value() &&
455 this_startup_seqno > in.startup_seqno.value())
458 HTTP_STATUS_BAD_REQUEST,
459 ccf::errors::StartupSeqnoIsOld,
461 "Node requested to join from seqno {} which is older than this "
462 "node startup seqno {}. A snapshot at least as recent as {} must "
464 in.startup_seqno.value(),
466 this_startup_seqno));
470 auto service = args.tx.rw(this->network.
service);
472 auto active_service = service->get();
473 if (!active_service.has_value())
476 HTTP_STATUS_INTERNAL_SERVER_ERROR,
477 ccf::errors::InternalError,
478 "No service is available to accept new node.");
481 auto config = args.tx.ro(network.
config);
482 auto service_config = config->get();
492 auto existing_node_info = check_node_exists(
494 args.rpc_ctx->get_session_context()->caller_cert,
495 joining_node_status);
496 if (existing_node_info.has_value())
503 this->network.ledger_secrets->get(
504 args.tx, existing_node_info->ledger_secret_seqno),
505 *this->network.identity.get(),
506 active_service->status,
507 existing_node_info->endorsed_certificate,
516 if (primary_id.has_value())
518 auto info =
nodes->get(primary_id.value());
522 args.rpc_ctx->get_session_context()->interface_id;
523 if (!interface_id.has_value())
526 HTTP_STATUS_INTERNAL_SERVER_ERROR,
527 ccf::errors::InternalError,
528 "Cannot redirect non-RPC request.");
530 const auto& address =
531 info->rpc_interfaces[interface_id.value()].published_address;
532 args.rpc_ctx->set_response_header(
533 http::headers::LOCATION,
534 fmt::format(
"https://{}/node/join", address));
537 HTTP_STATUS_PERMANENT_REDIRECT,
538 ccf::errors::NodeCannotHandleRequest,
539 "Node is not primary; cannot handle write");
544 HTTP_STATUS_INTERNAL_SERVER_ERROR,
545 ccf::errors::InternalError,
551 args.rpc_ctx->get_session_context()->caller_cert,
554 active_service->status,
563 auto existing_node_info = check_node_exists(
564 args.tx, args.rpc_ctx->get_session_context()->caller_cert);
565 if (existing_node_info.has_value())
571 auto node_info =
nodes->get(existing_node_info->node_id);
572 auto node_status = node_info->status;
574 rep.
node_id = existing_node_info->node_id;
575 if (is_taking_part_in_acking(node_status))
580 this->network.ledger_secrets->get(
581 args.tx, existing_node_info->ledger_secret_seqno),
582 *this->network.identity.get(),
583 active_service->status,
584 existing_node_info->endorsed_certificate,
597 HTTP_STATUS_BAD_REQUEST,
598 ccf::errors::InvalidNodeState,
600 "Joining node is not in expected state ({}).", node_status));
608 if (primary_id.has_value())
610 auto info =
nodes->get(primary_id.value());
614 args.rpc_ctx->get_session_context()->interface_id;
615 if (!interface_id.has_value())
618 HTTP_STATUS_INTERNAL_SERVER_ERROR,
619 ccf::errors::InternalError,
620 "Cannot redirect non-RPC request.");
622 const auto& address =
623 info->rpc_interfaces[interface_id.value()].published_address;
624 args.rpc_ctx->set_response_header(
625 http::headers::LOCATION,
626 fmt::format(
"https://{}/node/join", address));
629 HTTP_STATUS_PERMANENT_REDIRECT,
630 ccf::errors::NodeCannotHandleRequest,
631 "Node is not primary; cannot handle write");
636 HTTP_STATUS_INTERNAL_SERVER_ERROR,
637 ccf::errors::InternalError,
644 args.rpc_ctx->get_session_context()->caller_cert,
647 active_service->status,
651 make_endpoint(
"/join", HTTP_POST,
json_adapter(accept), no_auth_required)
653 .set_openapi_hidden(
true)
656 auto set_retired_committed = [
this](
auto& ctx, nlohmann::json&&) {
658 nodes->foreach([
this, &
nodes](
const auto& node_id,
auto node_info) {
659 auto gc_node =
nodes->get_globally_committed(node_id);
661 gc_node.has_value() &&
663 !node_info.retired_committed)
667 node_info.retired_committed =
true;
668 nodes->put(node_id, node_info);
670 LOG_DEBUG_FMT(
"Setting retired_committed on node {}", node_id);
678 "network/nodes/set_retired_committed",
681 {std::make_shared<NodeCertAuthnPolicy>()})
682 .set_openapi_hidden(
true)
685 auto get_state = [
this](
auto& args, nlohmann::json&&) {
687 auto [s, rts, lrs] = this->node_operation.
state();
688 result.
node_id = this->context.get_node_id();
695 auto signatures = args.tx.template ro<Signatures>(Tables::SIGNATURES);
696 auto sig = signatures->get();
697 if (!sig.has_value())
706 auto node_configuration_subsystem =
708 if (!node_configuration_subsystem)
711 HTTP_STATUS_INTERNAL_SERVER_ERROR,
712 ccf::errors::InternalError,
713 "NodeConfigurationSubsystem is not available");
716 node_configuration_subsystem->has_received_stop_notice();
720 make_read_only_endpoint(
726 auto get_quote = [
this](
auto& args, nlohmann::json&&) {
729 get_quote_for_this_node_v1(args.tx, node_quote_info);
733 q.
node_id = context.get_node_id();
740 auto node_info =
nodes->get(context.get_node_id());
741 if (node_info.has_value() && node_info->code_digest.has_value())
749 if (measurement.has_value())
756 HTTP_STATUS_INTERNAL_SERVER_ERROR,
757 ccf::errors::InvalidQuote,
758 "Failed to extract code id from node quote.");
767 HTTP_STATUS_NOT_FOUND,
768 ccf::errors::ResourceNotFound,
769 "Could not find node quote.");
774 HTTP_STATUS_INTERNAL_SERVER_ERROR,
775 ccf::errors::InternalError,
779 make_read_only_endpoint(
784 .set_auto_schema<void,
Quote>()
787 make_read_only_endpoint(
788 "/attestations/self",
796 auto get_quotes = [
this](
auto& args, nlohmann::json&&) {
801 const auto& node_id,
const auto& node_info) {
806 q.
raw = node_info.quote_info.quote;
808 q.
format = node_info.quote_info.format;
811 if (node_info.code_digest.has_value())
819 if (measurement.has_value())
824 quotes.emplace_back(q);
831 make_read_only_endpoint(
839 auto get_attestations =
840 [
this, get_quotes](
auto& args, nlohmann::json&& params) {
841 const auto res = get_quotes(args, std::move(params));
842 const auto body = std::get_if<nlohmann::json>(&res);
845 auto result = nlohmann::json::object();
846 result[
"attestations"] = (*body)[
"quotes"];
852 make_read_only_endpoint(
860 auto network_status = [
this](
auto& args, nlohmann::json&&) {
862 auto service = args.tx.ro(network.
service);
863 auto service_state = service->get();
864 if (service_state.has_value())
866 const auto& service_value = service_state.value();
872 service_value.current_service_create_txid;
877 if (primary_id.has_value())
885 HTTP_STATUS_NOT_FOUND,
886 ccf::errors::ResourceNotFound,
887 "Service state not available.");
889 make_read_only_endpoint(
897 auto service_previous_identity = [
this](
auto& args, nlohmann::json&&) {
898 auto psi_handle = args.tx.template ro<ccf::PreviousServiceIdentity>(
899 ccf::Tables::PREVIOUS_SERVICE_IDENTITY);
900 const auto psi = psi_handle->get();
910 HTTP_STATUS_NOT_FOUND,
911 ccf::errors::ResourceNotFound,
912 "This service is not a recovery of a previous service.");
915 make_read_only_endpoint(
916 "/service/previous_identity",
923 auto get_nodes = [
this](
auto& args, nlohmann::json&&) {
924 const auto parsed_query =
925 http::parse_query(args.rpc_ctx->get_request_query());
928 const auto host = http::get_query_value_opt<std::string>(
930 const auto port = http::get_query_value_opt<std::string>(
932 const auto status_str = http::get_query_value_opt<std::string>(
935 std::optional<NodeStatus> status;
936 if (status_str.has_value())
942 status = nlohmann::json(status_str.value()).get<
NodeStatus>();
947 HTTP_STATUS_BAD_REQUEST,
948 ccf::errors::InvalidQueryParameterValue,
950 "Query parameter '{}' is not a valid node status",
951 status_str.value()));
960 if (status.has_value() && status.value() != ni.
status)
966 bool is_matched =
false;
969 const auto& [pub_host, pub_port] =
970 split_net_address(interface.second.published_address);
973 (!
host.has_value() ||
host.value() == pub_host) &&
974 (!port.has_value() || port.value() == pub_port))
986 bool is_primary =
false;
989 is_primary =
consensus->primary() == nid;
998 nodes->get_version_of_previous_write(nid).value_or(0)});
1004 make_read_only_endpoint(
1010 .add_query_parameter<std::string>(
1012 .add_query_parameter<std::string>(
1014 .add_query_parameter<std::string>(
1018 auto get_removable_nodes = [
this](
auto& args, nlohmann::json&&) {
1021 auto nodes = args.tx.ro(this->network.
nodes);
1030 auto node =
nodes->get_globally_committed(node_id);
1033 node->retired_committed)
1035 out.
nodes.push_back(
1039 node->rpc_interfaces,
1041 nodes->get_version_of_previous_write(node_id).value_or(0)});
1049 make_read_only_endpoint(
1050 "/network/removable_nodes",
1057 auto delete_retired_committed_node =
1058 [
this](
auto& args, nlohmann::json&&) {
1061 std::string node_id;
1063 if (!get_path_param(
1064 args.rpc_ctx->get_request_path_params(),
1070 HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName,
error);
1073 auto nodes = args.tx.rw(this->network.
nodes);
1074 if (!
nodes->has(node_id))
1077 HTTP_STATUS_NOT_FOUND,
1078 ccf::errors::ResourceNotFound,
1082 auto node_endorsed_certificates =
1096 auto node =
nodes->get_globally_committed(node_id);
1099 node->retired_committed)
1101 nodes->remove(node_id);
1102 node_endorsed_certificates->remove(node_id);
1107 HTTP_STATUS_BAD_REQUEST,
1108 ccf::errors::NodeNotRetiredCommitted,
1109 "Node is not completely retired");
1116 "/network/nodes/{node_id}",
1120 .set_auto_schema<void,
bool>()
1123 auto get_self_signed_certificate = [
this](
auto& args, nlohmann::json&&) {
1127 make_command_endpoint(
1128 "/self_signed_certificate",
1136 auto get_node_info = [
this](
auto& args, nlohmann::json&&) {
1137 std::string node_id;
1139 if (!get_path_param(
1140 args.rpc_ctx->get_request_path_params(),
1146 HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName,
error);
1149 auto nodes = args.tx.ro(this->network.
nodes);
1150 auto info =
nodes->get(node_id);
1155 HTTP_STATUS_NOT_FOUND,
1156 ccf::errors::ResourceNotFound,
1160 bool is_primary =
false;
1164 if (primary.has_value() && primary.value() == node_id)
1169 auto& ni = info.value();
1176 nodes->get_version_of_previous_write(node_id).value_or(0)});
1178 make_read_only_endpoint(
1179 "/network/nodes/{node_id}",
1186 auto get_self_node = [
this](
auto& args, nlohmann::json&&) {
1187 auto node_id = this->context.get_node_id();
1188 auto nodes = args.tx.ro(this->network.
nodes);
1189 auto info =
nodes->get(node_id);
1191 bool is_primary =
false;
1195 if (primary.has_value() && primary.value() == node_id)
1201 if (info.has_value())
1205 auto& ni = info.value();
1212 nodes->get_version_of_previous_write(node_id).value_or(0)});
1217 auto node_configuration_subsystem =
1219 if (!node_configuration_subsystem)
1222 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1223 ccf::errors::InternalError,
1224 "NodeConfigurationSubsystem is not available");
1226 const auto& node_startup_config =
1227 node_configuration_subsystem->get().node_config;
1233 node_startup_config.node_data,
1237 make_read_only_endpoint(
1238 "/network/nodes/self",
1246 auto get_primary_node = [
this](
auto& args, nlohmann::json&&) {
1250 if (!primary_id.has_value())
1253 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1254 ccf::errors::InternalError,
1258 auto nodes = args.tx.ro(this->network.
nodes);
1259 auto info =
nodes->get(primary_id.value());
1263 HTTP_STATUS_NOT_FOUND,
1264 ccf::errors::ResourceNotFound,
1268 auto& ni = info.value();
1275 nodes->get_version_of_previous_write(primary_id.value())
1281 HTTP_STATUS_NOT_FOUND,
1282 ccf::errors::ResourceNotFound,
1283 "No configured consensus");
1286 make_read_only_endpoint(
1287 "/network/nodes/primary",
1294 auto head_primary = [
this](
auto& args) {
1297 args.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1301 args.rpc_ctx->set_response_status(HTTP_STATUS_PERMANENT_REDIRECT);
1305 if (!primary_id.has_value())
1307 args.rpc_ctx->set_error(
1308 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1309 ccf::errors::InternalError,
1314 auto nodes = args.tx.ro(this->network.
nodes);
1315 auto info =
nodes->get(primary_id.value());
1318 auto& interface_id =
1319 args.rpc_ctx->get_session_context()->interface_id;
1320 if (!interface_id.has_value())
1322 args.rpc_ctx->set_error(
1323 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1324 ccf::errors::InternalError,
1325 "Cannot redirect non-RPC request.");
1328 const auto& address =
1329 info->rpc_interfaces[interface_id.value()].published_address;
1330 args.rpc_ctx->set_response_header(
1331 http::headers::LOCATION,
1332 fmt::format(
"https://{}/node/primary", address));
1337 make_read_only_endpoint(
1338 "/primary", HTTP_HEAD, head_primary, no_auth_required)
1342 auto get_primary = [
this](
auto& args) {
1345 args.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1350 args.rpc_ctx->set_error(
1351 HTTP_STATUS_NOT_FOUND,
1352 ccf::errors::ResourceNotFound,
1353 "Node is not primary");
1357 make_read_only_endpoint(
1358 "/primary", HTTP_GET, get_primary, no_auth_required)
1362 auto get_backup = [
this](
auto& args) {
1365 args.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1370 args.rpc_ctx->set_error(
1371 HTTP_STATUS_NOT_FOUND,
1372 ccf::errors::ResourceNotFound,
1373 "Node is not backup");
1377 make_read_only_endpoint(
"/backup", HTTP_GET, get_backup, no_auth_required)
1381 auto consensus_config = [
this](
auto& args, nlohmann::json&&) {
1385 auto cfg =
consensus->get_latest_configuration();
1387 for (
auto& [nid, ninfo] : cfg)
1392 fmt::format(
"{}:{}", ninfo.hostname, ninfo.port)});
1399 HTTP_STATUS_NOT_FOUND,
1400 ccf::errors::ResourceNotFound,
1401 "No configured consensus");
1405 make_command_endpoint(
1414 auto consensus_state = [
this](
auto& args, nlohmann::json&&) {
1422 HTTP_STATUS_NOT_FOUND,
1423 ccf::errors::ResourceNotFound,
1424 "No configured consensus");
1428 make_command_endpoint(
1437 auto memory_usage = [](
auto& args) {
1439 if (ccf::pal::get_mallinfo(info))
1442 args.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1443 args.rpc_ctx->set_response_header(
1444 http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON);
1445 args.rpc_ctx->set_response_body(nlohmann::json(mu).dump());
1449 args.rpc_ctx->set_response_status(HTTP_STATUS_INTERNAL_SERVER_ERROR);
1450 args.rpc_ctx->set_response_body(
"Failed to read memory usage");
1453 make_command_endpoint(
"/memory", HTTP_GET, memory_usage, no_auth_required)
1458 auto node_metrics = [
this](
auto& args) {
1462 args.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1463 args.rpc_ctx->set_response_header(
1464 http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON);
1465 args.rpc_ctx->set_response_body(nlohmann::json(nm).dump());
1468 make_command_endpoint(
1469 "/metrics", HTTP_GET, node_metrics, no_auth_required)
1474 auto js_metrics = [
this](
auto& args, nlohmann::json&&) {
1477 uint64_t bytecode_size = 0;
1478 bytecode_map->foreach(
1479 [&bytecode_size](
const auto&,
const auto& bytecode) {
1480 bytecode_size += bytecode.size();
1483 auto js_engine_map = args.tx.ro(this->network.
js_engine);
1487 version_val->get() == std::string(ccf::quickjs_version);
1498 make_read_only_endpoint(
1506 auto version = [
this](
auto&, nlohmann::json&&) {
1515 make_command_endpoint(
1521 auto create = [
this](
auto& ctx, nlohmann::json&& params) {
1532 HTTP_STATUS_FORBIDDEN,
1533 ccf::errors::InternalError,
1534 "Node is not in initial state.");
1542 HTTP_STATUS_FORBIDDEN,
1543 ccf::errors::InternalError,
1544 "Service is already created.");
1548 ctx.tx, in.service_cert, in.create_txid, in.service_data, recovering);
1554 if (in.genesis_info.has_value())
1559 for (
const auto& info : in.genesis_info->members)
1565 ctx.tx, in.genesis_info->service_configuration);
1567 ctx.tx, in.genesis_info->constitution);
1575 throw std::logic_error(
"Could not cast tx to CommittableTx");
1581 auto endorsed_certificates =
1583 endorsed_certificates->put(in.node_id, in.node_endorsed_certificate);
1586 in.node_info_network,
1588 in.public_encryption_key,
1591 in.measurement.hex_str(),
1592 in.certificate_signing_request,
1599 !in.snp_uvm_endorsements.has_value())
1604 ctx.tx, in.measurement, in.quote_info.format);
1607 switch (in.quote_info.format)
1612 if (host_data.has_value())
1615 ctx.tx, host_data.value());
1619 LOG_FAIL_FMT(
"Unable to extract host data from virtual quote");
1629 ctx.tx, host_data, in.snp_security_policy);
1632 ctx.tx, in.snp_uvm_endorsements);
1637 ctx.tx, attestation);
1647 std::optional<ccf::ClaimsDigest::Digest> digest =
1649 if (digest.has_value())
1651 auto digest_value = digest.value();
1652 ctx.rpc_ctx->set_claims_digest(std::move(digest_value));
1659 "/create", HTTP_POST,
json_adapter(create), no_auth_required)
1660 .set_openapi_hidden(
true)
1664 auto refresh_jwt_keys = [
this](
auto& ctx, nlohmann::json&& body) {
1668 if (!primary_id.has_value())
1672 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1673 ccf::errors::InternalError,
1674 "Primary is unknown");
1677 const auto& sig_auth_ident =
1678 ctx.template get_caller<ccf::NodeCertAuthnIdentity>();
1679 if (primary_id.value() != sig_auth_ident.node_id)
1682 "JWT key auto-refresh: request does not originate from primary");
1684 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1685 ccf::errors::InternalError,
1686 "Request does not originate from primary.");
1697 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1698 ccf::errors::InternalError,
1699 "Unable to parse body.");
1702 auto issuers = ctx.tx.ro(this->network.
jwt_issuers);
1703 auto issuer_metadata_ = issuers->get(parsed.
issuer);
1704 if (!issuer_metadata_.has_value())
1707 "JWT key auto-refresh: {} is not a valid issuer", parsed.
issuer);
1709 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1710 ccf::errors::InternalError,
1711 fmt::format(
"{} is not a valid issuer.", parsed.
issuer));
1713 auto& issuer_metadata = issuer_metadata_.value();
1715 if (!issuer_metadata.auto_refresh)
1718 "JWT key auto-refresh: {} does not have auto_refresh enabled",
1721 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1722 ccf::errors::InternalError,
1724 "{} does not have auto_refresh enabled.", parsed.
issuer));
1727 if (!set_jwt_public_signing_keys(
1735 "JWT key auto-refresh: error while storing signing keys for issuer "
1739 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1740 ccf::errors::InternalError,
1742 "Error while storing signing keys for issuer {}.",
1749 "/jwt_keys/refresh",
1752 {std::make_shared<NodeCertAuthnPolicy>()})
1753 .set_openapi_hidden(
true)
1756 auto get_jwt_metrics = [
this](
auto& args,
const nlohmann::json& params) {
1759 make_read_only_endpoint(
1760 "/jwt_keys/refresh/metrics",
1767 auto service_config_handler =
1768 [
this](
auto& args,
const nlohmann::json& params) {
1772 "/service/configuration",
1780 auto list_indexing_strategies = [
this](
1782 const nlohmann::json& params) {
1783 return make_success(this->context.get_indexing_strategies().describe());
1787 "/index/strategies",
1792 .set_auto_schema<void, nlohmann::json>()
1795 auto get_ready_app =
1797 auto node_configuration_subsystem =
1799 if (!node_configuration_subsystem)
1801 ctx.rpc_ctx->set_error(
1802 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1803 ccf::errors::InternalError,
1804 "NodeConfigurationSubsystem is not available");
1808 !node_configuration_subsystem->has_received_stop_notice() &&
1812 ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT);
1816 ctx.rpc_ctx->set_response_status(HTTP_STATUS_SERVICE_UNAVAILABLE);
1820 make_read_only_endpoint(
1821 "/ready/app", HTTP_GET, get_ready_app, no_auth_required)
1822 .set_auto_schema<void,
void>()
1826 auto get_ready_gov =
1828 auto node_configuration_subsystem =
1830 if (!node_configuration_subsystem)
1832 ctx.rpc_ctx->set_error(
1833 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1834 ccf::errors::InternalError,
1835 "NodeConfigurationSubsystem is not available");
1839 !node_configuration_subsystem->has_received_stop_notice() &&
1843 ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT);
1847 ctx.rpc_ctx->set_response_status(HTTP_STATUS_SERVICE_UNAVAILABLE);
1851 make_read_only_endpoint(
1852 "/ready/gov", HTTP_GET, get_ready_gov, no_auth_required)
1853 .set_auto_schema<void,
void>()
1857 static constexpr auto snapshot_since_param_key =
"since";
1860 auto node_configuration_subsystem =
1862 if (!node_configuration_subsystem)
1864 ctx.rpc_ctx->set_error(
1865 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1866 ccf::errors::InternalError,
1867 "NodeConfigurationSubsystem is not available");
1871 const auto& snapshots_config =
1874 size_t latest_idx = 0;
1877 const auto parsed_query =
1878 http::parse_query(ctx.rpc_ctx->get_request_query());
1880 std::string error_reason;
1881 auto snapshot_since = http::get_query_value_opt<ccf::SeqNo>(
1882 parsed_query, snapshot_since_param_key, error_reason);
1884 if (snapshot_since.has_value())
1886 if (error_reason !=
"")
1888 ctx.rpc_ctx->set_error(
1889 HTTP_STATUS_BAD_REQUEST,
1890 ccf::errors::InvalidQueryParameterValue,
1891 std::move(error_reason));
1894 latest_idx = snapshot_since.value();
1898 const auto orig_latest = latest_idx;
1899 auto latest_committed_snapshot =
1901 snapshots_config.directory, latest_idx);
1903 if (!latest_committed_snapshot.has_value())
1905 ctx.rpc_ctx->set_error(
1906 HTTP_STATUS_NOT_FOUND,
1907 ccf::errors::ResourceNotFound,
1909 "This node has no committed snapshots since {}", orig_latest));
1913 const auto& snapshot_path = latest_committed_snapshot.value();
1915 LOG_DEBUG_FMT(
"Redirecting to snapshot: {}", snapshot_path);
1917 auto redirect_url = fmt::format(
"/node/snapshot/{}", snapshot_path);
1918 ctx.rpc_ctx->set_response_header(
1919 ccf::http::headers::LOCATION, redirect_url);
1920 ctx.rpc_ctx->set_response_status(HTTP_STATUS_PERMANENT_REDIRECT);
1922 make_command_endpoint(
1923 "/snapshot", HTTP_HEAD, find_snapshot, no_auth_required)
1927 .set_openapi_hidden(
true)
1929 make_command_endpoint(
1930 "/snapshot", HTTP_GET, find_snapshot, no_auth_required)
1934 .set_openapi_hidden(
true)
1938 auto node_configuration_subsystem =
1940 if (!node_configuration_subsystem)
1942 ctx.rpc_ctx->set_error(
1943 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1944 ccf::errors::InternalError,
1945 "NodeConfigurationSubsystem is not available");
1949 const auto& snapshots_config =
1952 std::string snapshot_name;
1954 if (!get_path_param(
1955 ctx.rpc_ctx->get_request_path_params(),
1960 ctx.rpc_ctx->set_error(
1961 HTTP_STATUS_BAD_REQUEST,
1962 ccf::errors::InvalidResourceName,
1967 files::fs::path snapshot_path =
1968 files::fs::path(snapshots_config.directory) / snapshot_name;
1970 std::ifstream f(snapshot_path, std::ios::binary);
1973 ctx.rpc_ctx->set_error(
1974 HTTP_STATUS_NOT_FOUND,
1975 ccf::errors::ResourceNotFound,
1977 "This node does not have a snapshot named {}", snapshot_name));
1981 LOG_DEBUG_FMT(
"Found snapshot: {}", snapshot_path.string());
1984 const auto total_size = (size_t)f.tellg();
1986 ctx.rpc_ctx->set_response_header(
"accept-ranges",
"bytes");
1988 if (ctx.rpc_ctx->get_request_verb() == HTTP_HEAD)
1990 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1991 ctx.rpc_ctx->set_response_header(
1992 ccf::http::headers::CONTENT_LENGTH, total_size);
1996 size_t range_start = 0;
1997 size_t range_end = total_size;
1999 const auto range_header = ctx.rpc_ctx->get_request_header(
"range");
2000 if (range_header.has_value())
2002 LOG_TRACE_FMT(
"Parsing range header {}", range_header.value());
2004 auto [unit, ranges] =
2005 ccf::nonstd::split_1(range_header.value(),
"=");
2006 if (unit !=
"bytes")
2008 ctx.rpc_ctx->set_error(
2009 HTTP_STATUS_BAD_REQUEST,
2010 ccf::errors::InvalidHeaderValue,
2011 "Only 'bytes' is supported as a Range header unit");
2015 if (ranges.find(
",") != std::string::npos)
2017 ctx.rpc_ctx->set_error(
2018 HTTP_STATUS_BAD_REQUEST,
2019 ccf::errors::InvalidHeaderValue,
2020 "Multiple ranges are not supported");
2024 const auto segments = ccf::nonstd::split(ranges,
"-");
2025 if (segments.size() != 2)
2027 ctx.rpc_ctx->set_error(
2028 HTTP_STATUS_BAD_REQUEST,
2029 ccf::errors::InvalidHeaderValue,
2031 "Invalid format, cannot parse range in {}",
2032 range_header.value()));
2036 const auto s_range_start = segments[0];
2037 const auto s_range_end = segments[1];
2039 if (!s_range_start.empty())
2042 const auto [p, ec] = std::from_chars(
2043 s_range_start.begin(), s_range_start.end(), range_start);
2044 if (ec != std::errc())
2046 ctx.rpc_ctx->set_error(
2047 HTTP_STATUS_BAD_REQUEST,
2048 ccf::errors::InvalidHeaderValue,
2050 "Unable to parse start of range value {} in {}",
2052 range_header.value()));
2057 if (range_start > total_size)
2059 ctx.rpc_ctx->set_error(
2060 HTTP_STATUS_BAD_REQUEST,
2061 ccf::errors::InvalidHeaderValue,
2063 "Start of range {} is larger than total file size {}",
2069 if (!s_range_end.empty())
2073 const auto [p, ec] = std::from_chars(
2074 s_range_end.begin(), s_range_end.end(), range_end);
2075 if (ec != std::errc())
2077 ctx.rpc_ctx->set_error(
2078 HTTP_STATUS_BAD_REQUEST,
2079 ccf::errors::InvalidHeaderValue,
2081 "Unable to parse end of range value {} in {}",
2083 range_header.value()));
2088 if (range_end > total_size)
2090 ctx.rpc_ctx->set_error(
2091 HTTP_STATUS_BAD_REQUEST,
2092 ccf::errors::InvalidHeaderValue,
2094 "End of range {} is larger than total file size {}",
2100 if (range_end < range_start)
2102 ctx.rpc_ctx->set_error(
2103 HTTP_STATUS_BAD_REQUEST,
2104 ccf::errors::InvalidHeaderValue,
2106 "Invalid range: Start ({}) and end ({}) out of order",
2115 range_end = total_size;
2120 if (!s_range_end.empty())
2124 const auto [p, ec] = std::from_chars(
2125 s_range_end.begin(), s_range_end.end(), offset);
2126 if (ec != std::errc())
2128 ctx.rpc_ctx->set_error(
2129 HTTP_STATUS_BAD_REQUEST,
2130 ccf::errors::InvalidHeaderValue,
2132 "Unable to parse end of range offset value {} in {}",
2134 range_header.value()));
2138 range_end = total_size;
2139 range_start = range_end - offset;
2143 ctx.rpc_ctx->set_error(
2144 HTTP_STATUS_BAD_REQUEST,
2145 ccf::errors::InvalidHeaderValue,
2146 "Invalid range: Must contain range-start or range-end");
2153 const auto range_size = range_end - range_start;
2156 "Reading {}-byte range from {} to {}",
2162 std::vector<uint8_t> contents(range_size);
2163 f.seekg(range_start);
2164 f.read((
char*)contents.data(), contents.size());
2168 ctx.rpc_ctx->set_response_status(HTTP_STATUS_PARTIAL_CONTENT);
2169 ctx.rpc_ctx->set_response_header(
2170 ccf::http::headers::CONTENT_TYPE,
2171 ccf::http::headervalues::contenttype::OCTET_STREAM);
2172 ctx.rpc_ctx->set_response_body(std::move(contents));
2176 ctx.rpc_ctx->set_response_header(
2178 fmt::format(
"bytes {}-{}/{}", range_start, range_end, total_size));
2180 make_command_endpoint(
2181 "/snapshot/{snapshot_name}", HTTP_HEAD, get_snapshot, no_auth_required)
2183 .set_openapi_hidden(
true)
2185 make_command_endpoint(
2186 "/snapshot/{snapshot_name}", HTTP_GET, get_snapshot, no_auth_required)
2188 .set_openapi_hidden(
true)