From 4e122bd245c0b27750d33870f05b8f23b5c9ce10 Mon Sep 17 00:00:00 2001 From: DanyLE Date: Mon, 16 Jan 2023 01:03:00 +0100 Subject: [PATCH] POC of working lua fastcgi --- .gitignore | 1 + Cargo.lock | 324 ++++++++++++++++++ Cargo.toml | 24 ++ config-example.toml | 10 + src/lib.rs | 785 ++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 167 ++++++++++ 6 files changed, 1311 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 config-example.toml create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f795ca7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,324 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "fastcgi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4159a0f48bea0281602e508eb070d7d7ba1f6ac2480f9db1a60a39274aea1cc" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "lua-src" +version = "544.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708ba3c844d5e9d38def4a09dd871c17c370f519b3c4b7261fbabe4a613a814c" +dependencies = [ + "cc", +] + +[[package]] +name = "luad" +version = "0.1.0" +dependencies = [ + "clap", + "fastcgi", + "libc", + "mlua", + "nix", + "serde", + "serde_derive", + "toml", +] + +[[package]] +name = "luajit-src" +version = "210.4.5+resty2cf5186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b7992a40e602786272d84c6f2beca44a588ededcfd57b48ec6f82008a7cb97" +dependencies = [ + "cc", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mlua" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee2ad7a9aa69056b148d9d590344bc155d3ce0d2200e3b2838f7034f6ba33c1" +dependencies = [ + "bstr", + "cc", + "lua-src", + "luajit-src", + "num-traits", + "once_cell", + "pkg-config", + "rustc-hash", +] + +[[package]] +name = "nix" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset", + "pin-utils", + "static_assertions", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "proc-macro2" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "toml" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a6184b9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "luad" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +fastcgi = "1.0.0" +mlua = { version = "0.8", features = ["lua54", "vendored"] } +clap = "2.33" +nix = "0.26.1" +serde = {version = "1.0", features = ["derive"]} +serde_derive = "1.0" +toml = "0.5" +libc = "0.2" + +[profile.release] +opt-level = 's' +# 's' for size +lto = true +# this strategy doesnot work on 1.3.9 +# panic = 'abort' +codegen-units = 1 \ No newline at end of file diff --git a/config-example.toml b/config-example.toml new file mode 100644 index 0000000..675025a --- /dev/null +++ b/config-example.toml @@ -0,0 +1,10 @@ +# TCP socket or Unix socket file +socket = "unix:/tmp/lua.sock" + +# pid file +pidfile = "/tmp/luad.pid" + +# user name +# user = "demo" +# group name +# group = "demo" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..1d99fe7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,785 @@ +use mlua::prelude::*; +use libc; +use nix; +use std::io::{Error, ErrorKind, Read,Write}; +use std::ffi::CString; +use std::fmt::Arguments; +use std::os::fd::RawFd; +use std::os::unix::io::{AsRawFd}; +use std::collections::HashMap; + +/// app author +pub const APP_AUTHOR: &str = "Dany LE "; + +/// app version +pub const APP_VERSION: &str = "0.1.0"; + +/// Application name +pub const DAEMON_NAME: &str = "luad"; + +/// Drop user privileges +/// +/// This function drop the privileges of the current user +/// to another inferior privileges user. +/// e.g. drop from root->maint +/// +/// # Arguments +/// +/// * `user` - system user name +/// * `group` - system group name +/// +/// # Errors +/// +/// * `nix::Error` - The error from the nix package +pub fn privdrop(useropt: Option<&str>, groupopt: Option<&str>) -> Result<(), nix::Error> { + match groupopt{ + Some(group) => { + INFO!("Dropping current process group to {}", group); + match nix::unistd::Group::from_name(group)? { + Some(group) => nix::unistd::setgid(group.gid), + None => Err(nix::Error::last()), + }?; + }, + None => {} + } + match useropt { + Some(user) => { + INFO!("Dropping current process user to {}", user); + match nix::unistd::User::from_name(user)? { + Some(user) => nix::unistd::setuid(user.uid), + None => Err(nix::Error::last()), + }? + }, + None => {} + } + Ok(()) +} + + +/// Utility function to catch common signal that +/// cause the program to exit +/// +/// Signals catched: SIGABRT, SIGINT, SIGTERM, SIGQUIT +/// +/// # Arguments +/// +/// * `f` - callback function that will be called when a signal is trapped +pub fn on_exit(f: fn(n: i32) -> ()) { + unsafe { + let _ = libc::signal(libc::SIGPIPE, libc::SIG_IGN); + let _ = libc::signal(libc::SIGABRT, (f as *const std::ffi::c_void) as usize); + let _ = libc::signal(libc::SIGINT, (f as *const std::ffi::c_void) as usize); + let _ = libc::signal(libc::SIGTERM, (f as *const std::ffi::c_void) as usize); + let _ = libc::signal(libc::SIGQUIT, (f as *const std::ffi::c_void) as usize); + }; +} + +/// Return an Error Result object from error string +/// +#[macro_export] +macro_rules! ERR { + ($x:expr) => { + Error::new( + ErrorKind::Other, + format!("({}:{}): {}", file!(), line!(), $x), + ) + }; +} + +/// Macro for error log helper +/// +#[macro_export] +macro_rules! INFO { + ($($args:tt)*) => ({ + let prefix = format!(":info@[{}:{}]: ",file!(), line!()); + let _ = LOG::log(&prefix[..], &LogLevel::INFO, format_args!($($args)*)); + }) +} + +/// Macro for warning log helper +/// +#[macro_export] +macro_rules! WARN { + ($($args:tt)*) => ({ + let prefix = format!(":warning@[{}:{}]: ",file!(), line!()); + let _ = LOG::log(&prefix[..], &LogLevel::WARN, format_args!($($args)*)); + }) +} + +/// Macro for info log helper +/// +#[macro_export] +macro_rules! ERROR { + ($($args:tt)*) => ({ + let prefix = format!(":error@[{}:{}]: ",file!(), line!()); + let _ = LOG::log(&prefix[..], &LogLevel::ERROR, format_args!($($args)*)); + }) +} + +/// Different Logging levels for `LOG` +pub enum LogLevel { + /// Error conditions + ERROR, + /// Normal, but significant, condition + INFO, + /// Warning conditions + WARN, +} + +/// Log struct wrapper +/// +pub struct LOG {} + +impl LOG { + /// Init the system log + /// + /// This should be called only once in the entire lifetime + /// of the program, the returned LOG instance should + /// be keep alive during the lifetime of the program (the main function). + /// When it is dropped, the connection to the system log will be + /// closed automatically + #[must_use] + pub fn init_log() -> Self { + // connect to the system log + unsafe { + libc::openlog( + std::ptr::null(), + libc::LOG_CONS | libc::LOG_PID | libc::LOG_NDELAY, + libc::LOG_DAEMON, + ); + } + Self {} + } + + /// Wrapper function that log error or info message to the + /// connected syslog server + /// + /// # Arguments + /// + /// * `prefix` - Prefix of the log message + /// * `level` - Log level + /// * `args` - Arguments object representing a format string and its arguments + /// + /// # Errors + /// + /// * `std error` - All errors related to formated and C string manipulation + pub fn log(prefix: &str, level: &LogLevel, args: Arguments<'_>) -> Result<(), Error> { + use std::fmt::Write; + let mut output = String::new(); + if output.write_fmt(args).is_err() { + return Err(ERR!("Unable to create format string from arguments")); + } + let log_fmt = format!("{}(v{}){}%s\n", DAEMON_NAME, APP_VERSION, prefix); + let fmt = CString::new(log_fmt.as_bytes())?; + let c_msg = CString::new(output.as_bytes())?; + let sysloglevel = match level { + LogLevel::ERROR => libc::LOG_ERR, + LogLevel::WARN => libc::LOG_WARNING, + _ => libc::LOG_NOTICE, + }; + unsafe { + libc::syslog(sysloglevel, fmt.as_ptr(), c_msg.as_ptr()); + } + Ok(()) + } +} + +impl Drop for LOG { + /// The connection to the syslog will be closed + /// automatically when the log object is drop + fn drop(&mut self) { + // Close the current connection to the system logger + unsafe { + libc::closelog(); + } + } +} + + +/// Protocol goes here +#[derive(Debug)] +enum FCGIHeaderType{ + BeginRequest, + AbortRequest, + EndRequest, + Params, + Stdin, + Stdout, + Stderr, + Data, + GetValues, + GetValuesResult, + Unknown, +} + +impl FCGIHeaderType { + + /// convert a u8 value to `FCGIHeaderType` value + /// + /// # Arguments + /// + /// * `value` - u8 header value + fn from_u8(value: u8) -> Self { + match value { + 1 => FCGIHeaderType::BeginRequest, + 2 => FCGIHeaderType::AbortRequest, + 3 => FCGIHeaderType::EndRequest, + 4 => FCGIHeaderType::Params, + 5 => FCGIHeaderType::Stdin, + 6 => FCGIHeaderType::Stdout, + 7 => FCGIHeaderType::Stderr, + 8 => FCGIHeaderType::Data, + 9 => FCGIHeaderType::GetValues, + 10 => FCGIHeaderType::GetValuesResult, + _ => FCGIHeaderType::Unknown, + } + } + /// convert an `FCGIHeaderType` value to u8 + /// + /// # Arguments + /// + /// * `value` - `FCGIHeaderType` header value + fn as_u8(&self) -> u8 { + match self { + FCGIHeaderType::BeginRequest => 1, + FCGIHeaderType::AbortRequest => 2, + FCGIHeaderType::EndRequest => 3, + FCGIHeaderType::Params => 4, + FCGIHeaderType::Stdin => 5, + FCGIHeaderType::Stdout => 6, + FCGIHeaderType::Stderr => 7, + FCGIHeaderType::Data => 8, + FCGIHeaderType::GetValues => 9, + FCGIHeaderType::GetValuesResult => 10, + FCGIHeaderType::Unknown => 11, + } + } +} + +impl std::fmt::Display for FCGIHeaderType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let s = match self { + FCGIHeaderType::BeginRequest => "FCGI_BEGIN_REQUEST", + FCGIHeaderType::AbortRequest => "FCGI_ABORT_REQUEST", + FCGIHeaderType::EndRequest => "FCGI_END_REQUEST", + FCGIHeaderType::Params => "FCGI_PARAMS", + FCGIHeaderType::Stdin => "FCGI_STDIN", + FCGIHeaderType::Stdout => "FCGI_STDOUT", + FCGIHeaderType::Stderr => "FCGI_STDERR", + FCGIHeaderType::Data => "FCGI_DATA", + FCGIHeaderType::GetValues => "FCGI_GET_VALUES", + FCGIHeaderType::GetValuesResult => "FCGI_GET_VALUES_RESULT", + FCGIHeaderType::Unknown => "FCGI_UNKNOWN_TYPE", + }; + write!(f, "{}", s) + } +} + +enum EndRequestStatus{ + Complete, + // CantMaxMPXConn, + // Overloaded, + UnknownRole, +} + +impl EndRequestStatus { + /// convert an `EndRequestStatus` value to u8 + /// + /// # Arguments + /// + /// * `value` - `EndRequestStatus` header value + fn as_u8(&self) -> u8 { + match self { + EndRequestStatus::Complete => 0, + //EndRequestStatus::CantMaxMPXConn => 1, + //EndRequestStatus::Overloaded => 2, + EndRequestStatus::UnknownRole => 3, + } + } +} + +const FCGI_HEADER_LEN:usize = 8; +const FCGI_VERSION:u8 = 1; + +#[derive(Debug)] +#[derive(PartialEq)] +enum FCGIRole { + Responder, + Authorizer, + Filter, + Unknown +} + +impl FCGIRole { + + /// convert a u8 value to `FCGIRole` value + /// + /// # Arguments + /// + /// * `value` - u16 header value + fn from_u16(value: u16) -> Self { + match value { + 1 => FCGIRole::Responder, + 2 => FCGIRole::Authorizer, + 3 => FCGIRole::Filter, + _ => FCGIRole::Unknown, + } + } +} + +impl std::fmt::Display for FCGIRole { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let s = match self { + FCGIRole::Responder => "FCGI_RESPONDER", + FCGIRole::Authorizer => "FCGI_AUTHORIZER", + FCGIRole::Filter => "FCGI_FILTER", + FCGIRole::Unknown => "FCGI_UNKNOWN_ROLE", + }; + write!(f, "{}", s) + } +} +#[derive(Debug)] +struct FCGIBeginRequestBody{ + role: FCGIRole, + flags: u8, +} + +impl FCGIBeginRequestBody { + pub fn from_bytes(data: &[u8]) -> Self + { + Self { + role: FCGIRole::from_u16(((data[0] as u16) << 8) | (data[1] as u16)), + flags: data[2], + } + } +} + +impl std::fmt::Display for FCGIBeginRequestBody { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "role: {} \n", self.role)?; + write!(f, "flags: {} \n", self.flags) + } +} + + +#[derive(Debug)] +struct FcgiHeader { + version: u8, + kind: FCGIHeaderType, + id: u16, + padding: u8, + length:u16, +} + +impl FcgiHeader { + pub fn from_bytes(data: &[u8]) -> Self + { + Self { + version: data[0], + kind: FCGIHeaderType::from_u8(data[1]), + id: ((data[2] as u16) << 8) | (data[3] as u16), + length: ((data[4] as u16) << 8) | (data[5]as u16), + padding: data[6] + } + } + + pub fn as_bytes(&self) -> Vec + { + vec![ + self.version, + self.kind.as_u8(), + (self.id >> 8) as u8, + (self.id & 0xFF) as u8, + (self.length >> 8) as u8, + (self.length & 0xFF) as u8, + self.padding, + 0 + ] + } +} + +impl std::fmt::Display for FcgiHeader { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Version: {} \n", self.version)?; + write!(f, "Kind: {} \n", self.kind)?; + write!(f, "ID: {} \n", self.id)?; + write!(f, "Data length: {} \n", self.length)?; + write!(f, "Padding: {} \n", self.padding) + } +} + +#[derive(Debug)] +#[derive(PartialEq)] +enum FCGIRequestState { + WaitForParams, + WaitForStdin, + WaitForStdout +} + +impl std::fmt::Display for FCGIRequestState { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let s = match self { + FCGIRequestState::WaitForParams => "WaitForParams", + FCGIRequestState::WaitForStdin => "WaitForStdin", + FCGIRequestState::WaitForStdout => "WaitForStdout", + }; + write!(f, "{}", s) + } +} + +struct FGCIRequest { + params: HashMap, + id: u16, + fd: RawFd, + data: Vec, + state: FCGIRequestState, +} + +struct FCGIStdoutStream { + fd: RawFd, + id: u16 +} + +impl FCGIStdoutStream { + fn _write(&self, buf: &[u8]) -> std::io::Result + { + let ret = unsafe { libc::write(self.fd, buf.as_ptr() as *const libc::c_void, buf.len()) }; + if ret != buf.len() as isize + { + let msg = format!("Unable to write data to {}: only {} out of {} bytes have been written", self.fd, ret, buf.len()); + return Err(ERR!(msg)); + } + Ok(ret as usize) + } +} + +impl Write for FCGIStdoutStream +{ + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self._write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + todo!() + } +} + +impl mlua::UserData for FCGIStdoutStream { + fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method("write", |_, this:& FCGIStdoutStream, strings:mlua::Variadic| { + let mut stream = FCGIStdoutStream + { + fd: this.fd, + id: this.id + }; + let mut output = String::new(); + for string in strings{ + output.push_str(&string); + } + fcgi_send_stdout(&mut stream, this.id, Some(output.as_bytes().to_vec())) + .map_err(|e| mlua::Error::external(ERR!(e.to_string())))?; + Ok(()) + }); + } +} + +fn lua_define_print(lua: &Lua, strings: mlua::Variadic) -> LuaResult<()> { + let global = lua.globals(); + let server = global.get::<_, mlua::Table>("_SERVER")?; + let fd = server.get::<_,RawFd>("FD")?; + let id: u16 = server.get::<_,u16>("ID")?; + if fd <= 0 { + return Err(mlua::Error::external(ERR!("Invalid file descriptor"))); + } + let mut data = String::new(); + for string in strings{ + data.push_str(&string); + } + // make the request + let mut stream = FCGIStdoutStream { fd, id}; + let body = data.as_bytes().to_vec(); + fcgi_send_stdout(&mut stream, id, Some(body)) + .map_err(|e| mlua::Error::external(ERR!(e.to_string())))?; + Ok(()) +} + +//fn lua_define_io_write(lua: &Lua, strings: String) -> LuaResult<()> { +//} + +fn fcgi_execute_request_handle(rq: & FGCIRequest) -> Result<(), Box> +{ + let lua = mlua::Lua::new(); + let global = lua.globals(); + let request = lua.create_table()?; + request.set("ID", rq.id)?; + request.set("FD", rq.fd)?; + for (k,v) in &rq.params{ + request.set(String::from(k),String::from(v))?; + } + // request params stored in _SERVER table + global.set("_SERVER", request)?; + // replace the print function + let func = lua.create_function(lua_define_print)?; + global.set("print", func)?; + + // replace the io.stdout + let stdout = FCGIStdoutStream { + fd: rq.fd, + id: rq.id + }; + let io = global.get::<_, mlua::Table>("io")?; + io.set("stdout", stdout)?; + let func = lua.create_function(lua_define_print)?; + io.set("write",func)?; + + let path = rq.params.get("SCRIPT_FILENAME").ok_or(ERR!("No SCRIPT_FILENAME found"))?; + let source = std::fs::read_to_string(path)?; + INFO!("source: {}", &source); + lua.load(&source).exec()?; + Ok(()) + + //global.set("hello", func)?; + //let source = std::fs::read_to_string(script).unwrap(); + //lua.load(&source).exec().unwrap(); + //lua.load("hello('world')").exec()?; +} + +fn fcgi_send_stderr(stream: &mut T,id: u16, eopt: Option>) -> Result<(), Box> +{ + let mut header = FcgiHeader { + version: FCGI_VERSION, + kind: FCGIHeaderType::Stderr, + id: id, + length: 0, + padding: 0 , + }; + if let Some(error) = eopt{ + let err_str = error.to_string(); + let str_len = err_str.len(); + let mut padding = (8 - str_len % 8) as u8; + if padding == 8 + { + padding = 0; + } + let mut body = err_str.as_bytes().to_vec(); + let pad = vec![0;padding as usize]; + header.length = str_len as u16; + header.padding = padding; + body.extend(pad); + stream.write_all(&header.as_bytes())?; + stream.write_all(&body)?; + } + else + { + stream.write_all(&header.as_bytes())?; + } + Ok(()) +} + +fn fcgi_send_stdout(stream: &mut T,id: u16, dopt: Option>) -> Result<(), Box> +{ + let mut header = FcgiHeader { + version: FCGI_VERSION, + kind: FCGIHeaderType::Stdout, + id: id, + length: 0, + padding: 0, + }; + if let Some(data) = dopt{ + header.length = data.len() as u16; + header.padding = (8 - header.length % 8) as u8; + if header.padding == 8 + { + header.padding = 0; + } + let mut body = data; + let pad = vec![0;header.padding as usize]; + body.extend(pad); + stream.write_all(&header.as_bytes())?; + stream.write_all(&body)?; + } + else + { + stream.write_all(&header.as_bytes())?; + } + Ok(()) +} + +fn fcgi_send_end_request(stream: &mut T,id:u16, status: EndRequestStatus) -> Result<(), Box> +{ + let header = FcgiHeader { + version: FCGI_VERSION, + kind: FCGIHeaderType::EndRequest, + id: id, + length: 8, + padding: 0 , + }; + let body = vec![ + 0,0,0,0, + status.as_u8(), + 0,0,0 + ]; + stream.write_all(&header.as_bytes())?; + stream.write_all(&body)?; + Ok(()) +} + +pub fn process_request(stream:&mut T)-> Result<(), Box>{ + let mut requests: HashMap = HashMap::new(); + loop{ + let header = fcgi_read_header(stream)?; + match header.kind { + FCGIHeaderType::BeginRequest => { + let body = FCGIBeginRequestBody::from_bytes(&fcgi_read_body(stream, &header)?); + INFO!("Begin Request: {:?}, with body {:?}", header, body); + if body.role != FCGIRole::Responder + { + fcgi_send_end_request(stream, header.id, EndRequestStatus::UnknownRole)?; + return Err(Box::new(ERR!("Only Responder role is supported"))); + } + // check if we have already request of this kind + if let Some(_) = requests.get(&header.id) + { + WARN!("Request {} already exists, ignore this message", header.id); + } + else + { + let rq:FGCIRequest = FGCIRequest { + id: header.id, + params: HashMap::new(), + data: vec![0;0], + state: FCGIRequestState::WaitForParams, + fd: stream.as_raw_fd(), + }; + requests.insert(header.id, rq); + } + }, + FCGIHeaderType::Params => { + if let Some(rq) = requests.get_mut(&header.id) + { + if rq.state != FCGIRequestState::WaitForParams + { + WARN!("Should not receive a param record as the request is in {} state", rq.state); + } + else + { + if header.length == 0 + { + INFO!("All param records read, now wait for stdin data on request: {}", header.id); + rq.state = FCGIRequestState::WaitForStdin; + } + else + { + fcgi_decode_params(rq,&fcgi_read_body(stream, &header)?)?; + } + } + } + else + { + WARN!("Uknow request {}, ignore param record", header.id); + } + }, + FCGIHeaderType::Stdin => { + if let Some(rq) = requests.get_mut(&header.id) + { + if rq.state != FCGIRequestState::WaitForStdin + { + WARN!("Should not receive a stdin record as the request is in {} state", rq.state); + } + else + { + if header.length == 0 + { + INFO!("All stdin records read, now wait for stdout data on request: {}", header.id); + rq.state = FCGIRequestState::WaitForStdout; + if let Err(error) = fcgi_execute_request_handle(rq) + { + // send stderror + fcgi_send_stderr(stream, header.id, Some(error))?; + } + fcgi_send_stderr(stream, header.id, None)?; + fcgi_send_stdout(stream, header.id, None)?; + // send end connection + fcgi_send_end_request(stream, header.id, EndRequestStatus::Complete)?; + break; + } + else + { + let body = fcgi_read_body(stream, &header)?; + rq.data.extend(body); + } + } + } + else + { + WARN!("Uknow request {}, ignore stdin record", header.id); + } + } + _ => { + WARN!("Unsupported record type: {} on request {}", header.kind, header.id); + } + } + } + Ok(()) +} + +fn fcgi_read_header(stream: &mut T) -> Result +{ + let mut buf = vec![0;FCGI_HEADER_LEN]; + stream.read_exact(&mut buf)?; + let header: FcgiHeader = FcgiHeader::from_bytes(&buf); + Ok(header) +} + +fn fcgi_read_body(stream: &mut T, header: & FcgiHeader) -> Result, Error> +{ + let mut buf = vec![0; header.length as usize]; + stream.read_exact(&mut buf)?; + let mut pad: Vec = vec![0; header.padding as usize]; + stream.read_exact(&mut pad)?; + + Ok(buf.to_vec()) +} + +fn fcgi_decode_strlen(data: &[u8]) -> usize +{ + let b0 = data[0]; + if b0 >> 7 == 0 + { + b0 as usize + } + else + { + return (((data[0] as usize) & 0x7f) << 24) + ((data[1] as usize) << 16) + ((data[2] as usize) << 8) + (data[3] as usize) + } +} + +fn fcgi_decode_params(rq: &mut FGCIRequest, data:& Vec) -> Result<(), Box> +{ + let mut index: usize = 1; + let key_len = fcgi_decode_strlen(data); + if key_len > 127 + { + index = 4; + } + let value_len = fcgi_decode_strlen(&data[index..]); + //INFO!("Key len {}, value len {}", key_len, value_len); + if value_len > 127 + { + index += 4; + } + else + { + index += 1; + } + //INFO!("data: {:?}", data); + //INFO!("key: {:?}", data[index..index + key_len].to_vec()); + //INFO!("Value: {:?}", data[index+key_len..index+key_len+value_len].to_vec()); + let key = String::from_utf8(data[index..index+key_len].to_vec())?; + let value: String = String::from_utf8(data[index+key_len..index+key_len+value_len].to_vec())?; + INFO!("PARAM: [{}] -> [{}]", key, value); + let _ = rq.params.insert(key, value); + Ok(()) + + +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a3e9500 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,167 @@ +//! Lua FastCGI main application +//! +//! **Author**: "Dany LE " +//! +//! +#![warn( + trivial_casts, + trivial_numeric_casts, + unused_extern_crates, + unused_qualifications, + unused_results, + missing_docs, + clippy::pedantic, + clippy::missing_docs_in_private_items +)] +use serde; +use toml; +use clap; +//use std::fs::File; +use std::io::Write; +use std::net::TcpListener; +use std::os::unix::io::AsRawFd; +use std::os::unix::net::UnixListener; +use std::panic; +use std::path::Path; +use std::os::fd::FromRawFd; +use std::thread; +use std::io::Read; +use luad::*; + +/// Callback: clean up function +/// +/// This function remove the unix socket file if +/// exist before quiting the program +/// +/// # Arguments +/// +/// * `n` - system exit code +fn clean_up(n: i32) { + if let Ok(socket_name) = std::env::var("socket") { + let file = socket_name.replace("unix:", ""); + let path = Path::new(&file); + if path.exists() { + std::fs::remove_file(path).unwrap(); + } + } + if n != 0 { + panic!("{}", format!("The LUA fastCGI daemon is terminated by system signal: {}", n)); + } +} + + + +fn handle_request(stream: &mut T) { + if let Err(error) = process_request(stream) + { + ERROR!("Unable to process request: {}", error); + } + INFO!("Request on socket {} is processed", stream.as_raw_fd()); +} + +/// Start the `fastCGI` server +/// +/// # Arguments +/// +/// * `socket_opt` - The socket string that the server listens on +fn serve(socket_opt: Option<&str>) { + + + // bind to a socket if any + if let Some(socket_name) = socket_opt { + // test if the socket name is an unix domain socket + if socket_name.starts_with("unix:") { + // e.g unix:/var/run/lighttpd/maint/efcgi.socket + INFO!("Use unix domain socket: {}", socket_name); + std::env::set_var("socket", socket_name); + let listener = UnixListener::bind(socket_name.replace("unix:", "")).unwrap(); + on_exit(clean_up); + for client in listener.incoming() { + let mut stream = client.unwrap(); + let _= std::thread::spawn(move || { + handle_request(&mut stream); + }); + } + } else { + // TCP socket eg. 127.0.0.1:9000 + INFO!("Use TCP socket: {}", socket_name); + let listener = TcpListener::bind(socket_name).unwrap(); + for client in listener.incoming() { + let mut stream = client.unwrap(); + let _= thread::spawn(move || { + handle_request(&mut stream); + }); + } + } + } else { + // if there is no socket configuration, assume that the stdio is already mapped + // to a socket. This is usually done by by the parent process (e.g. webserver) that launches efcgi + INFO!("No socket specified! use stdin as listenning socket"); + let stdin = std::io::stdin(); + let listener = unsafe{ UnixListener::from_raw_fd(stdin.as_raw_fd())}; + for client in listener.incoming() { + let mut stream = client.unwrap(); + + let _= thread::spawn(move || { + handle_request(&mut stream); + }); + } + } +} + +#[derive(serde::Deserialize, Debug)] +struct Config { + socket: Option, + pidfile: Option, + user: Option, + group: Option, +} + + + +/// Main application entry +/// +/// Run a `fastCGI` server +fn main() { + let _log = LOG::init_log(); + + let matches = clap::App::new(DAEMON_NAME) + .author(APP_AUTHOR) + .about("Lua general purpose socket handle daemon") + .version(APP_VERSION) + .arg( + clap::Arg::with_name("file") + .short("f") + .long("file") + .value_name("FILE") + .help("Configuration file") + .required(false) + .takes_value(true), + ) + .get_matches(); + + match matches.value_of("file") { + Some(path) => { + INFO!("Configuration file: {}", path); + let contents = std::fs::read_to_string(path).unwrap(); + let config: Config = toml::from_str(&contents).unwrap(); + + // write pid file + match config.pidfile { + Some(pidfile) => { + let mut f = std::fs::File::create(&pidfile).unwrap(); + write!(f, "{}", std::process::id()).unwrap(); + INFO!("PID file created at {}", pidfile); + }, + None => {} + } + // drop user privilege if only user and group available in + // the configuration file, otherwise ignore + privdrop(config.user.as_deref(), config.group.as_deref()).unwrap(); + serve(config.socket.as_deref()); + }, + None => { + serve(None); + } + } +}