undercroft

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

main.rs (5980B)


      1 
      2 use axum::{
      3     extract::Request,
      4     http::{HeaderMap,HeaderValue},
      5     routing::get, Router,
      6     http::StatusCode
      7 };
      8 use std::sync::Arc;
      9 use std::net::SocketAddr;
     10 
     11 use std::process::Command;
     12 
     13 use hyper::body::Incoming;
     14 use hyper_util::rt::{TokioExecutor, TokioIo};
     15 use tokio::net::TcpListener;
     16 use tokio_rustls::{
     17     rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer},
     18     rustls::ServerConfig,
     19     TlsAcceptor,
     20 };
     21 use rustls::{
     22     server::WebPkiClientVerifier,
     23     RootCertStore
     24 };
     25 use serde_yaml::Value as YamlValue;
     26 
     27 use tower_service::Service;
     28 
     29 use clap::Parser;
     30 use log::{error,warn};
     31 
     32 const JSON_MIME : HeaderValue = HeaderValue::from_static("application/json");
     33 
     34 #[derive(Parser)]
     35 #[command(name = "undercroft")]
     36 #[command(version = "0.1")]
     37 #[command(about = "serves sops secrets to certified https clients",  long_about = None)]
     38 
     39 struct Cli {
     40     #[arg(long)]
     41     ca_certificate: String,
     42     #[arg(long)]
     43     certificate: String,
     44     #[arg(long)]
     45     key: String,
     46     #[arg(long)]
     47     port: Option<u16>,
     48     #[arg(long)]
     49     bind: Option<String>,
     50     secrets: String
     51 }
     52 
     53 fn yaml_to_json(content: &str) -> Result<String, &str> {
     54     match serde_yaml::from_str::<YamlValue>(content) {
     55         Ok(v) => {
     56             match serde_json::to_string_pretty(&v)  {
     57                 Ok(s) => Ok(s),
     58                 Err(e) => {
     59                     println!("error {:?}", e);
     60                     Err("can't json")
     61                 }
     62             }
     63         },
     64         Err(e) => {
     65             println!("error {:?}", e);
     66             Err("can't yaml")
     67         }
     68     }
     69 }
     70 
     71 
     72 
     73 async fn send_it(headers: HeaderMap, secrets_path : String) -> (StatusCode, String) {
     74     let output = Command::new("sops")
     75 	.arg("decrypt")
     76 	.arg(secrets_path)
     77 	.output()
     78 	.expect("failed to execute process");
     79 
     80     if output.status.success() {
     81         let body = String::from_utf8(output.stdout).expect("bad utf8");
     82         match headers.get("accept") {
     83             Some(ctype) if ctype == &JSON_MIME => {
     84                 // this is a cheat as we only look for a single mime type
     85                 // and we only look in the first Accept header, so you
     86                 // can easily not get json if you ask for any other content-type
     87                 // as well
     88 
     89                 if let Ok(j) = yaml_to_json(&body) {
     90                     (StatusCode::OK, j)
     91                 } else {
     92                     (StatusCode::INTERNAL_SERVER_ERROR, "serialize error".to_string())
     93                 }
     94             }
     95             _ => {
     96 	        (StatusCode::OK, body)
     97             }
     98         }
     99     } else {
    100         println!("error calling sops: {:}",
    101                  String::from_utf8(output.stderr).expect("bad utf8"));
    102 	(StatusCode::INTERNAL_SERVER_ERROR,
    103                   "error calling sops".to_string())
    104     }
    105 
    106 }
    107 
    108 
    109 #[tokio::main]
    110 async fn main() {
    111     let cli = Cli::parse();
    112     println!("secrets path = {:}", cli.secrets);
    113 
    114     let addr : SocketAddr = match cli.bind {
    115 	Some(s) => s.parse().expect("can't parse bind address"),
    116 	None => {
    117 	    let port = cli.port.unwrap_or(3000);
    118 	    SocketAddr::from(([127, 0, 0, 1], port))
    119 	}
    120     };
    121 
    122     let private_key = PrivateKeyDer::from_pem_file(cli.key).unwrap();
    123 
    124     let certs = CertificateDer::pem_file_iter(cli.certificate)
    125         .unwrap()
    126         .map(|cert| cert.unwrap())
    127         .collect();
    128 
    129     let ca_cert =  CertificateDer::pem_file_iter(cli.ca_certificate)
    130         .unwrap()
    131         .map(|cert| cert.unwrap())
    132         .next();
    133 
    134     let mut root_cert_store = RootCertStore::empty();
    135     if let Some(c) = ca_cert {
    136 	root_cert_store.add(c).expect("bad ca certificate");
    137     };
    138     let client_cert_verifier =
    139 	WebPkiClientVerifier::builder(root_cert_store.into())
    140 	.build()
    141 	.unwrap();
    142 
    143     let mut tls_config =
    144         ServerConfig::builder()
    145         .with_client_cert_verifier(client_cert_verifier)
    146         .with_single_cert(certs, private_key)
    147         .expect("bad certificate/key");
    148 
    149     tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
    150 
    151     let tls_acceptor = TlsAcceptor::from(Arc::new(tls_config));
    152 
    153     let tcp_listener = TcpListener::bind(addr).await.unwrap();
    154 
    155     println!("listening on {}", addr);
    156 
    157     let handler = move |headers: HeaderMap| send_it(headers, cli.secrets) ;
    158     let app = Router::new().route("/", get(handler));
    159 
    160     loop {
    161         let tower_service = app.clone();
    162         let tls_acceptor = tls_acceptor.clone();
    163 
    164         // Wait for new tcp connection
    165         let (cnx, addr) = tcp_listener.accept().await.unwrap();
    166 
    167         tokio::spawn(async move {
    168             // Wait for tls handshake to happen
    169             let Ok(stream) = tls_acceptor.accept(cnx).await else {
    170                 error!("error during tls handshake connection from {}", addr);
    171                 return;
    172             };
    173 
    174             // Hyper has its own `AsyncRead` and `AsyncWrite` traits
    175             // and doesn't use tokio.  `TokioIo` converts between
    176             // them.
    177             let stream = TokioIo::new(stream);
    178 
    179             // Hyper also has its own `Service` trait and doesn't use
    180             // tower. We can use `hyper::service::service_fn` to
    181             // create a hyper `Service` that calls our app through
    182             // `tower::Service::call`.
    183             let hyper_service = hyper::service::service_fn(move |request: Request<Incoming>| {
    184 
    185                 // We have to clone `tower_service` because hyper's
    186 		// `Service` uses `&self` whereas
    187                 // tower's `Service` requires `&mut self`.
    188                 //
    189                 // We don't need to call `poll_ready` since `Router`
    190                 // is always ready.
    191                 tower_service.clone().call(request)
    192             });
    193 
    194             let ret = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
    195                 .serve_connection_with_upgrades(stream, hyper_service)
    196                 .await;
    197 	    if let Err(err) = ret {
    198                 warn!("error serving connection from {}: {}", addr, err);
    199             }
    200         });
    201     }
    202 }