CCF
Loading...
Searching...
No Matches
file_serving_handlers.h
Go to the documentation of this file.
1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the Apache 2.0 License.
3#pragma once
4
6#include "ccf/crypto/base64.h"
8#include "ccf/http_etag.h"
10#include "http/http_digest.h"
12#include "snapshots/filenames.h"
13
14namespace ccf::node
15{
16 // Compute and format the Repr-Digest header value for the given algorithm
17 // and data.
18 static std::string format_repr_digest(
19 const std::string& algo_name,
21 const uint8_t* data,
22 size_t size)
23 {
25 auto digest = hp->hash(data, size, md);
26 auto b64 = ccf::crypto::b64_from_raw(digest.data(), digest.size());
27 return fmt::format("{}=:{}:", algo_name, b64);
28 }
29
30 // Helper function to lookup redirect address based on the interface on this
31 // node which received the request. Will either return an address, or
32 // populate an appropriate error on the response context.
33 // Takes both CommandEndpointContext and ReadOnlyTx, so that it can be
34 // called be either read-only or read-write endpoints
35 static std::optional<std::string> get_redirect_address_for_node(
38 const ccf::NodeId& target_node)
39 {
40 auto* nodes = ro_tx.ro<ccf::Nodes>(ccf::Tables::NODES);
41
42 auto node_info = nodes->get(target_node);
43 if (!node_info.has_value())
44 {
45 LOG_FAIL_FMT("Node redirection error: Unknown node {}", target_node);
46 ctx.rpc_ctx->set_error(
47 HTTP_STATUS_INTERNAL_SERVER_ERROR,
48 ccf::errors::InternalError,
49 fmt::format(
50 "Cannot find node info to produce redirect response for node {}",
51 target_node));
52 return std::nullopt;
53 }
54
55 const auto interface_id = ctx.rpc_ctx->get_session_context()->interface_id;
56 if (!interface_id.has_value())
57 {
58 LOG_FAIL_FMT("Node redirection error: Non-RPC request");
59 ctx.rpc_ctx->set_error(
60 HTTP_STATUS_INTERNAL_SERVER_ERROR,
61 ccf::errors::InternalError,
62 "Cannot redirect non-RPC request");
63 return std::nullopt;
64 }
65
66 const auto& interfaces = node_info->rpc_interfaces;
67 const auto interface_it = interfaces.find(interface_id.value());
68 if (interface_it == interfaces.end())
69 {
71 "Node redirection error: Target missing interface {}",
72 interface_id.value());
73 ctx.rpc_ctx->set_error(
74 HTTP_STATUS_INTERNAL_SERVER_ERROR,
75 ccf::errors::InternalError,
76 fmt::format(
77 "Cannot redirect request. Received on RPC interface {}, which is "
78 "not present on target node {}",
79 interface_id.value(),
80 target_node));
81 return std::nullopt;
82 }
83
84 const auto& interface = interface_it->second;
85 return interface.published_address;
86 }
87
88 // Helper function to redirect to the next node in order after self_node_id
89 // using node IDs as a sorting key, and wrapping around to the lowest ID.
90 // Will either return an address, or populate an appropriate error on the
91 // response context.
92 static std::optional<std::string> get_redirect_address_for_next_node(
95 const ccf::NodeId& self_node_id)
96 {
97 auto* nodes = ro_tx.ro<ccf::Nodes>(ccf::Tables::NODES);
98 std::set<ccf::NodeId> other_node_ids;
99 nodes->foreach([&](const ccf::NodeId& node_id, const ccf::NodeInfo&) {
100 if (node_id != self_node_id)
101 {
102 other_node_ids.insert(node_id);
103 }
104 return true;
105 });
106
107 if (other_node_ids.empty())
108 {
110 "Node redirection error: No other nodes present in the network");
111 ctx.rpc_ctx->set_error(
112 HTTP_STATUS_INTERNAL_SERVER_ERROR,
113 ccf::errors::InternalError,
114 "Cannot redirect request. No other nodes present in the network");
115 return std::nullopt;
116 }
117
118 auto it = other_node_ids.upper_bound(self_node_id);
119 std::optional<ccf::NodeId> next_node_id;
120 if (it != other_node_ids.end())
121 {
122 next_node_id = *it;
123 }
124 else
125 {
126 next_node_id = *other_node_ids.begin();
127 }
128
129 return get_redirect_address_for_node(ctx, ro_tx, next_node_id.value());
130 }
131
132 // Helper function to get NodeConfigurationSubsystem from NodeContext,
133 // and populate error on ctx and log if not available
134 static std::shared_ptr<NodeConfigurationSubsystem>
135 get_node_configuration_subsystem(
136 ccf::AbstractNodeContext& node_context,
138 {
139 auto node_configuration_subsystem =
141 if (node_configuration_subsystem == nullptr)
142 {
144 "NodeConfigurationSubsystem is not available in NodeContext");
145 ctx.rpc_ctx->set_error(
146 HTTP_STATUS_INTERNAL_SERVER_ERROR,
147 ccf::errors::InternalError,
148 "NodeConfigurationSubsystem is not available");
149 }
150 return node_configuration_subsystem;
151 }
152
153 // Helper function to serve byte ranges from a file stream.
154 // This populates the response body, and range-related response headers. This
155 // may produce an error response if an invalid range was requested.
156 //
157 // If the request contains a Want-Repr-Digest header, the Repr-Digest
158 // response header is set with the digest of the full file (RFC 9530),
159 // regardless of any Range header.
160 //
161 // An ETag header is always set on successful responses, containing a
162 // SHA-256 digest of the response content in RFC 9530 structured field
163 // format. If an If-None-Match request header is present and matches
164 // the ETag, a 304 Not Modified response is returned instead. This
165 // applies to both GET and HEAD requests.
166 //
167 // This DOES NOT set a response header telling the client the name of the
168 // snapshot/chunk/... being served, so the caller should set this (along
169 // with any other metadata headers) _before_ calling this function, and
170 // generally avoid modifying the response further _after_ calling this
171 // function.
172 static void fill_range_response_from_file(
173 ccf::endpoints::CommandEndpointContext& ctx, std::ifstream& f)
174 {
175 f.seekg(0, std::ifstream::end);
176 const auto total_size = (size_t)f.tellg();
177
178 if (total_size == 0)
179 {
180 // Refuse to return an empty file - it's not going to be a valid snapshot
181 // or ledger chunk, and cannot be described in a Content-Range response
182 // header
183 ctx.rpc_ctx->set_error(
184 HTTP_STATUS_INTERNAL_SERVER_ERROR,
185 ccf::errors::EmptyFile,
186 "Found empty file");
187 return;
188 }
189
190 const bool is_head = ctx.rpc_ctx->get_request_verb() == HTTP_HEAD;
191
192 ctx.rpc_ctx->set_response_header("accept-ranges", "bytes");
193
194 // Parse Want-Repr-Digest if present
195 const auto want_digest =
196 ctx.rpc_ctx->get_request_header(ccf::http::headers::WANT_REPR_DIGEST);
197 std::optional<std::pair<std::string, ccf::crypto::MDType>> digest_algo;
198 if (want_digest.has_value())
199 {
200 digest_algo = ccf::http::parse_want_repr_digest(want_digest.value());
201 }
202
203 size_t range_start = 0;
204 size_t range_end = total_size;
205 bool has_range_header = false;
206 {
207 const auto range_header =
208 ctx.rpc_ctx->get_request_header(ccf::http::headers::RANGE);
209 if (range_header.has_value())
210 {
211 has_range_header = true;
212 LOG_TRACE_FMT("Parsing range header {}", range_header.value());
213
214 auto [unit, ranges] = ccf::nonstd::split_1(range_header.value(), "=");
215 if (unit != "bytes")
216 {
217 ctx.rpc_ctx->set_error(
218 HTTP_STATUS_BAD_REQUEST,
219 ccf::errors::InvalidHeaderValue,
220 "Only 'bytes' is supported as a Range header unit");
221 return;
222 }
223
224 if (ranges.find(',') != std::string::npos)
225 {
226 ctx.rpc_ctx->set_error(
227 HTTP_STATUS_BAD_REQUEST,
228 ccf::errors::InvalidHeaderValue,
229 "Multiple ranges are not supported");
230 return;
231 }
232
233 const auto segments = ccf::nonstd::split(ranges, "-");
234 if (segments.size() != 2)
235 {
236 ctx.rpc_ctx->set_error(
237 HTTP_STATUS_BAD_REQUEST,
238 ccf::errors::InvalidHeaderValue,
239 fmt::format(
240 "Invalid format, cannot parse range in {}",
241 range_header.value()));
242 return;
243 }
244
245 const auto s_range_start = segments[0];
246 const auto s_range_end = segments[1];
247
248 if (!s_range_start.empty())
249 {
250 {
251 const auto [p, ec] = std::from_chars(
252 s_range_start.begin(), s_range_start.end(), range_start);
253 if (ec != std::errc())
254 {
255 ctx.rpc_ctx->set_error(
256 HTTP_STATUS_BAD_REQUEST,
257 ccf::errors::InvalidHeaderValue,
258 fmt::format(
259 "Unable to parse start of range value {} in {}",
260 s_range_start,
261 range_header.value()));
262 return;
263 }
264 }
265
266 if (range_start > total_size)
267 {
268 ctx.rpc_ctx->set_error(
269 HTTP_STATUS_BAD_REQUEST,
270 ccf::errors::InvalidHeaderValue,
271 fmt::format(
272 "Start of range {} is larger than total file size {}",
273 range_start,
274 total_size));
275 return;
276 }
277
278 if (!s_range_end.empty())
279 {
280 // Range end in header is inclusive, but we prefer to reason about
281 // exclusive range end (ie - one past the end)
282 size_t inclusive_range_end = 0;
283
284 // Fully-specified range, like "X-Y"
285 {
286 const auto [p, ec] = std::from_chars(
287 s_range_end.begin(), s_range_end.end(), inclusive_range_end);
288 if (ec != std::errc())
289 {
290 ctx.rpc_ctx->set_error(
291 HTTP_STATUS_BAD_REQUEST,
292 ccf::errors::InvalidHeaderValue,
293 fmt::format(
294 "Unable to parse end of range value {} in {}",
295 s_range_end,
296 range_header.value()));
297 return;
298 }
299 }
300
301 range_end = inclusive_range_end + 1;
302
303 if (range_end > total_size)
304 {
306 "Requested ledger chunk range ending at {}, but file size is "
307 "only {} - shrinking range end",
308 range_end,
309 total_size);
310 range_end = total_size;
311 }
312
313 if (range_end < range_start)
314 {
315 ctx.rpc_ctx->set_error(
316 HTTP_STATUS_BAD_REQUEST,
317 ccf::errors::InvalidHeaderValue,
318 fmt::format(
319 "Invalid range: Start ({}) and end ({}) out of order",
320 range_start,
321 range_end));
322 return;
323 }
324 }
325 else
326 {
327 // Else this is an open-ended range like "X-"
328 range_end = total_size;
329 }
330 }
331 else
332 {
333 if (!s_range_end.empty())
334 {
335 // Negative range, like "-Y"
336 size_t offset = 0;
337 const auto [p, ec] =
338 std::from_chars(s_range_end.begin(), s_range_end.end(), offset);
339 if (ec != std::errc())
340 {
341 ctx.rpc_ctx->set_error(
342 HTTP_STATUS_BAD_REQUEST,
343 ccf::errors::InvalidHeaderValue,
344 fmt::format(
345 "Unable to parse end of range offset value {} in {}",
346 s_range_end,
347 range_header.value()));
348 return;
349 }
350
351 range_end = total_size;
352 range_start = range_end - offset;
353 }
354 else
355 {
356 ctx.rpc_ctx->set_error(
357 HTTP_STATUS_BAD_REQUEST,
358 ccf::errors::InvalidHeaderValue,
359 "Invalid range: Must contain range-start or range-end");
360 return;
361 }
362 }
363 }
364 }
365
366 const auto range_size = range_end - range_start;
367
369 "Reading {}-byte range from {} to {}",
370 range_size,
371 range_start,
372 range_end);
373
374 // Read file contents. We need the range content for the response body
375 // (GET) or to compute ETag/Repr-Digest (both GET and HEAD).
376 // If Repr-Digest is requested, read the full file; otherwise read
377 // only the requested range.
378 std::vector<uint8_t> contents;
379 if (digest_algo.has_value())
380 {
381 // Need full file contents for the digest
382 f.seekg(0);
383 std::vector<uint8_t> full_contents(total_size);
384 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
385 f.read(reinterpret_cast<char*>(full_contents.data()), total_size);
386 f.close();
387
388 auto bytes_read = static_cast<size_t>(f.gcount());
389 if (bytes_read < range_end)
390 {
391 ctx.rpc_ctx->set_error(
392 HTTP_STATUS_INTERNAL_SERVER_ERROR,
393 ccf::errors::InternalError,
394 "Server was unable to read the file correctly");
395 return;
396 }
397
398 if (bytes_read == total_size)
399 {
400 ctx.rpc_ctx->set_response_header(
401 ccf::http::headers::REPR_DIGEST,
402 format_repr_digest(
403 digest_algo->first,
404 digest_algo->second,
405 full_contents.data(),
406 full_contents.size()));
407 }
408
409 // Extract the requested range
410 contents.assign(
411 full_contents.begin() + range_start, full_contents.begin() + range_end);
412 }
413 else
414 {
415 // Read only the requested range
416 contents.resize(range_size);
417 f.seekg(range_start);
418 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
419 f.read(reinterpret_cast<char*>(contents.data()), contents.size());
420 f.close();
421 }
422
423 // Compute ETag over the response content (RFC 9530 structured field
424 // format)
425 auto hash_provider = ccf::crypto::make_hash_provider();
426 auto sha256_hash = hash_provider->hash(
427 contents.data(), contents.size(), ccf::crypto::MDType::SHA256);
428 auto sha256_b64 =
429 ccf::crypto::b64_from_raw(sha256_hash.data(), sha256_hash.size());
430 auto sha256_etag = fmt::format("sha-256=:{}:", sha256_b64);
431
432 ctx.rpc_ctx->set_response_header(
433 ccf::http::headers::ETAG, fmt::format("\"{}\"", sha256_etag));
434
435 // Check If-None-Match header
436 const auto if_none_match =
437 ctx.rpc_ctx->get_request_header(ccf::http::headers::IF_NONE_MATCH);
438 if (if_none_match.has_value())
439 {
440 try
441 {
442 ccf::http::Matcher matcher(if_none_match.value());
443
444 bool matched = matcher.is_any() || matcher.matches(sha256_etag);
445
446 if (!matched)
447 {
448 auto sha384_hash = hash_provider->hash(
449 contents.data(), contents.size(), ccf::crypto::MDType::SHA384);
450 auto sha384_b64 =
451 ccf::crypto::b64_from_raw(sha384_hash.data(), sha384_hash.size());
452 matched = matcher.matches(fmt::format("sha-384=:{}:", sha384_b64));
453 }
454
455 if (!matched)
456 {
457 auto sha512_hash = hash_provider->hash(
458 contents.data(), contents.size(), ccf::crypto::MDType::SHA512);
459 auto sha512_b64 =
460 ccf::crypto::b64_from_raw(sha512_hash.data(), sha512_hash.size());
461 matched = matcher.matches(fmt::format("sha-512=:{}:", sha512_b64));
462 }
463
464 if (matched)
465 {
466 ctx.rpc_ctx->set_response_status(HTTP_STATUS_NOT_MODIFIED);
467 ctx.rpc_ctx->set_response_body(std::vector<uint8_t>{});
468 return;
469 }
470 }
471 catch (const ccf::http::MatcherError& e)
472 {
473 ctx.rpc_ctx->set_error(
474 HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidHeaderValue, e.what());
475 return;
476 }
477 }
478
479 // Build successful response
480 ctx.rpc_ctx->set_response_header(
481 ccf::http::headers::CONTENT_TYPE,
482 ccf::http::headervalues::contenttype::OCTET_STREAM);
483
484 if (has_range_header)
485 {
486 ctx.rpc_ctx->set_response_status(HTTP_STATUS_PARTIAL_CONTENT);
487
488 // Convert back to HTTP-style inclusive range end
489 const auto inclusive_range_end = range_end - 1;
490
491 // Partial Content responses describe the current response in
492 // Content-Range
493 ctx.rpc_ctx->set_response_header(
494 ccf::http::headers::CONTENT_RANGE,
495 fmt::format(
496 "bytes {}-{}/{}", range_start, inclusive_range_end, total_size));
497 }
498 else
499 {
500 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
501 }
502
503 if (is_head)
504 {
505 // HEAD responses should not include a body, but should include
506 // Content-Length indicating the size of the resource
507 ctx.rpc_ctx->set_response_header(
508 ccf::http::headers::CONTENT_LENGTH,
509 has_range_header ? range_size : total_size);
510 }
511 else
512 {
513 ctx.rpc_ctx->set_response_body(std::move(contents));
514 }
515 }
516
517 static void init_file_serving_handlers(
519 {
520 static constexpr auto file_since_param_key = "since";
521
522 auto find_snapshot = [&](ccf::endpoints::ReadOnlyEndpointContext& ctx) {
523 size_t latest_idx = 0;
524 {
525 // Get latest_idx from query param, if present
526 const auto parsed_query =
527 http::parse_query(ctx.rpc_ctx->get_request_query());
528
529 std::string error_reason;
530 auto snapshot_since = http::get_query_value_opt<ccf::SeqNo>(
531 parsed_query, file_since_param_key, error_reason);
532
533 if (snapshot_since.has_value())
534 {
535 if (!error_reason.empty())
536 {
537 ctx.rpc_ctx->set_error(
538 HTTP_STATUS_BAD_REQUEST,
539 ccf::errors::InvalidQueryParameterValue,
540 std::move(error_reason));
541 return;
542 }
543 latest_idx = snapshot_since.value();
544 }
545 }
546
547 auto node_operation = node_context.get_subsystem<AbstractNodeOperation>();
548 if (node_operation == nullptr)
549 {
550 ctx.rpc_ctx->set_error(
551 HTTP_STATUS_INTERNAL_SERVER_ERROR,
552 ccf::errors::InternalError,
553 "Unable to access NodeOperation subsystem");
554 return;
555 }
556
557 if (!node_operation->can_replicate())
558 {
559 // Try to redirect to primary for preferable snapshot, expected to
560 // match later /join request
561 auto primary_id = node_operation->get_primary();
562 if (primary_id.has_value())
563 {
564 const auto address =
565 get_redirect_address_for_node(ctx, ctx.tx, *primary_id);
566 if (!address.has_value())
567 {
568 return;
569 }
570
571 auto location =
572 fmt::format("https://{}/node/snapshot", address.value());
573 if (latest_idx != 0)
574 {
575 location += fmt::format("?{}={}", file_since_param_key, latest_idx);
576 }
577
578 ctx.rpc_ctx->set_response_header(http::headers::LOCATION, location);
579 ctx.rpc_ctx->set_error(
580 HTTP_STATUS_PERMANENT_REDIRECT,
581 ccf::errors::NodeCannotHandleRequest,
582 "Node is not primary; redirecting for preferable snapshot");
583 return;
584 }
585
586 // If there is no current primary, fall-back to returning this
587 // node's best snapshot rather than terminating the fetch with an
588 // error
589 }
590
591 auto node_configuration_subsystem =
592 get_node_configuration_subsystem(node_context, ctx);
593 if (node_configuration_subsystem == nullptr)
594 {
595 return;
596 }
597
598 const auto& snapshots_config =
599 node_configuration_subsystem->get().node_config.snapshots;
600
601 const auto orig_latest = latest_idx;
602 auto latest_committed_snapshot =
604 snapshots_config.directory, latest_idx);
605
606 if (!latest_committed_snapshot.has_value())
607 {
608 ctx.rpc_ctx->set_error(
609 HTTP_STATUS_NOT_FOUND,
610 ccf::errors::ResourceNotFound,
611 fmt::format(
612 "This node has no committed snapshots since {}", orig_latest));
613 return;
614 }
615
616 const auto& snapshot_name = latest_committed_snapshot->filename();
617
618 const auto address =
619 get_redirect_address_for_node(ctx, ctx.tx, node_context.get_node_id());
620 if (!address.has_value())
621 {
622 return;
623 }
624
625 auto redirect_url = fmt::format(
626 "https://{}/node/snapshot/{}", address.value(), snapshot_name);
627 LOG_DEBUG_FMT("Redirecting to snapshot: {}", redirect_url);
628 ctx.rpc_ctx->set_response_header(
629 ccf::http::headers::LOCATION, redirect_url);
630 ctx.rpc_ctx->set_response_status(HTTP_STATUS_PERMANENT_REDIRECT);
631 };
632 registry
634 "/snapshot", HTTP_HEAD, find_snapshot, no_auth_required)
637 file_since_param_key, ccf::endpoints::OptionalParameter)
638 .require_operator_feature(endpoints::OperatorFeature::SnapshotRead)
639 .install();
640 registry
642 "/snapshot", HTTP_GET, find_snapshot, no_auth_required)
645 file_since_param_key, ccf::endpoints::OptionalParameter)
646 .require_operator_feature(endpoints::OperatorFeature::SnapshotRead)
647 .install();
648
649 // Find a ledger chunk that includes the since value
650 auto find_chunk = [&](ccf::endpoints::ReadOnlyEndpointContext& ctx) {
651 size_t since_idx = 0;
652 {
653 // Get since_idx from query param, if present
654 const auto parsed_query =
655 http::parse_query(ctx.rpc_ctx->get_request_query());
656
657 std::string error_reason;
658 auto chunk_since = http::get_query_value_opt<ccf::SeqNo>(
659 parsed_query, file_since_param_key, error_reason);
660
661 if (chunk_since.has_value())
662 {
663 if (!error_reason.empty())
664 {
665 ctx.rpc_ctx->set_error(
666 HTTP_STATUS_BAD_REQUEST,
667 ccf::errors::InvalidQueryParameterValue,
668 std::move(error_reason));
669 return;
670 }
671 since_idx = chunk_since.value();
672 }
673 else
674 {
675 ctx.rpc_ctx->set_error(
676 HTTP_STATUS_BAD_REQUEST,
677 ccf::errors::InvalidQueryParameterValue,
678 fmt::format(
679 "Missing required query parameter '{}'", file_since_param_key));
680 return;
681 }
682 }
683 LOG_DEBUG_FMT("Finding ledger chunk including index {}", since_idx);
684
685 auto node_operation = node_context.get_subsystem<AbstractNodeOperation>();
686 if (node_operation == nullptr)
687 {
688 ctx.rpc_ctx->set_error(
689 HTTP_STATUS_INTERNAL_SERVER_ERROR,
690 ccf::errors::InternalError,
691 "Unable to access NodeOperation subsystem");
692 return;
693 }
694
695 auto address =
696 get_redirect_address_for_node(ctx, ctx.tx, node_context.get_node_id());
697 if (!address.has_value())
698 {
699 return;
700 }
701
702 auto read_ledger_subsystem =
704 if (read_ledger_subsystem == nullptr)
705 {
706 ctx.rpc_ctx->set_error(
707 HTTP_STATUS_INTERNAL_SERVER_ERROR,
708 ccf::errors::InternalError,
709 "LedgerReadSubsystem is not available");
710 return;
711 }
712
713 const auto chunk_path =
714 read_ledger_subsystem->committed_ledger_path_with_idx(since_idx);
715
716 // If the file is found locally, always serve it from this node
717 if (chunk_path.has_value())
718 {
719 const auto chunk_filename = chunk_path.value().filename();
720
721 auto redirect_url = fmt::format(
722 "https://{}/node/ledger_chunk/{}", address.value(), chunk_filename);
723 LOG_DEBUG_FMT("Redirecting to ledger chunk: {}", redirect_url);
724 ctx.rpc_ctx->set_response_header(
725 ccf::http::headers::LOCATION, redirect_url);
726 ctx.rpc_ctx->set_response_status(HTTP_STATUS_PERMANENT_REDIRECT);
727 return;
728 }
729
730 // Otherwise, if the file is before our init index, i.e. where we started
731 // replicating, redirect to the next node in order.
732 const size_t init_idx = read_ledger_subsystem->get_init_idx();
733 if (since_idx < init_idx)
734 {
736 "This node cannot serve ledger chunk including index {} which is "
737 "before its init index {} - trying to redirect to next node",
738 since_idx,
739 init_idx);
740
741 address = get_redirect_address_for_next_node(
742 ctx, ctx.tx, node_context.get_node_id());
743 if (!address.has_value())
744 {
745 return;
746 }
747
748 auto location = fmt::format(
749 "https://{}/node/ledger_chunk?{}={}",
750 address.value(),
751 file_since_param_key,
752 since_idx);
753 ctx.rpc_ctx->set_response_header(http::headers::LOCATION, location);
754 ctx.rpc_ctx->set_error(
755 HTTP_STATUS_PERMANENT_REDIRECT,
756 ccf::errors::NodeCannotHandleRequest,
757 "Node does not have ledger chunk; redirecting to next node");
758 return;
759 }
760
761 // If the file is beyond our init index, but we do not have it, we are
762 // probably a backup and lagging behind. Redirect to primary.
763 if (!node_operation->can_replicate())
764 {
766 "This node cannot serve ledger chunk including index {} - trying "
767 "to redirect to primary",
768 since_idx);
769 auto primary_id = node_operation->get_primary();
770 if (primary_id.has_value())
771 {
772 address = get_redirect_address_for_node(ctx, ctx.tx, *primary_id);
773 if (address.has_value())
774 {
775 auto location =
776 fmt::format("https://{}/node/ledger_chunk", address.value());
777 location += fmt::format("?{}={}", file_since_param_key, since_idx);
778
779 ctx.rpc_ctx->set_response_header(http::headers::LOCATION, location);
780 ctx.rpc_ctx->set_error(
781 HTTP_STATUS_PERMANENT_REDIRECT,
782 ccf::errors::NodeCannotHandleRequest,
783 fmt::format(
784 "Ledger chunk including index {} not found locally; "
785 "redirecting to primary",
786 since_idx));
787 return;
788 }
789 }
790 }
791
792 // Redirect possibilities exhausted
793 ctx.rpc_ctx->set_error(
794 HTTP_STATUS_NOT_FOUND,
795 ccf::errors::ResourceNotFound,
796 fmt::format(
797 "This node has no ledger chunk including index {}", since_idx));
798 return;
799 };
800 registry
802 "/ledger_chunk", HTTP_HEAD, find_chunk, no_auth_required)
805 file_since_param_key, ccf::endpoints::RequiredParameter)
806 .require_operator_feature(endpoints::OperatorFeature::LedgerChunkRead)
807 .set_openapi_summary("Ledger chunk metadata")
809 "Redirect to the corresponding /node/ledger_chunk/{chunk_name} "
810 "endpoint for the ledger chunk including the sequence number specified "
811 "in the 'since' query parameter.")
812 .install();
813 registry
815 "/ledger_chunk", HTTP_GET, find_chunk, no_auth_required)
818 file_since_param_key, ccf::endpoints::RequiredParameter)
819 .require_operator_feature(endpoints::OperatorFeature::LedgerChunkRead)
820 .set_openapi_summary("Download ledger chunk")
822 "Redirect to the corresponding /node/ledger_chunk/{chunk_name} "
823 "endpoint for the ledger chunk including the sequence number specified "
824 "in the 'since' query parameter.")
825 .install();
826
827 auto get_snapshot = [&](ccf::endpoints::CommandEndpointContext& ctx) {
828 auto node_configuration_subsystem =
829 get_node_configuration_subsystem(node_context, ctx);
830 if (node_configuration_subsystem == nullptr)
831 {
832 return;
833 }
834
835 const auto& snapshots_config =
836 node_configuration_subsystem->get().node_config.snapshots;
837
838 std::string snapshot_name;
839 std::string error;
841 ctx.rpc_ctx->get_request_path_params(),
842 "snapshot_name",
843 snapshot_name,
844 error))
845 {
846 ctx.rpc_ctx->set_error(
847 HTTP_STATUS_BAD_REQUEST,
848 ccf::errors::InvalidResourceName,
849 std::move(error));
850 return;
851 }
852
853 files::fs::path snapshot_path =
854 files::fs::path(snapshots_config.directory) / snapshot_name;
855
856 std::ifstream f(snapshot_path, std::ios::binary);
857 if (!f.good())
858 {
859 ctx.rpc_ctx->set_error(
860 HTTP_STATUS_NOT_FOUND,
861 ccf::errors::ResourceNotFound,
862 fmt::format(
863 "This node does not have a snapshot named {}", snapshot_name));
864 return;
865 }
866
867 LOG_DEBUG_FMT("Found snapshot: {}", snapshot_path.string());
868
869 ctx.rpc_ctx->set_response_header(
870 ccf::http::headers::CCF_SNAPSHOT_NAME, snapshot_name);
871
872 fill_range_response_from_file(ctx, f);
873 return;
874 };
875 registry
877 "/snapshot/{snapshot_name}", HTTP_HEAD, get_snapshot, no_auth_required)
880 .install();
881 registry
883 "/snapshot/{snapshot_name}", HTTP_GET, get_snapshot, no_auth_required)
886 .install();
887
888 auto get_ledger_chunk = [&](ccf::endpoints::CommandEndpointContext& ctx) {
889 auto node_configuration_subsystem =
890 get_node_configuration_subsystem(node_context, ctx);
891 if (node_configuration_subsystem == nullptr)
892 {
893 return;
894 }
895
896 const auto& ledger_config =
897 node_configuration_subsystem->get().node_config.ledger;
898
899 std::string chunk_name;
900 std::string error;
902 ctx.rpc_ctx->get_request_path_params(),
903 "chunk_name",
904 chunk_name,
905 error))
906 {
907 ctx.rpc_ctx->set_error(
908 HTTP_STATUS_BAD_REQUEST,
909 ccf::errors::InvalidResourceName,
910 std::move(error));
911 return;
912 }
913
914 LOG_DEBUG_FMT("Fetching ledger chunk {}", chunk_name);
915
916 files::fs::path chunk_path =
917 files::fs::path(ledger_config.directory) / chunk_name;
918
919 std::ifstream f(chunk_path, std::ios::binary);
920 if (!f.good())
921 {
922 ctx.rpc_ctx->set_error(
923 HTTP_STATUS_NOT_FOUND,
924 ccf::errors::ResourceNotFound,
925 fmt::format(
926 "This node does not have a ledger chunk named {}", chunk_name));
927 return;
928 }
929
930 LOG_DEBUG_FMT("Found ledger chunk: {}", chunk_path.string());
931
932 ctx.rpc_ctx->set_response_header(
933 ccf::http::headers::CCF_LEDGER_CHUNK_NAME, chunk_name);
934
935 fill_range_response_from_file(ctx, f);
936
937 return;
938 };
939 registry
941 "/ledger_chunk/{chunk_name}",
942 HTTP_HEAD,
943 get_ledger_chunk,
944 no_auth_required)
947 .set_openapi_summary("Ledger chunk metadata")
949 "Metadata about a specific ledger chunk (Content-Length and "
950 "x-ms-ccf-ledger-chunk-name)")
951 .install();
952 registry
954 "/ledger_chunk/{chunk_name}",
955 HTTP_GET,
956 get_ledger_chunk,
957 no_auth_required)
960 .set_openapi_summary("Download ledger chunk")
962 "Download a specific ledger chunk by name. Supports HTTP Range header "
963 "for partial downloads.")
964 .install();
965 }
966}
Definition node_operation_interface.h:24
Definition base_endpoint_registry.h:121
Definition node_configuration_subsystem.h:13
Definition ledger_subsystem.h:11
virtual Endpoint make_command_endpoint(const std::string &method, RESTVerb verb, const CommandEndpointFunction &f, const AuthnPolicies &ap)
Definition endpoint_registry.cpp:332
virtual Endpoint make_read_only_endpoint(const std::string &method, RESTVerb verb, const ReadOnlyEndpointFunction &f, const AuthnPolicies &ap)
Definition endpoint_registry.cpp:313
Definition http_etag.h:15
const char * what() const noexcept override
Definition http_etag.h:22
Definition http_etag.h:32
bool matches(const std::string &etag) const
Check if a given ETag matches the If-Match/If-None-Match header.
Definition http_etag.h:78
bool is_any() const
Check if the header will match any ETag (*)
Definition http_etag.h:84
Definition tx.h:159
M::ReadOnlyHandle * ro(M &m)
Definition tx.h:168
Definition map.h:30
#define LOG_TRACE_FMT
Definition internal_logger.h:13
#define LOG_DEBUG_FMT
Definition internal_logger.h:14
#define LOG_FAIL_FMT
Definition internal_logger.h:16
std::string b64_from_raw(const uint8_t *data, size_t size)
Definition base64.cpp:41
MDType
Definition md_type.h:10
std::shared_ptr< HashProvider > make_hash_provider()
Definition hash.cpp:41
@ RequiredParameter
Definition endpoint.h:122
@ OptionalParameter
Definition endpoint.h:123
bool get_path_param(const ccf::PathParams &params, const std::string &param_name, T &value, std::string &error)
Definition endpoint_registry.h:75
Definition file_serving_handlers.h:15
@ error
Definition tls_session.h:23
uint64_t SeqNo
Definition tx_id.h:36
std::optional< fs::path > find_latest_committed_snapshot_in_directory(const fs::path &directory, std::optional< size_t > minimum_idx=std::nullopt)
Definition filenames.h:238
Definition node_context.h:12
virtual ccf::NodeId get_node_id() const
Definition node_context.h:65
std::shared_ptr< T > get_subsystem() const
Definition node_context.h:60
Definition node_info.h:30
Definition endpoint_context.h:27
std::shared_ptr< ccf::RpcContext > rpc_ctx
Definition endpoint_context.h:34
Endpoint & add_query_parameter(const std::string &param_name, QueryParamPresence presence=QueryParamPresence::RequiredParameter)
Definition endpoint.h:449
Endpoint & require_operator_feature(OperatorFeature feature)
Definition endpoint.cpp:16
Endpoint & set_openapi_description(const std::string &description)
Definition endpoint.cpp:106
void install()
Definition endpoint.cpp:135
Endpoint & set_forwarding_required(ForwardingRequired fr)
Definition endpoint.cpp:74
Endpoint & set_openapi_summary(const std::string &summary)
Definition endpoint.cpp:112
Definition endpoint_context.h:86