From e9f71f600d3036ba961b438f58e4a2918f3ed40f Mon Sep 17 00:00:00 2001 From: int 80h Date: Sun, 17 May 2020 00:17:35 -0400 Subject: [PATCH] SCGI --- README | 6 +- cgi-scripts/scgi/scgi.pl | 16 ++++++ cgi-scripts/scgi/scgi.py | 21 +++++++ config.toml | 2 + src/cgi.rs | 119 ++++++++++++++++++++++++++++++--------- src/config.rs | 1 + src/main.rs | 15 +++++ 7 files changed, 153 insertions(+), 27 deletions(-) create mode 100755 cgi-scripts/scgi/scgi.pl create mode 100755 cgi-scripts/scgi/scgi.py diff --git a/README b/README index aff805b..ffe5f6b 100644 --- a/README +++ b/README @@ -33,7 +33,11 @@ running: Thanks to tiwesdaeg for figuring it out. -## CGI Environments +## CGI and SCGI + +There's example SCGI scripts for python and perl in the cgi-scripts directory. + +### CGI Environments In the configuration file there's "cgi" which is an optional bool to turn cgi on. If it's true it'll run scripts from any directory. To limit it to only one diff --git a/cgi-scripts/scgi/scgi.pl b/cgi-scripts/scgi/scgi.pl new file mode 100755 index 0000000..6fff2ef --- /dev/null +++ b/cgi-scripts/scgi/scgi.pl @@ -0,0 +1,16 @@ +#!/usr/local/bin/perl + + +use SCGI; +use IO::Socket; + +my $socket = IO::Socket::INET->new(Listen => 5, ReuseAddr => 1, LocalPort => 4001) + or die "cannot bind to port 4001: $!"; + +my $scgi = SCGI->new($socket, blocking => 1); + +while (my $request = $scgi->accept) { + $request->read_env; + read $request->connection, my $body, $request->env->{CONTENT_LENGTH}; + print { $request->connection } "20\ttext/gemini\r\nperl\n"; + } diff --git a/cgi-scripts/scgi/scgi.py b/cgi-scripts/scgi/scgi.py new file mode 100755 index 0000000..c7f193f --- /dev/null +++ b/cgi-scripts/scgi/scgi.py @@ -0,0 +1,21 @@ +#! /usr/local/bin/python3.6 +import scgi +import scgi.scgi_server + +class TimeHandler(scgi.scgi_server.SCGIHandler): + def produce(self, env, bodysize, input, output): + header = "20\ttext/gemini\r\n" + hi = "python\n" + output.write(header.encode()) + output.write(hi.encode()) + + # Main program: create an SCGIServer object to + # listen on port 4000. We tell the SCGIServer the + # handler class that implements our application. +server = scgi.scgi_server.SCGIServer( + handler_class=TimeHandler, + port=4000 + ) + # Tell our SCGIServer to start servicing requests. + # This loops forever. +server.serve() diff --git a/config.toml b/config.toml index d8f873e..0cf8ff6 100644 --- a/config.toml +++ b/config.toml @@ -18,6 +18,8 @@ cgi = true # cgipath is optional and only checked if cgi is true. It restricts cgi to only # this directory. cgi = "/path/to/cgi-bin/" +# scgi is optional +scgi = { "/scgi" = "localhost:4000" } # cgienv is optional cgienv = { "GIT_PROJECT_ROOT" = "/srv/git" } # usrdir is optional. it'll look in /home/usr/public_gemini diff --git a/src/cgi.rs b/src/cgi.rs index c7a5939..b705328 100644 --- a/src/cgi.rs +++ b/src/cgi.rs @@ -3,43 +3,68 @@ use std::io; use std::path::PathBuf; use tokio::process::Command; use url::Url; +use std::net::{SocketAddr, ToSocketAddrs}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; use crate::config; use crate::conn; use crate::logger; use crate::status::Status; +fn envs(peer_addr: SocketAddr, srv: &config::ServerCfg, url: &url::Url) -> HashMap { + let mut envs = HashMap::new(); + envs.insert("GATEWAY_INTERFACE".to_string(), "CGI/1.1".to_string()); + envs.insert("GEMINI_URL".to_string(), url.to_string()); + envs.insert("SERVER_NAME".to_string(), url.host_str().unwrap().to_string()); + envs.insert("SERVER_PROTOCOL".to_string(), "GEMINI".to_string()); + let addr = peer_addr.ip().to_string(); + envs.insert("REMOTE_ADDR".to_string(), addr.clone()); + envs.insert("REMOTE_HOST".to_string(), addr); + let port = peer_addr.port().to_string(); + envs.insert("REMOTE_PORT".to_string(), port); + envs.insert("SERVER_SOFTWARE".to_string(), env!("CARGO_PKG_NAME").to_string()); + + if let Some(q) = url.query() { + envs.insert("QUERY_STRING".to_string(), q.to_string()); + } + + match &srv.server.cgienv { + Some(c) => { + for (k, v) in c.iter() { + envs.insert(k.clone(), v.clone()); + } + } + None => {} + } + envs +} + +fn check(byt: u8, peer_addr: SocketAddr, u: url::Url) -> bool { + match byt { + 49 => { + logger::logger(peer_addr, Status::Input, u.as_str()); + }, + 50 => { + logger::logger(peer_addr, Status::Success, u.as_str()); + }, + 51..=54 => {} + _ => { + logger::logger(peer_addr, Status::CGIError, u.as_str()); + return false; + }, + } + true +} + pub async fn cgi( mut con: conn::Connection, srv: &config::ServerCfg, path: PathBuf, url: Url, ) -> Result<(), io::Error> { - let mut envs = HashMap::new(); - envs.insert("GATEWAY_INTERFACE", "CGI/1.1"); - envs.insert("GEMINI_URL", url.as_str()); - envs.insert("SERVER_NAME", url.host_str().unwrap()); - envs.insert("SCRIPT_NAME", path.file_name().unwrap().to_str().unwrap()); - envs.insert("SERVER_PROTOCOL", "GEMINI"); - let addr = con.peer_addr.ip().to_string(); - envs.insert("REMOTE_ADDR", &addr); - envs.insert("REMOTE_HOST", &addr); - let port = con.peer_addr.port().to_string(); - envs.insert("REMOTE_PORT", &port); - envs.insert("SERVER_SOFTWARE", env!("CARGO_PKG_NAME")); - - if let Some(q) = url.query() { - envs.insert("QUERY_STRING", q); - } - - match &srv.server.cgienv { - Some(c) => { - for (k, v) in c.iter() { - envs.insert(&k, &v); - } - } - None => {} - } + let mut envs = envs(con.peer_addr, srv, &url); + envs.insert("SCRIPT_NAME".to_string(), path.file_name().unwrap().to_str().unwrap().to_string()); let cmd = Command::new(path.to_str().unwrap()) .env_clear() @@ -71,7 +96,49 @@ pub async fn cgi( return Ok(()); } let cmd = String::from_utf8(cmd.stdout).unwrap(); - logger::logger(con.peer_addr, Status::Success, url.as_str()); + if !check(cmd.as_bytes()[0], con.peer_addr, url) { + con.send_status(Status::CGIError, None).await?; + return Ok(()); + } + con.send_raw(cmd.as_bytes()).await?; return Ok(()); } + +pub async fn scgi(addr: String, u: url::Url, mut con: conn::Connection, srv: &config::ServerCfg) -> Result<(), io::Error> { + let addr = addr + .to_socket_addrs()? + .next() + .ok_or_else(|| io::Error::from(io::ErrorKind::AddrNotAvailable))?; + + let mut stream = match TcpStream::connect(&addr).await { + Ok(s) => s, + Err(_) => { + logger::logger(con.peer_addr, Status::CGIError, u.as_str()); + con.send_status(Status::CGIError, None).await?; + return Ok(()); + } + }; + let envs = envs(con.peer_addr, srv, &u); + let len = 0usize; + let mut byt = String::from(format!("CONTENT_LENGTH\x00{}\x00SCGI\x001\x00 + RQUEST_METHOD\x00POST\x00REQUEST_URI\x00{}\x00", len, u.path())); + for (k, v) in envs.iter() { + byt.push_str(&format!("{}\x00{}\x00", k, v)); + } + byt = byt.len().to_string() + ":" + &byt + ","; + + stream.write_all(byt.as_bytes()).await?; + stream.flush().await?; + + let mut buf = vec![]; + stream.read_to_end(&mut buf).await?; + let req = String::from_utf8_lossy(&buf[..]); + if !check(req.as_bytes()[0], con.peer_addr, u) { + con.send_status(Status::CGIError, None).await?; + return Ok(()); + } + + con.send_raw(req.as_bytes()).await?; + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index 74c2546..eed0893 100644 --- a/src/config.rs +++ b/src/config.rs @@ -24,6 +24,7 @@ pub struct Server { pub usrdir: Option, pub proxy: Option>, pub redirect: Option>, + pub scgi: Option>, } #[derive(Debug, Clone)] diff --git a/src/main.rs b/src/main.rs index ce7664c..b345b0a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -180,6 +180,21 @@ async fn handle_connection( None => {} } + match &srv.server.scgi { + Some(sc) => { + let u = url.path().trim_end_matches("/"); + match sc.get(u) { + Some(r) => { + cgi::scgi(r.to_string(), url, con, srv).await?; + return Ok(()); + } + None => {} + } + }, + None => {}, + } + + let mut path = PathBuf::new(); if url.path().starts_with("/~") && srv.server.usrdir.unwrap_or(false) {