Migrate `/v1/`, `/v1/node` routes to heartwood
Signed-off-by: xphoniex <dj.2dixx@gmail.com>
This commit is contained in:
parent
da55e521a1
commit
acd7aeb9c9
File diff suppressed because it is too large
Load Diff
|
|
@ -16,17 +16,22 @@ logfmt = [
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { version = "1" }
|
anyhow = { version = "1" }
|
||||||
flate2 = { version = "1" }
|
axum = { version = "0.5.16", default-features = false, features = ["json", "headers", "query"] }
|
||||||
lexopt = { version = "0.2.1" }
|
|
||||||
thiserror = { version = "1" }
|
|
||||||
tokio = { version = "1.21", default-features = false, features = ["macros", "rt-multi-thread"] }
|
|
||||||
tracing = { version = "0.1.37", default-features = false, features = ["std", "log"] }
|
|
||||||
tracing-subscriber = { version = "0.3", default-features = false, features = ["std", "ansi", "fmt"] }
|
|
||||||
tracing-logfmt = { version = "0.2", optional = true }
|
|
||||||
axum = { version = "0.5.16", default-features = false }
|
|
||||||
axum-server = { version = "0.4.2", default-features = false }
|
axum-server = { version = "0.4.2", default-features = false }
|
||||||
|
ethers-core = { version = "1.0" }
|
||||||
|
flate2 = { version = "1" }
|
||||||
hyper = { version = "0.14.17", default-features = false }
|
hyper = { version = "0.14.17", default-features = false }
|
||||||
tower-http = { version = "0.3.4", default-features = false, features = ["trace"] }
|
lexopt = { version = "0.2.1" }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = { version = "1", features = ["preserve_order"] }
|
||||||
|
siwe = { version = "0.5" }
|
||||||
|
thiserror = { version = "1" }
|
||||||
|
time = { version = "0.3.17" }
|
||||||
|
tokio = { version = "1.21", default-features = false, features = ["macros", "rt-multi-thread"] }
|
||||||
|
tower-http = { version = "0.3.4", default-features = false, features = ["trace", "cors"] }
|
||||||
|
tracing = { version = "0.1.37", default-features = false, features = ["std", "log"] }
|
||||||
|
tracing-logfmt = { version = "0.2", optional = true }
|
||||||
|
tracing-subscriber = { version = "0.3", default-features = false, features = ["std", "ansi", "fmt"] }
|
||||||
|
|
||||||
[dependencies.radicle]
|
[dependencies.radicle]
|
||||||
path = "../radicle"
|
path = "../radicle"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use axum::body::{Body, BoxBody};
|
||||||
|
use axum::http::header::{AUTHORIZATION, CONTENT_TYPE};
|
||||||
|
use axum::http::Method;
|
||||||
|
use axum::response::{IntoResponse, Json};
|
||||||
|
use axum::routing::get;
|
||||||
|
use axum::{Extension, Router};
|
||||||
|
use hyper::http::{Request, Response};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::json;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use tower_http::cors::{self, CorsLayer};
|
||||||
|
use tower_http::trace::TraceLayer;
|
||||||
|
use tracing::Span;
|
||||||
|
|
||||||
|
use radicle::{Profile, Storage};
|
||||||
|
|
||||||
|
mod auth;
|
||||||
|
mod v1;
|
||||||
|
|
||||||
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
/// Identifier for sessions
|
||||||
|
type SessionId = String;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Context {
|
||||||
|
profile: Arc<Profile>,
|
||||||
|
sessions: Arc<RwLock<HashMap<SessionId, auth::AuthState>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub fn new(profile: Arc<Profile>) -> Self {
|
||||||
|
Self {
|
||||||
|
profile,
|
||||||
|
sessions: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn storage(&self) -> &Storage {
|
||||||
|
&self.profile.storage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn router(ctx: Context) -> Router {
|
||||||
|
let root_router = Router::new()
|
||||||
|
.route("/", get(root_handler))
|
||||||
|
.layer(Extension(ctx.clone()));
|
||||||
|
|
||||||
|
Router::new()
|
||||||
|
.merge(root_router)
|
||||||
|
.merge(v1::router(ctx))
|
||||||
|
.layer(
|
||||||
|
CorsLayer::new()
|
||||||
|
.max_age(Duration::from_secs(86400))
|
||||||
|
.allow_origin(cors::Any)
|
||||||
|
.allow_methods([Method::GET, Method::POST, Method::PUT])
|
||||||
|
.allow_headers([CONTENT_TYPE, AUTHORIZATION]),
|
||||||
|
)
|
||||||
|
.layer(
|
||||||
|
TraceLayer::new_for_http()
|
||||||
|
.make_span_with(|request: &Request<Body>| {
|
||||||
|
tracing::info_span!(
|
||||||
|
"request",
|
||||||
|
method = %request.method(),
|
||||||
|
uri = %request.uri(),
|
||||||
|
status = tracing::field::Empty,
|
||||||
|
latency = tracing::field::Empty,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on_response(
|
||||||
|
|response: &Response<BoxBody>, latency: Duration, span: &Span| {
|
||||||
|
span.record("status", &tracing::field::debug(response.status()));
|
||||||
|
span.record("latency", &tracing::field::debug(latency));
|
||||||
|
|
||||||
|
tracing::info!("Processed");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn root_handler(Extension(ctx): Extension<Context>) -> impl IntoResponse {
|
||||||
|
let response = json!({
|
||||||
|
"message": "Welcome!",
|
||||||
|
"service": "radicle-httpd",
|
||||||
|
"version": format!("{}-{}", VERSION, env!("GIT_HEAD")),
|
||||||
|
"node": { "id": ctx.profile.public_key },
|
||||||
|
"path": "/",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"href": "/v1/projects",
|
||||||
|
"rel": "projects",
|
||||||
|
"type": "GET"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/v1/node",
|
||||||
|
"rel": "node",
|
||||||
|
"type": "GET"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/v1/delegates/:urn/projects",
|
||||||
|
"rel": "projects",
|
||||||
|
"type": "GET"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
Json(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct PaginationQuery {
|
||||||
|
pub page: Option<usize>,
|
||||||
|
pub per_page: Option<usize>,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use ethers_core::types::{Signature, H160};
|
||||||
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DateTime(OffsetDateTime);
|
||||||
|
|
||||||
|
impl Serialize for DateTime {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
serializer.serialize_str(&format!("{}", self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct AuthRequest {
|
||||||
|
pub message: String,
|
||||||
|
#[serde(deserialize_with = "deserialize_signature")]
|
||||||
|
pub signature: Signature,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_signature<'de, D>(deserializer: D) -> Result<Signature, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let buf = String::deserialize(deserializer)?;
|
||||||
|
Signature::from_str(&buf).map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum AuthState {
|
||||||
|
Authorized(Session),
|
||||||
|
Unauthorized {
|
||||||
|
nonce: String,
|
||||||
|
expiration_time: DateTime,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// We copy the implementation of siwe::Message here to derive Serialization and Debug
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Session {
|
||||||
|
pub domain: String,
|
||||||
|
pub address: H160,
|
||||||
|
pub statement: Option<String>,
|
||||||
|
pub uri: String,
|
||||||
|
pub version: u64,
|
||||||
|
pub chain_id: u64,
|
||||||
|
pub nonce: String,
|
||||||
|
pub issued_at: DateTime,
|
||||||
|
pub expiration_time: Option<DateTime>,
|
||||||
|
pub resources: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<siwe::Message> for Session {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(message: siwe::Message) -> Result<Session, Error> {
|
||||||
|
Ok(Session {
|
||||||
|
domain: message.domain.host().to_string(),
|
||||||
|
address: H160(message.address),
|
||||||
|
statement: None,
|
||||||
|
uri: message.uri.to_string(),
|
||||||
|
version: message.version as u64,
|
||||||
|
chain_id: message.chain_id,
|
||||||
|
nonce: message.nonce,
|
||||||
|
issued_at: DateTime(message.issued_at.as_ref().to_owned()),
|
||||||
|
expiration_time: message
|
||||||
|
.expiration_time
|
||||||
|
.map(|x| DateTime(x.as_ref().to_owned())),
|
||||||
|
resources: message.resources.iter().map(|r| r.to_string()).collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
#[test]
|
||||||
|
fn test_auth_request_de() {
|
||||||
|
let json = serde_json::json!({
|
||||||
|
"message": "Hello World!",
|
||||||
|
"signature": "20096c6ed2bcccb88c9cafbbbbda7a5a3cff6d0ca318c07faa58464083ca40a92f899fbeb26a4c763a7004b13fd0f1ba6c321d4e3a023e30f63c40d4154b99a41c"
|
||||||
|
});
|
||||||
|
|
||||||
|
let _req: super::AuthRequest = serde_json::from_value(json).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
mod node;
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
use crate::api::Context;
|
||||||
|
|
||||||
|
pub fn router(ctx: Context) -> Router {
|
||||||
|
let routes = Router::new().merge(node::router(ctx));
|
||||||
|
|
||||||
|
Router::new().nest("/v1", routes)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::routing::get;
|
||||||
|
use axum::{Extension, Json, Router};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use radicle::node::NodeId;
|
||||||
|
|
||||||
|
use crate::api::Context;
|
||||||
|
|
||||||
|
pub fn router(ctx: Context) -> Router {
|
||||||
|
let node_id = ctx.profile.public_key;
|
||||||
|
|
||||||
|
Router::new()
|
||||||
|
.route("/node", get(node_handler))
|
||||||
|
.layer(Extension(node_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the node id for the node identity.
|
||||||
|
/// `GET /node`
|
||||||
|
async fn node_handler(Extension(node_id): Extension<NodeId>) -> impl IntoResponse {
|
||||||
|
let response = json!({
|
||||||
|
"id": node_id.to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Json(response)
|
||||||
|
}
|
||||||
|
|
@ -32,6 +32,8 @@ use radicle::profile::Profile;
|
||||||
|
|
||||||
use error::Error;
|
use error::Error;
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -39,7 +41,7 @@ pub struct Options {
|
||||||
pub listen: net::SocketAddr,
|
pub listen: net::SocketAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the Git Server.
|
/// Run the Server.
|
||||||
pub async fn run(options: Options) -> anyhow::Result<()> {
|
pub async fn run(options: Options) -> anyhow::Result<()> {
|
||||||
let git_version = Command::new("git")
|
let git_version = Command::new("git")
|
||||||
.arg("version")
|
.arg("version")
|
||||||
|
|
@ -48,12 +50,12 @@ pub async fn run(options: Options) -> anyhow::Result<()> {
|
||||||
.stdout;
|
.stdout;
|
||||||
tracing::info!("{}", str::from_utf8(&git_version)?.trim());
|
tracing::info!("{}", str::from_utf8(&git_version)?.trim());
|
||||||
|
|
||||||
let profile = radicle::Profile::load()?;
|
let profile = Arc::new(radicle::Profile::load()?);
|
||||||
tracing::info!("using radicle home at {}", profile.home.display());
|
tracing::info!("using radicle home at {}", profile.home.display());
|
||||||
|
|
||||||
let app = Router::new()
|
let git_router = Router::new()
|
||||||
.route("/:project/*request", any(git_handler))
|
.route("/:project/*request", any(git_handler))
|
||||||
.layer(Extension(Arc::new(profile)))
|
.layer(Extension(profile.clone()))
|
||||||
.layer(
|
.layer(
|
||||||
TraceLayer::new_for_http()
|
TraceLayer::new_for_http()
|
||||||
.make_span_with(|request: &Request<Body>| {
|
.make_span_with(|request: &Request<Body>| {
|
||||||
|
|
@ -73,11 +75,18 @@ pub async fn run(options: Options) -> anyhow::Result<()> {
|
||||||
tracing::info!("Processed");
|
tracing::info!("Processed");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
.into_make_service_with_connect_info::<SocketAddr>();
|
|
||||||
|
let ctx = api::Context::new(profile);
|
||||||
|
let api_router = api::router(ctx);
|
||||||
|
|
||||||
tracing::info!("listening on http://{}", options.listen);
|
tracing::info!("listening on http://{}", options.listen);
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.merge(git_router)
|
||||||
|
.nest("/api", api_router)
|
||||||
|
.into_make_service_with_connect_info::<SocketAddr>();
|
||||||
|
|
||||||
axum::Server::bind(&options.listen)
|
axum::Server::bind(&options.listen)
|
||||||
.serve(app)
|
.serve(app)
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue