Skip to content

Commit

Permalink
[BACK-2720] Admin API (#39)
Browse files Browse the repository at this point in the history
Add admin API router with routes protected by a secret key
  • Loading branch information
erikreppel authored Apr 19, 2024
2 parents 58f1707 + 6acbe88 commit fb89ebf
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 8 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ alloy-sol-macro = { git = "https://github.com/alloy-rs/core", rev = "7574bfc", f
url = "2.5.0"
futures = "0.3.30"
sha256 = "1.5.0"
tower = "0.4.13"


[patch.crates-io]
Expand Down
2 changes: 1 addition & 1 deletion examples/extra_rules_and_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ async fn main() -> eyre::Result<()> {
let ctl = mintpool::run::start_p2p_services(&config, rules).await?;

// Add some custom routes in addition to the defaults. You could also add middleware or anything else you can do with axum.
let mut router = mintpool::api::router_with_defaults().await;
let mut router = mintpool::api::router_with_defaults();
router = router
.route("/simple", get(my_simple_route))
.route("/count", get(query_route));
Expand Down
165 changes: 161 additions & 4 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::storage;
use crate::types::PremintTypes;
use axum::extract::State;
use axum::http::StatusCode;
use axum::middleware::from_fn_with_state;
use axum::routing::{get, post};
use axum::{Json, Router};
use serde::Serialize;
Expand All @@ -15,33 +16,53 @@ use tokio::net::TcpListener;
pub struct AppState {
pub db: SqlitePool,
pub controller: ControllerInterface,
pub api_secret: Option<String>,
}

impl AppState {
pub async fn from(controller: ControllerInterface) -> Self {
pub async fn from(config: &Config, controller: ControllerInterface) -> Self {
let (snd, recv) = tokio::sync::oneshot::channel();
controller
.send_command(ControllerCommands::Query(DBQuery::Direct(snd)))
.await
.unwrap();
let db = recv.await.unwrap().expect("Failed to get db");
Self { db, controller }
Self {
db,
controller,
api_secret: config.admin_api_secret.clone(),
}
}
}

pub async fn router_with_defaults() -> Router<AppState> {
pub fn router_with_defaults() -> Router<AppState> {
Router::new()
.route("/health", get(health))
.route("/list-all", get(list_all))
.route("/submit-premint", post(submit_premint))
}

fn with_admin_routes(state: AppState, router: Router<AppState>) -> Router<AppState> {
let admin = Router::new()
.route("/admin/node", get(admin::node_info))
.route("/admin/add-peer", post(admin::add_peer))
.layer(from_fn_with_state(state, admin::auth_middleware));

router.merge(admin)
}

pub async fn start_api(
config: &Config,
controller: ControllerInterface,
router: Router<AppState>,
use_admin_routes: bool,
) -> eyre::Result<()> {
let app_state = AppState::from(controller).await;
let app_state = AppState::from(config, controller).await;
let mut router = router;
if use_admin_routes {
router = with_admin_routes(app_state.clone(), router);
}

let router = router.with_state(app_state);
let addr = format!("{}:{}", config.initial_network_ip(), config.api_port);
let listener = TcpListener::bind(addr.clone()).await.unwrap();
Expand Down Expand Up @@ -134,3 +155,139 @@ pub enum APIResponse {
Error { message: String },
Success { message: String },
}

pub mod admin {
use crate::api::{APIResponse, AppState};
use crate::controller::ControllerCommands;
use crate::p2p::NetworkState;
use axum::body::Body;
use axum::extract::{Request, State};
use axum::http::StatusCode;
use axum::middleware::Next;
use axum::response::Response;
use axum::Json;
use serde::Serialize;

pub async fn auth_middleware(
State(state): State<AppState>,
request: Request,
next: Next,
) -> Response {
let secret = match state.api_secret {
Some(s) => s,
None => {
return Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(Body::new("Unauthorized".to_string()))
.expect("Invalid response");
}
};

match request.headers().get("Authorization") {
Some(auth) => {
if auth.to_str().unwrap_or_default() != secret.as_str() {
return Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(Body::new("Unauthorized. Invalid secret".to_string()))
.expect("Invalid response");
}
}
None => {
return Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(Body::new(
"Unauthorized. Set Header Authorization: <secret>".to_string(),
))
.expect("Invalid response");
}
};
next.run(request).await
}

#[derive(serde::Deserialize)]
pub struct PeerRequest {
peer: String,
}

// should be behind an auth middleware
pub async fn add_peer(
State(state): State<AppState>,
Json(request): Json<PeerRequest>,
) -> (StatusCode, Json<APIResponse>) {
match state
.controller
.send_command(ControllerCommands::ConnectToPeer {
address: request.peer.clone(),
})
.await
{
Ok(_) => (
StatusCode::OK,
Json(APIResponse::Success {
message: "Peer added".into(),
}),
),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(APIResponse::Error {
message: e.to_string(),
}),
),
}
}

pub async fn node_info(
State(state): State<AppState>,
) -> Result<Json<NodeInfoResponse>, StatusCode> {
let (snd, rcv) = tokio::sync::oneshot::channel();
match state
.controller
.send_command(ControllerCommands::ReturnNetworkState { channel: snd })
.await
{
Ok(_) => match rcv.await {
Ok(info) => Ok(Json(NodeInfoResponse::from(info))),
Err(e) => Err(StatusCode::INTERNAL_SERVER_ERROR),
},
Err(e) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}

#[derive(Serialize)]
pub struct NodeInfoResponse {
pub local_peer_id: String,
pub num_peers: u64,
pub dht_peers: Vec<Vec<String>>,
pub gossipsub_peers: Vec<String>,
pub all_external_addresses: Vec<Vec<String>>,
}

impl From<NetworkState> for NodeInfoResponse {
fn from(state: NetworkState) -> Self {
let NetworkState {
local_peer_id,
network_info,
dht_peers,
gossipsub_peers,
all_external_addresses,
..
} = state;
let dht_peers = dht_peers
.into_iter()
.map(|peer| peer.iter().map(|p| p.to_string()).collect())
.collect();
let gossipsub_peers = gossipsub_peers.into_iter().map(|p| p.to_string()).collect();
let all_external_addresses = all_external_addresses
.into_iter()
.map(|peer| peer.into_iter().map(|p| p.to_string()).collect())
.collect();
Self {
local_peer_id: local_peer_id.to_string(),
num_peers: network_info.num_peers() as u64,
dht_peers,
gossipsub_peers,
all_external_addresses,
}
}
}
}
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,17 @@ pub struct Config {
#[envconfig(from = "EXTERNAL_ADDRESS")]
pub external_address: Option<String>,

// if true interactive repl will run with node
#[envconfig(from = "INTERACTIVE", default = "false")]
pub interactive: bool,

// dictates if rpc will be used for rules evaluation
#[envconfig(from = "ENABLE_RPC", default = "true")]
pub enable_rpc: bool,

// secret key used to access admin api routes
#[envconfig(from = "ADMIN_API_SECRET")]
pub admin_api_secret: Option<String>,
}

impl Config {
Expand All @@ -82,6 +88,7 @@ impl Config {
external_address: None,
interactive: false,
enable_rpc: true,
admin_api_secret: None,
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ async fn main() -> eyre::Result<()> {
rules.add_default_rules();
let ctl = start_p2p_services(&config, rules).await?;

let router = api::router_with_defaults().await;
api::start_api(&config, ctl.clone(), router).await?;
let router = api::router_with_defaults();
api::start_api(&config, ctl.clone(), router, true).await?;

start_watch_chain::<ZoraPremintV2>(&config, ctl.clone()).await;
if config.interactive {
Expand Down
2 changes: 1 addition & 1 deletion src/p2p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use libp2p::multiaddr::Protocol;
use libp2p::swarm::{ConnectionId, NetworkBehaviour, NetworkInfo, SwarmEvent};
use libp2p::{gossipsub, kad, noise, tcp, yamux, Multiaddr, PeerId};
use sha256::digest;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::hash::Hasher;
use std::time::Duration;
use tokio::select;

Expand Down

0 comments on commit fb89ebf

Please sign in to comment.