From 27767690ca7d7c5b44172a857581f08c728a740d Mon Sep 17 00:00:00 2001 From: hinto-janai Date: Mon, 5 Aug 2024 19:50:38 -0400 Subject: [PATCH] rpc: impl `cuprate-rpc-interface` (#233) * fixed-bytes: add `serde`, document feature flags * fixed-bytes: add derives * rpc: add `as _` syntax to macro * rpc: use `ByteArrayVec` and `ContainerAsBlob` for binary types * fixed-bytes: re-add derives * rpc-types: dedup default value within macro * readme: fixed bytes section * types: custom epee - `BlockCompleteEntry` * types: custom epee - `KeyImageSpentStatus` * types: custom epee - `PoolInfoExtent` * types: add `Status::Other(String)` variant * types: custom epee - `TxEntry`, add `read_epee_field` macro * bin: custom epee - `GetBlocks` * types: add `serde.rs` * misc: make `TxEntry` an `enum`, impl serde * misc: `unimplemented!()` for `TxEntry`'s epee * types: add `BlockCompleteEntry` * rpc: replace `BlockCompleteEntry` with `cuprate-types` * types: document `BlockCompleteEntry` * bin: fix `number_of_fields` for `GetBlocksResponse` * misc: add `Distribution` * distribution: add todo * misc fixes * readme: add `(De)serialization invariants` * distribution: compress variants * types: add `block_complete_entry.rs` * net: fix imports * p2p: fix imports * turn off default-features * p2p: fix imports * misc fixes * Update net/wire/Cargo.toml Co-authored-by: Boog900 * distribution: module doc * wire: re-export types * test-utils: add `crate::rpc::types` module * test-utils: conditional json doc-tests * bin: use enum for `GetBlocksResponse` * misc: use lowercase for stringify * json: add test data, fix macro doc tests * json: add all data * other: add all data * bin: add skeleton * docs * move type to correct file * remove duplicated fields for custom epee * rpc: `client/{client,constants}.rs` -> `client.rs` * lib.rs: remove `clippy::module_inception` * macros: add json doc test macro * json: add some tests * json: add doc-test for all types * add all other JSON doc-tests * move doc-test macros to files * base: add doc-tests * rpc: add `cuprate-rpc-interface` skeleton files * traits * json_rpc_method: add `.is_restricted()` * add route fn signatures * types: add rpc enums * interface: routes, types * interface: simplify routes * rewrite interface fns * types: remove `()` type alias, add `(restricted)` * types: add `other::InPeers` * interface: routes * types: fix `is_restricted()` * interface: reorder short-circuit bool * clean up traits/bounds * types: remove `axum` feature * interface: cleanup unused imports * interface: call handler in routes * json: TODO distribution test * interface: readme intro * combine `RpcHandler` + `RpcService`, add `RpcDummyHandler` * interface: readme docs + test * `IsRestricted` -> `RpcCall` * fix no input route problem * interface: `RpcHandlerDummy` docs * interface: crate docs * replace `create_router` with `RouterBuilder` * types: docs * types: doc `JsonRpc{Request,Response}` * types: readme docs * interface: doc `route/` * interface: fix `todo!()` * interface: allow customizing HTTP method on route functions * interface: fix tests * fix derives * Update rpc/interface/README.md Co-authored-by: Boog900 * Update rpc/interface/README.md Co-authored-by: Boog900 * interface: make `RpcHandler`'s `Future` generic * interface: add JSON-RPC notification todo * formatting * interface: use associated type bound for `RpcHandler`'s `Future` --------- Co-authored-by: Boog900 --- Cargo.lock | 196 ++++++++++++++++++++++++ rpc/interface/Cargo.toml | 17 +++ rpc/interface/README.md | 161 ++++++++++++++++++++ rpc/interface/src/lib.rs | 122 +++++++++++++++ rpc/interface/src/route/bin.rs | 108 ++++++++++++++ rpc/interface/src/route/fallback.rs | 18 +++ rpc/interface/src/route/json_rpc.rs | 68 +++++++++ rpc/interface/src/route/mod.rs | 9 ++ rpc/interface/src/route/other.rs | 138 +++++++++++++++++ rpc/interface/src/router_builder.rs | 198 +++++++++++++++++++++++++ rpc/interface/src/rpc_error.rs | 34 +++++ rpc/interface/src/rpc_handler.rs | 57 +++++++ rpc/interface/src/rpc_handler_dummy.rs | 142 ++++++++++++++++++ rpc/interface/src/rpc_request.rs | 33 +++++ rpc/interface/src/rpc_response.rs | 33 +++++ 15 files changed, 1334 insertions(+) create mode 100644 rpc/interface/README.md create mode 100644 rpc/interface/src/route/bin.rs create mode 100644 rpc/interface/src/route/fallback.rs create mode 100644 rpc/interface/src/route/json_rpc.rs create mode 100644 rpc/interface/src/route/mod.rs create mode 100644 rpc/interface/src/route/other.rs create mode 100644 rpc/interface/src/router_builder.rs create mode 100644 rpc/interface/src/rpc_error.rs create mode 100644 rpc/interface/src/rpc_handler.rs create mode 100644 rpc/interface/src/rpc_handler_dummy.rs create mode 100644 rpc/interface/src/rpc_request.rs create mode 100644 rpc/interface/src/rpc_response.rs diff --git a/Cargo.lock b/Cargo.lock index eaf5f99..1f99810 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,12 +94,73 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -374,6 +435,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam" version = "0.8.4" @@ -764,6 +834,20 @@ dependencies = [ [[package]] name = "cuprate-rpc-interface" version = "0.0.0" +dependencies = [ + "axum", + "cuprate-epee-encoding", + "cuprate-helper", + "cuprate-json-rpc", + "cuprate-rpc-types", + "futures", + "paste", + "serde", + "serde_json", + "tokio", + "tower", + "ureq", +] [[package]] name = "cuprate-rpc-types" @@ -1030,6 +1114,16 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flexible-transcript" version = "0.3.2" @@ -1179,6 +1273,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1299,6 +1412,12 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.3.1" @@ -1308,9 +1427,11 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1610,6 +1731,12 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "md-5" version = "0.10.6" @@ -1638,6 +1765,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.7.3" @@ -2204,6 +2337,7 @@ version = "0.23.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -2351,6 +2485,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.10.8" @@ -2486,6 +2642,18 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + [[package]] name = "synchronoise" version = "1.0.1" @@ -2706,6 +2874,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2770,6 +2939,24 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots", +] + [[package]] name = "url" version = "2.5.1" @@ -2877,6 +3064,15 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/rpc/interface/Cargo.toml b/rpc/interface/Cargo.toml index 47af5cd..a83c0f0 100644 --- a/rpc/interface/Cargo.toml +++ b/rpc/interface/Cargo.toml @@ -9,7 +9,24 @@ repository = "https://github.com/Cuprate/cuprate/tree/main/rpc/cuprate-rpc-inte keywords = ["cuprate", "rpc", "interface"] [features] +default = ["dummy", "serde"] +dummy = [] [dependencies] +cuprate-epee-encoding = { path = "../../net/epee-encoding", default-features = false } +cuprate-json-rpc = { path = "../json-rpc", default-features = false } +cuprate-rpc-types = { path = "../types", features = ["serde", "epee"], default-features = false } +cuprate-helper = { path = "../../helper", features = ["asynch"], default-features = false } + +axum = { version = "0.7.5", features = ["json"], default-features = false } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, features = ["std"] } +tower = { workspace = true } +paste = { workspace = true } +futures = { workspace = true } [dev-dependencies] +axum = { version = "0.7.5", features = ["json", "tokio", "http2"] } +serde_json = { workspace = true, features = ["std"] } +tokio = { workspace = true, features = ["full"] } +ureq = { version = "2.10.0", features = ["json"] } \ No newline at end of file diff --git a/rpc/interface/README.md b/rpc/interface/README.md new file mode 100644 index 0000000..3a63ac4 --- /dev/null +++ b/rpc/interface/README.md @@ -0,0 +1,161 @@ +# `cuprate-rpc-interface` +This crate provides Cuprate's RPC _interface_. + +This crate is _not_ a standalone RPC server, it is just the interface. + +```text + cuprate-rpc-interface provides these parts + │ │ +┌───────────────────────────┤ ├───────────────────┐ +▼ ▼ ▼ ▼ +CLIENT ─► ROUTE ─► REQUEST ─► HANDLER ─► RESPONSE ─► CLIENT + ▲ ▲ + └───┬───┘ + │ + You provide this part +``` + +Everything coming _in_ from a client is handled by this crate. + +This is where your [`RpcHandler`] turns this [`RpcRequest`] into a [`RpcResponse`]. + +You hand this `Response` back to `cuprate-rpc-interface` and it will take care of sending it back to the client. + +The main handler used by Cuprate is implemented in the `cuprate-rpc-handler` crate; +it implements the standard RPC handlers modeled after `monerod`. + +# Purpose +`cuprate-rpc-interface` is built on-top of [`axum`], +which is the crate _actually_ handling everything. + +This crate simply handles: +- Registering endpoint routes (e.g. `/get_block.bin`) +- Defining handler function signatures +- (De)serialization of requests/responses (JSON-RPC, binary, JSON) + +The actual server details are all handled by the [`axum`] and [`tower`] ecosystem. + +The proper usage of this crate is to: +1. Implement a [`RpcHandler`] +2. Use it with [`RouterBuilder`] to generate an + [`axum::Router`] with all Monero RPC routes set +3. Do whatever with it + +# The [`RpcHandler`] +This is your [`tower::Service`] that converts [`RpcRequest`]s into [`RpcResponse`]s, +i.e. the "inner handler". + +Said concretely, `RpcHandler` is a `tower::Service` where the associated types are from this crate: +- [`RpcRequest`] +- [`RpcResponse`] +- [`RpcError`] + +`RpcHandler`'s [`Future`](std::future::Future) is generic, _although_, +it must output `Result`. + +The `RpcHandler` must also hold some state that is required +for RPC server operation. + +The only state currently needed is [`RpcHandler::restricted`], which determines if an RPC +server is restricted or not, and thus, if some endpoints/methods are allowed or not. + +# Unknown endpoint behavior +TODO: decide what this crate should return (per different endpoint) +when a request is received to an unknown endpoint, including HTTP stuff, e.g. status code. + +# Unknown JSON-RPC method behavior +TODO: decide what this crate returns when a `/json_rpc` +request is received with an unknown method, including HTTP stuff, e.g. status code. + +# Example +Example usage of this crate + starting an RPC server. + +This uses `RpcHandlerDummy` as the handler; it always responds with the +correct response type, but set to a default value regardless of the request. + +```rust +use std::sync::Arc; + +use tokio::{net::TcpListener, sync::Barrier}; + +use cuprate_json_rpc::{Request, Response, Id}; +use cuprate_rpc_types::{ + json::{JsonRpcRequest, JsonRpcResponse, GetBlockCountResponse}, + other::{OtherRequest, OtherResponse}, +}; +use cuprate_rpc_interface::{RouterBuilder, RpcHandlerDummy, RpcRequest}; + +// Send a `/get_height` request. This endpoint has no inputs. +async fn get_height(port: u16) -> OtherResponse { + let url = format!("http://127.0.0.1:{port}/get_height"); + ureq::get(&url) + .set("Content-Type", "application/json") + .call() + .unwrap() + .into_json() + .unwrap() +} + +// Send a JSON-RPC request with the `get_block_count` method. +// +// The returned [`String`] is JSON. +async fn get_block_count(port: u16) -> String { + let url = format!("http://127.0.0.1:{port}/json_rpc"); + let method = JsonRpcRequest::GetBlockCount(Default::default()); + let request = Request::new(method); + ureq::get(&url) + .set("Content-Type", "application/json") + .send_json(request) + .unwrap() + .into_string() + .unwrap() +} + +#[tokio::main] +async fn main() { + // Start a local RPC server. + let port = { + // Create the router. + let state = RpcHandlerDummy { restricted: false }; + let router = RouterBuilder::new().all().build().with_state(state); + + // Start a server. + let listener = TcpListener::bind("127.0.0.1:0") + .await + .unwrap(); + let port = listener.local_addr().unwrap().port(); + + // Run the server with `axum`. + tokio::task::spawn(async move { + axum::serve(listener, router).await.unwrap(); + }); + + port + }; + + // Assert the response is the default. + let response = get_height(port).await; + let expected = OtherResponse::GetHeight(Default::default()); + assert_eq!(response, expected); + + // Assert the response JSON is correct. + let response = get_block_count(port).await; + let expected = r#"{"jsonrpc":"2.0","id":null,"result":{"status":"OK","untrusted":false,"count":0}}"#; + assert_eq!(response, expected); + + // Assert that (de)serialization works. + let expected = Response::ok(Id::Null, Default::default()); + let response: Response = serde_json::from_str(&response).unwrap(); + assert_eq!(response, expected); +} +``` + +# Feature flags +List of feature flags for `cuprate-rpc-interface`. + +All are enabled by default. + +| Feature flag | Does what | +|--------------|-----------| +| `serde` | Enables serde on applicable types +| `dummy` | Enables the `RpcHandlerDummy` type \ No newline at end of file diff --git a/rpc/interface/src/lib.rs b/rpc/interface/src/lib.rs index 8b13789..2656b07 100644 --- a/rpc/interface/src/lib.rs +++ b/rpc/interface/src/lib.rs @@ -1 +1,123 @@ +#![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_cfg))] +//---------------------------------------------------------------------------------------------------- Lints +// Forbid lints. +// Our code, and code generated (e.g macros) cannot overrule these. +#![forbid( + // `unsafe` is allowed but it _must_ be + // commented with `SAFETY: reason`. + clippy::undocumented_unsafe_blocks, + // Never. + unused_unsafe, + redundant_semicolons, + unused_allocation, + coherence_leak_check, + while_true, + + // Maybe can be put into `#[deny]`. + unconditional_recursion, + for_loops_over_fallibles, + unused_braces, + unused_labels, + keyword_idents, + non_ascii_idents, + variant_size_differences, + single_use_lifetimes, + + // Probably can be put into `#[deny]`. + future_incompatible, + let_underscore, + break_with_label_and_loop, + duplicate_macro_attributes, + exported_private_dependencies, + large_assignments, + overlapping_range_endpoints, + semicolon_in_expressions_from_macros, + noop_method_call, +)] +// Deny lints. +// Some of these are `#[allow]`'ed on a per-case basis. +#![deny( + clippy::all, + clippy::correctness, + clippy::suspicious, + clippy::style, + clippy::complexity, + clippy::perf, + clippy::pedantic, + clippy::nursery, + clippy::cargo, + unused_doc_comments, + unused_mut, + missing_docs, + deprecated, + unused_comparisons, + nonstandard_style, + unreachable_pub +)] +#![allow( + // FIXME: this lint affects crates outside of + // `database/` for some reason, allow for now. + clippy::cargo_common_metadata, + + // FIXME: adding `#[must_use]` onto everything + // might just be more annoying than useful... + // although it is sometimes nice. + clippy::must_use_candidate, + + // FIXME: good lint but too many false positives + // with our `Env` + `RwLock` setup. + clippy::significant_drop_tightening, + + // FIXME: good lint but is less clear in most cases. + clippy::items_after_statements, + + // TODO + rustdoc::bare_urls, + + clippy::module_name_repetitions, + clippy::module_inception, + clippy::redundant_pub_crate, + clippy::option_if_let_else, +)] +// Allow some lints when running in debug mode. +#![cfg_attr( + debug_assertions, + allow( + clippy::todo, + clippy::multiple_crate_versions, + unused_imports, + unused_variables + ) +)] +// Allow some lints in tests. +#![cfg_attr( + test, + allow( + clippy::cognitive_complexity, + clippy::needless_pass_by_value, + clippy::cast_possible_truncation, + clippy::too_many_lines + ) +)] +// TODO: remove me after finishing impl +#![allow(dead_code, unreachable_code, clippy::diverging_sub_expression)] + +//---------------------------------------------------------------------------------------------------- Mod +mod route; +mod router_builder; +mod rpc_error; +mod rpc_handler; +#[cfg(feature = "dummy")] +mod rpc_handler_dummy; +mod rpc_request; +mod rpc_response; + +pub use router_builder::RouterBuilder; +pub use rpc_error::RpcError; +pub use rpc_handler::RpcHandler; +#[cfg(feature = "dummy")] +pub use rpc_handler_dummy::RpcHandlerDummy; +pub use rpc_request::RpcRequest; +pub use rpc_response::RpcResponse; diff --git a/rpc/interface/src/route/bin.rs b/rpc/interface/src/route/bin.rs new file mode 100644 index 0000000..b17b98c --- /dev/null +++ b/rpc/interface/src/route/bin.rs @@ -0,0 +1,108 @@ +//! Binary route functions. + +//---------------------------------------------------------------------------------------------------- Import +use axum::{body::Bytes, extract::State, http::StatusCode}; +use tower::ServiceExt; + +use cuprate_epee_encoding::from_bytes; +use cuprate_rpc_types::bin::{BinRequest, BinResponse, GetTransactionPoolHashesRequest}; + +use crate::{rpc_handler::RpcHandler, rpc_request::RpcRequest, rpc_response::RpcResponse}; + +//---------------------------------------------------------------------------------------------------- Routes +/// This macro generates route functions that expect input. +/// +/// See below for usage. +macro_rules! generate_endpoints_with_input { + ($( + // Syntax: + // Function name => Expected input type + $endpoint:ident => $variant:ident + ),*) => { paste::paste! { + $( + /// TODO + #[allow(unused_mut)] + pub(crate) async fn $endpoint( + State(handler): State, + mut request: Bytes, + ) -> Result { + // Serialize into the request type. + let request = BinRequest::$variant( + from_bytes(&mut request).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + ); + + generate_endpoints_inner!($variant, handler, request) + } + )* + }}; +} + +/// This macro generates route functions that expect _no_ input. +/// +/// See below for usage. +macro_rules! generate_endpoints_with_no_input { + ($( + // Syntax: + // Function name => Expected input type (that is empty) + $endpoint:ident => $variant:ident + ),*) => { paste::paste! { + $( + /// TODO + #[allow(unused_mut)] + pub(crate) async fn $endpoint( + State(handler): State, + ) -> Result { + const REQUEST: BinRequest = BinRequest::$variant([<$variant Request>] {}); + generate_endpoints_inner!($variant, handler, REQUEST) + } + )* + }}; +} + +/// De-duplicated inner function body for: +/// - [`generate_endpoints_with_input`] +/// - [`generate_endpoints_with_no_input`] +macro_rules! generate_endpoints_inner { + ($variant:ident, $handler:ident, $request:expr) => { + paste::paste! { + { + // Send request. + let request = RpcRequest::Binary($request); + let channel = $handler.oneshot(request).await?; + + // Assert the response from the inner handler is correct. + let RpcResponse::Binary(response) = channel else { + panic!("RPC handler did not return a binary response"); + }; + let BinResponse::$variant(response) = response else { + panic!("RPC handler returned incorrect response"); + }; + + // Serialize to bytes and respond. + match cuprate_epee_encoding::to_bytes(response) { + Ok(bytes) => Ok(bytes.freeze()), + Err(e) => Err(StatusCode::INTERNAL_SERVER_ERROR), + } + } + } + }; +} + +generate_endpoints_with_input! { + get_blocks => GetBlocks, + get_blocks_by_height => GetBlocksByHeight, + get_hashes => GetHashes, + get_o_indexes => GetOutputIndexes, + get_outs => GetOuts, + get_output_distribution => GetOutputDistribution +} + +generate_endpoints_with_no_input! { + get_transaction_pool_hashes => GetTransactionPoolHashes +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/rpc/interface/src/route/fallback.rs b/rpc/interface/src/route/fallback.rs new file mode 100644 index 0000000..9478901 --- /dev/null +++ b/rpc/interface/src/route/fallback.rs @@ -0,0 +1,18 @@ +//! Fallback route functions. + +//---------------------------------------------------------------------------------------------------- Import +use axum::http::StatusCode; + +//---------------------------------------------------------------------------------------------------- Routes +/// Fallback route function. +/// +/// This is used as the fallback endpoint in [`crate::RouterBuilder`]. +pub(crate) async fn fallback() -> StatusCode { + StatusCode::NOT_FOUND +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/rpc/interface/src/route/json_rpc.rs b/rpc/interface/src/route/json_rpc.rs new file mode 100644 index 0000000..bd35e43 --- /dev/null +++ b/rpc/interface/src/route/json_rpc.rs @@ -0,0 +1,68 @@ +//! JSON-RPC 2.0 endpoint route functions. + +//---------------------------------------------------------------------------------------------------- Import +use std::borrow::Cow; + +use axum::{extract::State, http::StatusCode, Json}; +use tower::ServiceExt; + +use cuprate_json_rpc::{ + error::{ErrorCode, ErrorObject}, + Id, +}; +use cuprate_rpc_types::{ + json::{JsonRpcRequest, JsonRpcResponse}, + RpcCallValue, +}; + +use crate::{rpc_handler::RpcHandler, rpc_request::RpcRequest, rpc_response::RpcResponse}; + +//---------------------------------------------------------------------------------------------------- Routes +/// The `/json_rpc` route function used in [`crate::RouterBuilder`]. +pub(crate) async fn json_rpc( + State(handler): State, + Json(request): Json>, +) -> Result>, StatusCode> { + // TODO: + // + // JSON-RPC notifications (requests without `id`) + // must not be responded too, although, the request's side-effects + // must remain. How to do this considering this function will + // always return and cause `axum` to respond? + + // Return early if this RPC server is restricted and + // the requested method is only for non-restricted RPC. + if request.body.is_restricted() && handler.restricted() { + let error_object = ErrorObject { + code: ErrorCode::ServerError(-1 /* TODO */), + message: Cow::Borrowed("Restricted. TODO: mimic monerod message"), + data: None, + }; + + // JSON-RPC 2.0 rule: + // If there was an error in detecting the `Request`'s ID, + // the `Response` must contain an `Id::Null` + let id = request.id.unwrap_or(Id::Null); + + let response = cuprate_json_rpc::Response::err(id, error_object); + + return Ok(Json(response)); + } + + // Send request. + let request = RpcRequest::JsonRpc(request); + let channel = handler.oneshot(request).await?; + + // Assert the response from the inner handler is correct. + let RpcResponse::JsonRpc(response) = channel else { + panic!("RPC handler returned incorrect response"); + }; + + Ok(Json(response)) +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/rpc/interface/src/route/mod.rs b/rpc/interface/src/route/mod.rs new file mode 100644 index 0000000..7ff9ab8 --- /dev/null +++ b/rpc/interface/src/route/mod.rs @@ -0,0 +1,9 @@ +//! Routing functions. +//! +//! These are the function signatures passed to +//! [`crate::RouterBuilder`] when registering routes. + +pub(crate) mod bin; +pub(crate) mod fallback; +pub(crate) mod json_rpc; +pub(crate) mod other; diff --git a/rpc/interface/src/route/other.rs b/rpc/interface/src/route/other.rs new file mode 100644 index 0000000..ce778db --- /dev/null +++ b/rpc/interface/src/route/other.rs @@ -0,0 +1,138 @@ +//! Other JSON endpoint route functions. + +//---------------------------------------------------------------------------------------------------- Import +use axum::{extract::State, http::StatusCode, Json}; +use tower::ServiceExt; + +use cuprate_rpc_types::{ + other::{ + GetAltBlocksHashesRequest, GetAltBlocksHashesResponse, GetHeightRequest, GetHeightResponse, + GetLimitRequest, GetLimitResponse, GetNetStatsRequest, GetNetStatsResponse, GetOutsRequest, + GetOutsResponse, GetPeerListRequest, GetPeerListResponse, GetPublicNodesRequest, + GetPublicNodesResponse, GetTransactionPoolHashesRequest, GetTransactionPoolHashesResponse, + GetTransactionPoolRequest, GetTransactionPoolResponse, GetTransactionPoolStatsRequest, + GetTransactionPoolStatsResponse, GetTransactionsRequest, GetTransactionsResponse, + InPeersRequest, InPeersResponse, IsKeyImageSpentRequest, IsKeyImageSpentResponse, + MiningStatusRequest, MiningStatusResponse, OtherRequest, OtherResponse, OutPeersRequest, + OutPeersResponse, PopBlocksRequest, PopBlocksResponse, SaveBcRequest, SaveBcResponse, + SendRawTransactionRequest, SendRawTransactionResponse, SetBootstrapDaemonRequest, + SetBootstrapDaemonResponse, SetLimitRequest, SetLimitResponse, SetLogCategoriesRequest, + SetLogCategoriesResponse, SetLogHashRateRequest, SetLogHashRateResponse, + SetLogLevelRequest, SetLogLevelResponse, StartMiningRequest, StartMiningResponse, + StopDaemonRequest, StopDaemonResponse, StopMiningRequest, StopMiningResponse, + UpdateRequest, UpdateResponse, + }, + RpcCall, +}; + +use crate::{rpc_handler::RpcHandler, rpc_request::RpcRequest, rpc_response::RpcResponse}; + +//---------------------------------------------------------------------------------------------------- Routes +/// This macro generates route functions that expect input. +/// +/// See below for usage. +macro_rules! generate_endpoints_with_input { + ($( + // Syntax: + // Function name => Expected input type + $endpoint:ident => $variant:ident + ),*) => { paste::paste! { + $( + pub(crate) async fn $endpoint( + State(handler): State, + Json(request): Json<[<$variant Request>]>, + ) -> Result]>, StatusCode> { + generate_endpoints_inner!($variant, handler, request) + } + )* + }}; +} + +/// This macro generates route functions that expect _no_ input. +/// +/// See below for usage. +macro_rules! generate_endpoints_with_no_input { + ($( + // Syntax: + // Function name => Expected input type (that is empty) + $endpoint:ident => $variant:ident + ),*) => { paste::paste! { + $( + pub(crate) async fn $endpoint( + State(handler): State, + ) -> Result]>, StatusCode> { + generate_endpoints_inner!($variant, handler, [<$variant Request>] {}) + } + )* + }}; +} + +/// De-duplicated inner function body for: +/// - [`generate_endpoints_with_input`] +/// - [`generate_endpoints_with_no_input`] +macro_rules! generate_endpoints_inner { + ($variant:ident, $handler:ident, $request:expr) => { + paste::paste! { + { + // Check if restricted. + if [<$variant Request>]::IS_RESTRICTED && $handler.restricted() { + // TODO: mimic `monerod` behavior. + return Err(StatusCode::FORBIDDEN); + } + + // Send request. + let request = RpcRequest::Other(OtherRequest::$variant($request)); + let channel = $handler.oneshot(request).await?; + + // Assert the response from the inner handler is correct. + let RpcResponse::Other(response) = channel else { + panic!("RPC handler did not return a binary response"); + }; + let OtherResponse::$variant(response) = response else { + panic!("RPC handler returned incorrect response") + }; + + Ok(Json(response)) + } + } + }; +} + +generate_endpoints_with_input! { + get_transactions => GetTransactions, + is_key_image_spent => IsKeyImageSpent, + send_raw_transaction => SendRawTransaction, + start_mining => StartMining, + get_peer_list => GetPeerList, + set_log_hash_rate => SetLogHashRate, + set_log_level => SetLogLevel, + set_log_categories => SetLogCategories, + set_bootstrap_daemon => SetBootstrapDaemon, + set_limit => SetLimit, + out_peers => OutPeers, + in_peers => InPeers, + get_outs => GetOuts, + update => Update, + pop_blocks => PopBlocks, + get_public_nodes => GetPublicNodes +} + +generate_endpoints_with_no_input! { + get_height => GetHeight, + get_alt_blocks_hashes => GetAltBlocksHashes, + stop_mining => StopMining, + mining_status => MiningStatus, + save_bc => SaveBc, + get_transaction_pool => GetTransactionPool, + get_transaction_pool_stats => GetTransactionPoolStats, + stop_daemon => StopDaemon, + get_limit => GetLimit, + get_net_stats => GetNetStats, + get_transaction_pool_hashes => GetTransactionPoolHashes +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/rpc/interface/src/router_builder.rs b/rpc/interface/src/router_builder.rs new file mode 100644 index 0000000..d370cf4 --- /dev/null +++ b/rpc/interface/src/router_builder.rs @@ -0,0 +1,198 @@ +//! Free functions. + +use std::marker::PhantomData; + +//---------------------------------------------------------------------------------------------------- Use +use axum::{ + routing::{method_routing::get, post}, + Router, +}; + +use crate::{ + route::{bin, fallback, json_rpc, other}, + rpc_handler::RpcHandler, +}; + +//---------------------------------------------------------------------------------------------------- RouterBuilder +/// Generate the `RouterBuilder` struct. +macro_rules! generate_router_builder { + ($( + // Syntax: + // $BUILDER_FUNCTION_NAME => + // $ACTUAL_ENDPOINT_STRING => + // $ENDPOINT_FUNCTION_MODULE::$ENDPOINT_FUNCTION => + // ($HTTP_METHOD(s)) + $endpoint_ident:ident => + $endpoint_string:literal => + $endpoint_module:ident::$endpoint_fn:ident => + ($($http_method:ident),*) + ),* $(,)?) => { + /// Builder for creating the RPC router. + /// + /// This builder allows you to selectively enable endpoints for the router, + /// and a [`fallback`](RouterBuilder::fallback) route. + /// + /// The [`default`](RouterBuilder::default) is to enable [`all`](RouterBuilder::all) routes. + /// + /// # Routes + /// Functions that enable routes are separated into 3 groups: + /// - `json_rpc` (enables all of JSON RPC 2.0) + /// - `other_` (e.g. [`other_get_height`](RouterBuilder::other_get_height)) + /// - `bin_` (e.g. [`bin_get_blocks`](RouterBuilder::bin_get_blocks)) + /// + /// For a list of all `monerod` routes, see + /// [here](https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server.h#L97-L189), + /// or the source file of this type. + /// + /// # Aliases + /// Some routes have aliases, such as [`/get_height`](RouterBuilder::other_get_height) + /// and [`/getheight`](RouterBuilder::other_getheight). + /// + /// These both route to the same handler function, but they do not enable each other. + /// + /// If desired, you can enable `/get_height` but not `/getheight`. + /// + /// # Example + /// ```rust + /// use cuprate_rpc_interface::{RouterBuilder, RpcHandlerDummy}; + /// + /// // Create a router with _only_ `/json_rpc` enabled. + /// let only_json_rpc = RouterBuilder::::new() + /// .json_rpc() + /// .build(); + /// + /// // Create a router with: + /// // - `/get_outs.bin` enabled + /// // - A fallback enabled + /// let get_outs_bin_and_fallback = RouterBuilder::::new() + /// .bin_get_outs() + /// .fallback() + /// .build(); + /// + /// // Create a router with all endpoints enabled. + /// let all = RouterBuilder::::new() + /// .all() + /// .build(); + /// ``` + #[allow(clippy::struct_excessive_bools)] + #[derive(Clone)] + pub struct RouterBuilder { + router: Router, + } + + impl RouterBuilder { + /// Create a new [`Self`]. + #[must_use] + pub fn new() -> Self { + Self { + router: Router::new(), + } + } + + /// Build [`Self`] into a [`Router`]. + /// + /// All endpoints enabled in [`RouterBuilder`] + /// will be enabled in this [`Router`]. + pub fn build(self) -> Router { + self.router + } + + /// Enable all endpoints, including [`Self::fallback`]. + #[must_use] + pub fn all(mut self) -> Self { + $( + self = self.$endpoint_ident(); + )* + + self.fallback() + } + + /// Enable the catch-all fallback route. + /// + /// Any unknown or disabled route will route here, e.g.: + /// - `get_info` + /// - `getinfo` + /// - `asdf` + #[must_use] + pub fn fallback(self) -> Self { + Self { + router: self.router.fallback(fallback::fallback), + } + } + + $( + #[doc = concat!( + "Enable the `", + $endpoint_string, + "` endpoint.", + )] + #[must_use] + pub fn $endpoint_ident(self) -> Self { + Self { + router: self.router.route( + $endpoint_string, + ::axum::routing::method_routing::MethodRouter::new() + $(.$http_method($endpoint_module::$endpoint_fn::))* + ), + } + } + )* + } + }; +} + +generate_router_builder! { + // JSON-RPC 2.0 route. + json_rpc => "/json_rpc" => json_rpc::json_rpc => (get, post), + + // Other JSON routes. + other_get_height => "/get_height" => other::get_height => (get, post), + other_getheight => "/getheight" => other::get_height => (get, post), + other_get_transactions => "/get_transactions" => other::get_transactions => (get, post), + other_gettransactions => "/gettransactions" => other::get_transactions => (get, post), + other_get_alt_blocks_hashes => "/get_alt_blocks_hashes" => other::get_alt_blocks_hashes => (get, post), + other_is_key_image_spent => "/is_key_image_spent" => other::is_key_image_spent => (get, post), + other_send_raw_transaction => "/send_raw_transaction" => other::send_raw_transaction => (get, post), + other_sendrawtransaction => "/sendrawtransaction" => other::send_raw_transaction => (get, post), + other_start_mining => "/start_mining" => other::start_mining => (get, post), + other_stop_mining => "/stop_mining" => other::stop_mining => (get, post), + other_mining_status => "/mining_status" => other::mining_status => (get, post), + other_save_bc => "/save_bc" => other::save_bc => (get, post), + other_get_peer_list => "/get_peer_list" => other::get_peer_list => (get, post), + other_get_public_nodes => "/get_public_nodes" => other::get_public_nodes => (get, post), + other_set_log_hash_rate => "/set_log_hash_rate" => other::set_log_hash_rate => (get, post), + other_set_log_level => "/set_log_level" => other::set_log_level => (get, post), + other_set_log_categories => "/set_log_categories" => other::set_log_categories => (get, post), + other_get_transaction_pool => "/get_transaction_pool" => other::get_transaction_pool => (get, post), + other_get_transaction_pool_hashes => "/get_transaction_pool_hashes" => other::get_transaction_pool_hashes => (get, post), + other_get_transaction_pool_stats => "/get_transaction_pool_stats" => other::get_transaction_pool_stats => (get, post), + other_set_bootstrap_daemon => "/set_bootstrap_daemon" => other::set_bootstrap_daemon => (get, post), + other_stop_daemon => "/stop_daemon" => other::stop_daemon => (get, post), + other_get_net_stats => "/get_net_stats" => other::get_net_stats => (get, post), + other_get_limit => "/get_limit" => other::get_limit => (get, post), + other_set_limit => "/set_limit" => other::set_limit => (get, post), + other_out_peers => "/out_peers" => other::out_peers => (get, post), + other_in_peers => "/in_peers" => other::in_peers => (get, post), + other_get_outs => "/get_outs" => other::get_outs => (get, post), + other_update => "/update" => other::update => (get, post), + other_pop_blocks => "/pop_blocks" => other::pop_blocks => (get, post), + + // Binary routes. + bin_get_blocks => "/get_blocks.bin" => bin::get_blocks => (get, post), + bin_getblocks => "/getblocks.bin" => bin::get_blocks => (get, post), + bin_get_blocks_by_height => "/get_blocks_by_height.bin" => bin::get_blocks_by_height => (get, post), + bin_getblocks_by_height => "/getblocks_by_height.bin" => bin::get_blocks_by_height => (get, post), + bin_get_hashes => "/get_hashes.bin" => bin::get_hashes => (get, post), + bin_gethashes => "/gethashes.bin" => bin::get_hashes => (get, post), + bin_get_o_indexes => "/get_o_indexes.bin" => bin::get_o_indexes => (get, post), + bin_get_outs => "/get_outs.bin" => bin::get_outs => (get, post), + bin_get_transaction_pool_hashes => "/get_transaction_pool_hashes.bin" => bin::get_transaction_pool_hashes => (get, post), + bin_get_output_distribution => "/get_output_distribution.bin" => bin::get_output_distribution => (get, post), +} + +impl Default for RouterBuilder { + /// Uses [`Self::all`]. + fn default() -> Self { + Self::new().all() + } +} diff --git a/rpc/interface/src/rpc_error.rs b/rpc/interface/src/rpc_error.rs new file mode 100644 index 0000000..92b9cc1 --- /dev/null +++ b/rpc/interface/src/rpc_error.rs @@ -0,0 +1,34 @@ +//! RPC errors. + +//---------------------------------------------------------------------------------------------------- Import +use axum::http::StatusCode; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +//---------------------------------------------------------------------------------------------------- RpcError +/// Possible errors during RPC operation. +/// +/// These are any errors that can happen _during_ a handler function. +/// I.e. if this error surfaces, it happened _after_ the request was +/// deserialized. +/// +/// This is the `Error` type required to be used in an [`RpcHandler`](crate::RpcHandler). +/// +/// TODO: This is empty as possible errors will be +/// enumerated when the handler functions are created. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub enum RpcError {} + +impl From for StatusCode { + fn from(value: RpcError) -> Self { + // TODO + Self::INTERNAL_SERVER_ERROR + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/rpc/interface/src/rpc_handler.rs b/rpc/interface/src/rpc_handler.rs new file mode 100644 index 0000000..3d1c28d --- /dev/null +++ b/rpc/interface/src/rpc_handler.rs @@ -0,0 +1,57 @@ +//! RPC handler trait. + +//---------------------------------------------------------------------------------------------------- Use +use std::{future::Future, task::Poll}; + +use axum::{http::StatusCode, response::IntoResponse}; +use futures::{channel::oneshot::channel, FutureExt}; +use tower::Service; + +use cuprate_helper::asynch::InfallibleOneshotReceiver; +use cuprate_json_rpc::Id; +use cuprate_rpc_types::json::JsonRpcRequest; + +use crate::{rpc_error::RpcError, rpc_request::RpcRequest, rpc_response::RpcResponse}; + +//---------------------------------------------------------------------------------------------------- RpcHandler +/// An RPC handler. +/// +/// This trait represents a type that can turn [`RpcRequest`]s into [`RpcResponse`]s. +/// +/// Implementors of this trait must be [`tower::Service`]s that use: +/// - [`RpcRequest`] as the generic `Request` type +/// - [`RpcResponse`] as the associated `Response` type +/// - [`RpcError`] as the associated `Error` type +/// - A generic [`Future`] that outputs `Result` +/// +/// See this crate's `RpcHandlerDummy` for an implementation example of this trait. +/// +/// # Panics +/// Your [`RpcHandler`] must reply to [`RpcRequest`]s with the correct +/// [`RpcResponse`] or else this crate will panic during routing functions. +/// +/// For example, upon a [`RpcRequest::Binary`] must be replied with +/// [`RpcRequest::Binary`]. If an [`RpcRequest::Other`] were returned instead, +/// this crate would panic. +pub trait RpcHandler: + Clone + + Send + + Sync + + 'static + + Service< + RpcRequest, + Response = RpcResponse, + Error = RpcError, + Future: Future> + Send + Sync + 'static, + > +{ + /// Is this [`RpcHandler`] restricted? + /// + /// If this returns `true`, restricted methods and endpoints such as: + /// - `/json_rpc`'s `relay_tx` method + /// - The `/pop_blocks` endpoint + /// + /// will automatically be denied access when using the + /// [`axum::Router`] provided by [`RouterBuilder`](crate::RouterBuilder). + fn restricted(&self) -> bool; +} diff --git a/rpc/interface/src/rpc_handler_dummy.rs b/rpc/interface/src/rpc_handler_dummy.rs new file mode 100644 index 0000000..97b7585 --- /dev/null +++ b/rpc/interface/src/rpc_handler_dummy.rs @@ -0,0 +1,142 @@ +//! Dummy implementation of [`RpcHandler`]. + +//---------------------------------------------------------------------------------------------------- Use +use std::task::Poll; + +use futures::{channel::oneshot::channel, FutureExt}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use tower::Service; + +use cuprate_helper::asynch::InfallibleOneshotReceiver; +use cuprate_json_rpc::Id; +use cuprate_rpc_types::json::JsonRpcRequest; + +use crate::{ + rpc_error::RpcError, rpc_handler::RpcHandler, rpc_request::RpcRequest, + rpc_response::RpcResponse, +}; + +//---------------------------------------------------------------------------------------------------- RpcHandlerDummy +/// An [`RpcHandler`] that always returns [`Default::default`]. +/// +/// This `struct` implements [`RpcHandler`], and always responds +/// with the response `struct` set to [`Default::default`]. +/// +/// See the [`crate`] documentation for example usage. +/// +/// This is mostly used for testing purposes and can +/// be disabled by disable the `dummy` feature flag. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub struct RpcHandlerDummy { + /// Should this RPC server be [restricted](RpcHandler::restricted)? + /// + /// The dummy will honor this [`bool`] + /// on restricted methods/endpoints. + pub restricted: bool, +} + +impl RpcHandler for RpcHandlerDummy { + fn restricted(&self) -> bool { + self.restricted + } +} + +impl Service for RpcHandlerDummy { + type Response = RpcResponse; + type Error = RpcError; + type Future = InfallibleOneshotReceiver>; + + fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: RpcRequest) -> Self::Future { + use cuprate_rpc_types::bin::BinRequest as BReq; + use cuprate_rpc_types::bin::BinResponse as BResp; + use cuprate_rpc_types::json::JsonRpcRequest as JReq; + use cuprate_rpc_types::json::JsonRpcResponse as JResp; + use cuprate_rpc_types::other::OtherRequest as OReq; + use cuprate_rpc_types::other::OtherResponse as OResp; + + #[rustfmt::skip] + #[allow(clippy::default_trait_access)] + let resp = match req { + RpcRequest::JsonRpc(j) => RpcResponse::JsonRpc(cuprate_json_rpc::Response::ok(Id::Null, match j.body { + JReq::GetBlockCount(_) => JResp::GetBlockCount(Default::default()), + JReq::OnGetBlockHash(_) => JResp::OnGetBlockHash(Default::default()), + JReq::SubmitBlock(_) => JResp::SubmitBlock(Default::default()), + JReq::GenerateBlocks(_) => JResp::GenerateBlocks(Default::default()), + JReq::GetLastBlockHeader(_) => JResp::GetLastBlockHeader(Default::default()), + JReq::GetBlockHeaderByHash(_) => JResp::GetBlockHeaderByHash(Default::default()), + JReq::GetBlockHeaderByHeight(_) => JResp::GetBlockHeaderByHeight(Default::default()), + JReq::GetBlockHeadersRange(_) => JResp::GetBlockHeadersRange(Default::default()), + JReq::GetBlock(_) => JResp::GetBlock(Default::default()), + JReq::GetConnections(_) => JResp::GetConnections(Default::default()), + JReq::GetInfo(_) => JResp::GetInfo(Default::default()), + JReq::HardForkInfo(_) => JResp::HardForkInfo(Default::default()), + JReq::SetBans(_) => JResp::SetBans(Default::default()), + JReq::GetBans(_) => JResp::GetBans(Default::default()), + JReq::Banned(_) => JResp::Banned(Default::default()), + JReq::FlushTransactionPool(_) => JResp::FlushTransactionPool(Default::default()), + JReq::GetOutputHistogram(_) => JResp::GetOutputHistogram(Default::default()), + JReq::GetCoinbaseTxSum(_) => JResp::GetCoinbaseTxSum(Default::default()), + JReq::GetVersion(_) => JResp::GetVersion(Default::default()), + JReq::GetFeeEstimate(_) => JResp::GetFeeEstimate(Default::default()), + JReq::GetAlternateChains(_) => JResp::GetAlternateChains(Default::default()), + JReq::RelayTx(_) => JResp::RelayTx(Default::default()), + JReq::SyncInfo(_) => JResp::SyncInfo(Default::default()), + JReq::GetTransactionPoolBacklog(_) => JResp::GetTransactionPoolBacklog(Default::default()), + JReq::GetMinerData(_) => JResp::GetMinerData(Default::default()), + JReq::PruneBlockchain(_) => JResp::PruneBlockchain(Default::default()), + JReq::CalcPow(_) => JResp::CalcPow(Default::default()), + JReq::FlushCache(_) => JResp::FlushCache(Default::default()), + JReq::AddAuxPow(_) => JResp::AddAuxPow(Default::default()), + JReq::GetTxIdsLoose(_) => JResp::GetTxIdsLoose(Default::default()), + })), + RpcRequest::Binary(b) => RpcResponse::Binary(match b { + BReq::GetBlocks(_) => BResp::GetBlocks(Default::default()), + BReq::GetBlocksByHeight(_) => BResp::GetBlocksByHeight(Default::default()), + BReq::GetHashes(_) => BResp::GetHashes(Default::default()), + BReq::GetOutputIndexes(_) => BResp::GetOutputIndexes(Default::default()), + BReq::GetOuts(_) => BResp::GetOuts(Default::default()), + BReq::GetTransactionPoolHashes(_) => BResp::GetTransactionPoolHashes(Default::default()), + BReq::GetOutputDistribution(_) => BResp::GetOutputDistribution(Default::default()), + }), + RpcRequest::Other(o) => RpcResponse::Other(match o { + OReq::GetHeight(_) => OResp::GetHeight(Default::default()), + OReq::GetTransactions(_) => OResp::GetTransactions(Default::default()), + OReq::GetAltBlocksHashes(_) => OResp::GetAltBlocksHashes(Default::default()), + OReq::IsKeyImageSpent(_) => OResp::IsKeyImageSpent(Default::default()), + OReq::SendRawTransaction(_) => OResp::SendRawTransaction(Default::default()), + OReq::StartMining(_) => OResp::StartMining(Default::default()), + OReq::StopMining(_) => OResp::StopMining(Default::default()), + OReq::MiningStatus(_) => OResp::MiningStatus(Default::default()), + OReq::SaveBc(_) => OResp::SaveBc(Default::default()), + OReq::GetPeerList(_) => OResp::GetPeerList(Default::default()), + OReq::SetLogHashRate(_) => OResp::SetLogHashRate(Default::default()), + OReq::SetLogLevel(_) => OResp::SetLogLevel(Default::default()), + OReq::SetLogCategories(_) => OResp::SetLogCategories(Default::default()), + OReq::SetBootstrapDaemon(_) => OResp::SetBootstrapDaemon(Default::default()), + OReq::GetTransactionPool(_) => OResp::GetTransactionPool(Default::default()), + OReq::GetTransactionPoolStats(_) => OResp::GetTransactionPoolStats(Default::default()), + OReq::StopDaemon(_) => OResp::StopDaemon(Default::default()), + OReq::GetLimit(_) => OResp::GetLimit(Default::default()), + OReq::SetLimit(_) => OResp::SetLimit(Default::default()), + OReq::OutPeers(_) => OResp::OutPeers(Default::default()), + OReq::InPeers(_) => OResp::InPeers(Default::default()), + OReq::GetNetStats(_) => OResp::GetNetStats(Default::default()), + OReq::GetOuts(_) => OResp::GetOuts(Default::default()), + OReq::Update(_) => OResp::Update(Default::default()), + OReq::PopBlocks(_) => OResp::PopBlocks(Default::default()), + OReq::GetTransactionPoolHashes(_) => OResp::GetTransactionPoolHashes(Default::default()), + OReq::GetPublicNodes(_) => OResp::GetPublicNodes(Default::default()), + }) + }; + + let (tx, rx) = channel(); + drop(tx.send(Ok(resp))); + InfallibleOneshotReceiver::from(rx) + } +} diff --git a/rpc/interface/src/rpc_request.rs b/rpc/interface/src/rpc_request.rs new file mode 100644 index 0000000..3b66a78 --- /dev/null +++ b/rpc/interface/src/rpc_request.rs @@ -0,0 +1,33 @@ +//! RPC requests. + +//---------------------------------------------------------------------------------------------------- Import +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use cuprate_rpc_types::{bin::BinRequest, json::JsonRpcRequest, other::OtherRequest}; + +//---------------------------------------------------------------------------------------------------- RpcRequest +/// All possible RPC requests. +/// +/// This enum encapsulates all possible RPC requests: +/// - JSON RPC 2.0 requests +/// - Binary requests +/// - Other JSON requests +/// +/// It is the `Request` type required to be used in an [`RpcHandler`](crate::RpcHandler). +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub enum RpcRequest { + /// JSON-RPC 2.0 requests. + JsonRpc(cuprate_json_rpc::Request), + /// Binary requests. + Binary(BinRequest), + /// Other JSON requests. + Other(OtherRequest), +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/rpc/interface/src/rpc_response.rs b/rpc/interface/src/rpc_response.rs new file mode 100644 index 0000000..7e8ecdb --- /dev/null +++ b/rpc/interface/src/rpc_response.rs @@ -0,0 +1,33 @@ +//! RPC responses. + +//---------------------------------------------------------------------------------------------------- Import +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use cuprate_rpc_types::{bin::BinResponse, json::JsonRpcResponse, other::OtherResponse}; + +//---------------------------------------------------------------------------------------------------- RpcResponse +/// All possible RPC responses. +/// +/// This enum encapsulates all possible RPC responses: +/// - JSON RPC 2.0 responses +/// - Binary responses +/// - Other JSON responses +/// +/// It is the `Response` type required to be used in an [`RpcHandler`](crate::RpcHandler). +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub enum RpcResponse { + /// JSON RPC 2.0 responses. + JsonRpc(cuprate_json_rpc::Response), + /// Binary responses. + Binary(BinResponse), + /// Other JSON responses. + Other(OtherResponse), +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +}