From 9b985426924bcc60b3ae88916bf2639c0056a9a1 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 20 Dec 2023 12:27:10 +0100 Subject: [PATCH] httpd: Add `/api/v1/profile` route to get entire profile That route is limited for requests that come from a loopback IP. --- radicle-httpd/src/api/error.rs | 2 +- radicle-httpd/src/api/v1.rs | 7 ++ radicle-httpd/src/api/v1/profile.rs | 116 ++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 radicle-httpd/src/api/v1/profile.rs diff --git a/radicle-httpd/src/api/error.rs b/radicle-httpd/src/api/error.rs index e5c2ac4e..9698ead3 100644 --- a/radicle-httpd/src/api/error.rs +++ b/radicle-httpd/src/api/error.rs @@ -95,7 +95,7 @@ impl IntoResponse for Error { Error::CobStore(e @ radicle::cob::store::Error::NotFound(_, _)) => { (StatusCode::NOT_FOUND, Some(e.to_string())) } - Error::Auth(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())), + Error::Auth(msg) => (StatusCode::UNAUTHORIZED, Some(msg.to_string())), Error::Crypto(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())), Error::Surf(radicle_surf::Error::Git(e)) if radicle::git::is_not_found_err(&e) => { (StatusCode::NOT_FOUND, Some(e.message().to_owned())) diff --git a/radicle-httpd/src/api/v1.rs b/radicle-httpd/src/api/v1.rs index 112be1e5..189a2220 100644 --- a/radicle-httpd/src/api/v1.rs +++ b/radicle-httpd/src/api/v1.rs @@ -1,5 +1,6 @@ mod delegates; mod node; +mod profile; mod projects; mod sessions; mod stats; @@ -20,6 +21,7 @@ pub fn router(ctx: Context) -> Router { let routes = Router::new() .merge(root_router) .merge(node::router(ctx.clone())) + .merge(profile::router(ctx.clone())) .merge(sessions::router(ctx.clone())) .merge(delegates::router(ctx.clone())) .merge(projects::router(ctx.clone())) @@ -51,6 +53,11 @@ async fn root_handler(State(ctx): State) -> impl IntoResponse { "rel": "projects", "type": "GET" }, + { + "href": "/profile", + "rel": "profile", + "type": "GET" + }, { "href": "/stats", "rel": "stats", diff --git a/radicle-httpd/src/api/v1/profile.rs b/radicle-httpd/src/api/v1/profile.rs new file mode 100644 index 00000000..a0ab0958 --- /dev/null +++ b/radicle-httpd/src/api/v1/profile.rs @@ -0,0 +1,116 @@ +use std::net::SocketAddr; + +use axum::extract::{ConnectInfo, State}; +use axum::response::IntoResponse; +use axum::routing::get; +use axum::{Json, Router}; +use serde_json::json; + +use crate::api::error::Error; +use crate::api::Context; + +pub fn router(ctx: Context) -> Router { + Router::new() + .route("/profile", get(profile_handler)) + .with_state(ctx) +} + +/// Return local profile information. +/// `GET /profile` +async fn profile_handler( + State(ctx): State, + ConnectInfo(addr): ConnectInfo, +) -> impl IntoResponse { + if !addr.ip().is_loopback() { + return Err(Error::Auth("Profile data is only shown for localhost")); + } + + Ok::<_, Error>(Json( + json!({ "config": ctx.profile.config, "home": ctx.profile.home.path() }), + )) +} + +#[cfg(test)] +mod routes { + use std::net::SocketAddr; + + use axum::extract::connect_info::MockConnectInfo; + use axum::http::StatusCode; + use serde_json::json; + + use crate::test::{self, get}; + + #[tokio::test] + async fn test_remote_profile() { + let tmp = tempfile::tempdir().unwrap(); + let seed = test::seed(tmp.path()); + let app = super::router(seed.clone()) + .layer(MockConnectInfo(SocketAddr::from(([192, 168, 1, 1], 8080)))); + let response = get(&app, "/profile").await; + + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + assert_eq!( + response.json().await, + json!({ + "error": "Profile data is only shown for localhost", + "code": 401 + }) + ) + } + + #[tokio::test] + async fn test_profile() { + let tmp = tempfile::tempdir().unwrap(); + let seed = test::seed(tmp.path()); + let app = super::router(seed.clone()) + .layer(MockConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080)))); + let response = get(&app, "/profile").await; + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.json().await, + json!({ + "config": { + "publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid", + "preferredSeeds": [ + "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776" + ], + "cli": { + "hints": true + }, + "node": { + "alias": "seed", + "listen": [], + "peers": { + "type": "dynamic", + "target": 8 + }, + "connect": [], + "externalAddresses": [], + "network": "main", + "relay": true, + "limits": { + "routingMaxSize": 1000, + "routingMaxAge": 604800, + "gossipMaxAge": 1209600, + "fetchConcurrency": 1, + "rate": { + "inbound": { + "fillRate": 0.2, + "capacity": 32 + }, + "outbound": { + "fillRate": 1.0, + "capacity": 64 + } + } + }, + "policy": "block", + "scope": "followed" + } + }, + "home": seed.profile.path() + }) + ); + } +}