diff options
author | metamuffin <metamuffin@disroot.org> | 2024-08-12 20:49:47 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2024-08-12 20:49:47 +0200 |
commit | ef8172874f650078e8cfb6e1582de4ece5495640 (patch) | |
tree | 35ced9080db86739904e903a68999fd1606bc9b1 | |
parent | b28c5418b0635bf2fc3b0d18922df4ebb7cccd57 (diff) | |
download | gnix-ef8172874f650078e8cfb6e1582de4ece5495640.tar gnix-ef8172874f650078e8cfb6e1582de4ece5495640.tar.bz2 gnix-ef8172874f650078e8cfb6e1582de4ece5495640.tar.zst |
add any/all conditions and experimental CGI support
-rw-r--r-- | Cargo.lock | 11 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | readme.md | 9 | ||||
-rw-r--r-- | src/config.rs | 2 | ||||
-rw-r--r-- | src/error.rs | 9 | ||||
-rw-r--r-- | src/modules/cgi.rs | 142 | ||||
-rw-r--r-- | src/modules/mod.rs | 34 | ||||
-rw-r--r-- | src/modules/switch.rs | 4 |
8 files changed, 187 insertions, 26 deletions
@@ -609,6 +609,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", + "users", ] [[package]] @@ -1575,6 +1576,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + +[[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -48,3 +48,5 @@ thiserror = "1.0.61" aes-gcm-siv = "0.11.1" argon2 = "0.6.0-pre.0" rand = "0.9.0-alpha.1" + +users = "0.11.0"
\ No newline at end of file @@ -166,6 +166,8 @@ themselves; in that case the request is passed on. prefix - `!path_is <path>`: Checks if the URI path is exactly what you specified - `!has_header <name>`: Checks if the request includes a certain header. + - `!any [conditions]`: Checks if any of a set of conditions are satisfied. + - `!all [conditions]`: Checks if all conditions are satisfied. - `case_true` Handler with matched requests (module) - `case_false` Handler for all other requests (module) @@ -178,6 +180,13 @@ themselves; in that case the request is passed on. - Responds with a permanent redirect. - Takes the location to redirect to. (string) +- **module `cgi`** + - Runs a CGI script on the request. **This is experimental and probably + vulnerable! Don't use this.** + - `bin`: Path to the CGI binary + - `user`: User that the script is executed as. Requires to run gnix as root. + (optional string) + #### Credentials config format Login credentials for `cookie_auth` and `http_basic_auth` are supplied as either diff --git a/src/config.rs b/src/config.rs index ef90f46..e3b0d3c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -161,7 +161,7 @@ impl<'de> Deserialize<'de> for DynNode { .ok_or(serde::de::Error::unknown_variant(s, &[]))? .instanciate(tv.value) .map_err(|e| { - let x = format!("instanciating modules {s:?}: {e:?}"); + let x = format!("instanciating module {s:?}: {e:?}"); serde::de::Error::custom(e.context(x)) })?; diff --git a/src/error.rs b/src/error.rs index e7e5af2..14b2842 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,14 +1,11 @@ -use tokio::sync::TryAcquireError; - #[derive(Debug, thiserror::Error)] pub enum ServiceError { #[error("no response generated; the proxy is misconfigured")] NoResponse, #[error("request taken; the proxy is misconfigured")] RequestTaken, - #[error("limit reached. try again")] - Limit(#[from] TryAcquireError), + Limit(#[from] tokio::sync::TryAcquireError), #[error("hyper error")] Hyper(hyper::Error), #[error("no host")] @@ -37,6 +34,10 @@ pub enum ServiceError { UpgradeFailed, #[error("{0}")] Custom(String), + #[error("parse int error: {0}")] + ParseIntError(#[from] std::num::ParseIntError), + #[error("invalid header")] + InvalidHeader, #[error("impossible error")] Other, } diff --git a/src/modules/cgi.rs b/src/modules/cgi.rs new file mode 100644 index 0000000..b6f1033 --- /dev/null +++ b/src/modules/cgi.rs @@ -0,0 +1,142 @@ +use super::{Node, NodeKind, NodeResponse}; +use crate::error::ServiceError; +use anyhow::{anyhow, Result}; +use futures::TryStreamExt; +use http_body_util::{combinators::BoxBody, StreamBody}; +use hyper::{ + body::Frame, + header::{HeaderName, HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}, + Response, StatusCode, +}; +use serde::Deserialize; +use serde_yaml::Value; +use std::{future::Future, path::PathBuf, pin::Pin, process::Stdio, str::FromStr, sync::Arc}; +use tokio::{ + io::{AsyncBufReadExt, BufReader}, + process::Command, +}; +use tokio_util::io::ReaderStream; +use users::get_user_by_name; + +pub struct CgiKind; + +#[derive(Deserialize)] +struct CgiConfig { + bin: PathBuf, + user: Option<String>, +} +struct Cgi { + config: CgiConfig, + user: Option<u32>, +} + +impl NodeKind for CgiKind { + fn name(&self) -> &'static str { + "cgi" + } + fn instanciate(&self, config: Value) -> Result<Arc<dyn Node>> { + Ok(Arc::new(Cgi::new(serde_yaml::from_value::<CgiConfig>( + config, + )?)?)) + } +} +impl Cgi { + pub fn new(config: CgiConfig) -> Result<Self> { + Ok(Self { + user: config + .user + .as_ref() + .map(|u| { + get_user_by_name(u) + .map(|u| u.uid()) + .ok_or(anyhow!("user does not exist")) + }) + .transpose()?, + config, + }) + } +} +impl Node for Cgi { + fn handle<'a>( + &'a self, + context: &'a mut super::NodeContext, + request: super::NodeRequest, + ) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>> { + Box::pin(async move { + let mut command = Command::new(&self.config.bin); + command.stdin(Stdio::piped()); + command.stdout(Stdio::piped()); + command.stderr(Stdio::inherit()); + if let Some(uid) = self.user { + command.uid(uid); + } + + // command.env("AUTH_TYPE", ); + command.env( + "CONTENT_LENGTH", + request + .headers() + .get(CONTENT_LENGTH) + .map(|x| x.to_str().ok()) + .flatten() + .unwrap_or_default(), + ); + command.env( + "CONTENT_TYPE", + request + .headers() + .get(CONTENT_TYPE) + .map(|x| x.to_str().ok()) + .flatten() + .unwrap_or_default(), + ); + command.env("GATEWAY_INTERFACE", "CGI/1.1"); + command.env("PATH_INFO", request.uri().path()); + command.env("PATH_TRANSLATED", request.uri().path()); + command.env("QUERY_STRING", request.uri().query().unwrap_or_default()); + command.env("REMOTE_ADDR", context.addr.to_string()); + // command.env("REMOTE_HOST", )); + // command.env("REMOTE_IDENT", )); + // command.env("REMOTE_USER", )); + command.env("REQUEST_METHOD", request.method().to_string()); + // command.env("SCRIPT_NAME", ); + // command.env("SERVER_NAME", ); + // command.env("SERVER_PORT", ); + command.env("SERVER_PROTOCOL", "HTTP/1.1"); + command.env("SERVER_SOFTWARE", "gnix"); + + let mut child = command.spawn()?; + let mut stdout = BufReader::new(child.stdout.take().unwrap()); + let mut line = String::new(); + let mut response = Response::new(()); + loop { + line.clear(); + stdout.read_line(&mut line).await?; + let line = line.trim(); + if line.is_empty() { + break; + } + let (key, value) = line.split_once(":").ok_or(ServiceError::Other)?; + let value = value.trim(); + if key == "Status" { + *response.status_mut() = StatusCode::from_u16( + value.split_once(" ").unwrap_or((value, "")).0.parse()?, + ) + .map_err(|_| ServiceError::InvalidHeader)?; + } else { + response.headers_mut().insert( + HeaderName::from_str(key).map_err(|_| ServiceError::InvalidHeader)?, + HeaderValue::from_str(value).map_err(|_| ServiceError::InvalidHeader)?, + ); + } + } + Ok(response.map(|()| { + BoxBody::new(StreamBody::new( + ReaderStream::new(stdout) + .map_ok(Frame::data) + .map_err(ServiceError::Io), + )) + })) + }) + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 00425bf..987646f 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -1,24 +1,15 @@ use crate::error::ServiceError; use crate::State; -use accesslog::AccessLogKind; -use auth::{basic::HttpBasicAuthKind, cookie::CookieAuthKind}; use bytes::Bytes; -use error::ErrorKind; -use file::FileKind; -use files::FilesKind; use futures::Future; -use headers::HeadersKind; -use hosts::HostsKind; use http_body_util::combinators::BoxBody; use hyper::{body::Incoming, Request, Response}; -use proxy::ProxyKind; -use redirect::RedirectKind; use serde_yaml::Value; use std::{net::SocketAddr, pin::Pin, sync::Arc}; -use switch::SwitchKind; pub mod accesslog; pub mod auth; +pub mod cgi; pub mod error; pub mod file; pub mod files; @@ -32,17 +23,18 @@ pub type NodeRequest = Request<Incoming>; pub type NodeResponse = Response<BoxBody<Bytes, ServiceError>>; pub static MODULES: &[&dyn NodeKind] = &[ - &HttpBasicAuthKind, - &CookieAuthKind, - &ProxyKind, - &HostsKind, - &FilesKind, - &FileKind, - &AccessLogKind, - &ErrorKind, - &HeadersKind, - &SwitchKind, - &RedirectKind, + &auth::basic::HttpBasicAuthKind, + &auth::cookie::CookieAuthKind, + &proxy::ProxyKind, + &hosts::HostsKind, + &files::FilesKind, + &file::FileKind, + &accesslog::AccessLogKind, + &error::ErrorKind, + &headers::HeadersKind, + &switch::SwitchKind, + &redirect::RedirectKind, + &cgi::CgiKind, ]; pub struct NodeContext { diff --git a/src/modules/switch.rs b/src/modules/switch.rs index bbb9e98..943a81d 100644 --- a/src/modules/switch.rs +++ b/src/modules/switch.rs @@ -47,6 +47,8 @@ impl Node for Switch { #[derive(Deserialize)] #[serde(rename_all = "snake_case")] enum Condition { + Any(Vec<Condition>), + All(Vec<Condition>), IsWebsocketUpgrade, IsPost, IsGet, @@ -66,6 +68,8 @@ impl Condition { Condition::PathIs(path) => req.uri().path() == path, Condition::IsPost => req.method() == Method::POST, Condition::IsGet => req.method() == Method::GET, + Condition::Any(conds) => conds.iter().any(|c| c.test(req)), + Condition::All(conds) => conds.iter().all(|c| c.test(req)), } } } |