summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-08-12 20:49:47 +0200
committermetamuffin <metamuffin@disroot.org>2024-08-12 20:49:47 +0200
commitef8172874f650078e8cfb6e1582de4ece5495640 (patch)
tree35ced9080db86739904e903a68999fd1606bc9b1
parentb28c5418b0635bf2fc3b0d18922df4ebb7cccd57 (diff)
downloadgnix-ef8172874f650078e8cfb6e1582de4ece5495640.tar
gnix-ef8172874f650078e8cfb6e1582de4ece5495640.tar.bz2
gnix-ef8172874f650078e8cfb6e1582de4ece5495640.tar.zst
add any/all conditions and experimental CGI support
-rw-r--r--Cargo.lock11
-rw-r--r--Cargo.toml2
-rw-r--r--readme.md9
-rw-r--r--src/config.rs2
-rw-r--r--src/error.rs9
-rw-r--r--src/modules/cgi.rs142
-rw-r--r--src/modules/mod.rs34
-rw-r--r--src/modules/switch.rs4
8 files changed, 187 insertions, 26 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 83ee38f..e3765bf 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 646cad5..16f6753 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
diff --git a/readme.md b/readme.md
index 428fabd..609d420 100644
--- a/readme.md
+++ b/readme.md
@@ -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)),
}
}
}