Skip to content

Commit

Permalink
feat: Add axum web server
Browse files Browse the repository at this point in the history
`cli` has been configured with a command to launch an http server using
axum. `cargo run --bin annapurna-cli server http` can now launch a
webserver.

This expects a running instance of
[lockpad](https://github.com/justinrubek/lockpad) or equivalent host
running and its URL accessible in environment variable
`ANNAPURNA_AUTH_URL`.
  • Loading branch information
justinrubek committed Jun 18, 2023
1 parent 63b0041 commit 9aa1260
Show file tree
Hide file tree
Showing 10 changed files with 1,682 additions and 50 deletions.
1,513 changes: 1,465 additions & 48 deletions Cargo.lock

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
[package]
name = "annapurna-cli"
name = "annapurna"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "annapurna-cli"
path = "src/main.rs"

[dependencies]
annapurna-logic = { path = "../logic" }
annapurna-http = { path = "../http" }
clap = { version = "4.1.13", features = ["derive"] }
config = "0.13.3"
lockpad-auth = { path = "../../../lockpad/crates/auth" }
ron = "0.8.0"
# clap = { version = "4.0.19", features = ["derive"] }
# reqwest = { version = "0.11.12", features = ["rustls-tls"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.87"
tracing-subscriber = "0.3.16"
reqwest = { version = "0.11.14", default-features = false, features = ["rustls-tls", "json"] }
tokio.workspace = true
# tokio = { version = "1", features = ["full"] }
5 changes: 5 additions & 0 deletions crates/cli/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
pub(crate) mod server;
use server::ServerCommand;

#[derive(clap::Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub(crate) struct Args {
Expand All @@ -8,6 +11,8 @@ pub(crate) struct Args {
#[derive(clap::Subcommand, Debug)]
pub(crate) enum Commands {
Command(Command),
/// commands for running the server
Server(ServerCommand),
}

#[derive(clap::Args, Debug)]
Expand Down
45 changes: 45 additions & 0 deletions crates/cli/src/commands/server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use annapurna::config::Config;
use lockpad_auth::PublicKey;

#[derive(clap::Args, Debug)]
pub(crate) struct ServerCommand {
#[clap(subcommand)]
pub command: ServerCommands,

#[arg(default_value = "0.0.0.0:5000", long, short)]
pub addr: std::net::SocketAddr,
}

#[derive(clap::Subcommand, Debug)]
pub(crate) enum ServerCommands {
/// start the http server
Http,
}

impl ServerCommand {
pub(crate) async fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
let config = Config::load()?;
let auth_url = config.auth_url;

let client = reqwest::Client::new();
let res = client
.get(format!("{auth_url}/.well-known/jwks.json"))
.send()
.await
.unwrap();
let jwks_str = res.text().await.unwrap();

let key_set = PublicKey::parse_from_jwks(&jwks_str)?;

let server = annapurna_http::Server::builder()
.addr(self.addr)
.public_keys(key_set)
.build()?;

match self.command {
ServerCommands::Http => server.run().await?,
}

Ok(())
}
}
16 changes: 16 additions & 0 deletions crates/cli/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Config {
pub auth_url: String,
}

impl Config {
pub fn load() -> Result<Self, config::ConfigError> {
let config = config::Config::builder()
.add_source(config::Environment::with_prefix("ANNAPURNA"))
.build()?;

config.try_deserialize()
}
}
1 change: 1 addition & 0 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod config;
4 changes: 3 additions & 1 deletion crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use std::collections::HashMap;
pub mod commands;
use commands::{BasicCommands, Commands};

fn main() -> Result<(), Box<dyn std::error::Error>> {
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();

let args = commands::Args::parse();
Expand All @@ -31,6 +32,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
recipe(recipes, inventory);
}
},
Commands::Server(server) => server.run().await?,
}

Ok(())
Expand Down
16 changes: 16 additions & 0 deletions crates/http/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "annapurna-http"
version.workspace = true
edition.workspace = true

[dependencies]
axum = { workspace = true }
hyper = "0.14.24"
lockpad-auth = { path = "../../../lockpad/crates/auth" }
serde = { workspace = true }
serde_json = "1.0.87"
thiserror = { workspace = true }
tokio = { workspace = true }
tower = { version = "0.4", features = ["util"] }
tower-http = { version = "0.3.0", features = ["fs", "cors"] }
tracing = { workspace = true }
24 changes: 24 additions & 0 deletions crates/http/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Hyper(#[from] hyper::Error),
#[error(transparent)]
LockpadAuth(#[from] lockpad_auth::error::Error),

#[error("Failed to build server struct")]
ServerBuilder,
}

pub type Result<T> = std::result::Result<T, Error>;

impl axum::response::IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
tracing::info!(?self, "error response");
#[allow(clippy::match_single_binding)]
let status = match self {
_ => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
};

(status, self.to_string()).into_response()
}
}
97 changes: 97 additions & 0 deletions crates/http/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#[allow(unused_imports)]
use axum::{
routing::{get, post},
Router,
};
use lockpad_auth::PublicKey;
use std::net::SocketAddr;

pub mod error;

use error::Result;

pub struct Server {
addr: SocketAddr,

public_keys: Vec<PublicKey>,
}

#[derive(Clone)]
pub struct ServerState {
pub public_key: PublicKey,
}

impl AsRef<PublicKey> for ServerState {
fn as_ref(&self) -> &PublicKey {
&self.public_key
}
}

impl Server {
pub fn builder() -> Builder {
Builder::default()
}

pub async fn run(self) -> Result<()> {
let cors = tower_http::cors::CorsLayer::permissive();

let public_key = self.public_keys[0].clone();
let state = ServerState { public_key };

let app = Router::new()
.route("/", get(root))
.with_state(state)
.layer(cors);

tracing::info!("Listening on {0}", self.addr);
axum::Server::bind(&self.addr)
.serve(app.into_make_service())
.await?;

Ok(())
}
}

pub struct Builder {
addr: Option<SocketAddr>,
public_keys: Option<Vec<PublicKey>>,
}

impl Builder {
pub fn new() -> Self {
Self {
addr: None,
public_keys: None,
}
}

pub fn addr(mut self, addr: SocketAddr) -> Self {
self.addr = Some(addr);
self
}

pub fn public_keys(mut self, public_keys: Vec<PublicKey>) -> Self {
self.public_keys = Some(public_keys);
self
}

pub fn build(self) -> Result<Server> {
let addr = self.addr.ok_or(error::Error::ServerBuilder)?;
let public_keys = self.public_keys.ok_or(error::Error::ServerBuilder)?;

Ok(Server { addr, public_keys })
}
}

impl Default for Builder {
fn default() -> Self {
Self {
addr: Some(SocketAddr::from(([0, 0, 0, 0], 3000))),
public_keys: None,
}
}
}

async fn root() -> &'static str {
"Hello, World!"
}

0 comments on commit 9aa1260

Please sign in to comment.