Skip to content

Commit

Permalink
Merge pull request #9 from w-henderson/config-include
Browse files Browse the repository at this point in the history
Added an `include` directive to the configuration format
  • Loading branch information
w-henderson authored Oct 10, 2021
2 parents 8f05aec + cc02374 commit 1bfd55c
Show file tree
Hide file tree
Showing 16 changed files with 264 additions and 56 deletions.
2 changes: 1 addition & 1 deletion humphrey-server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "humphrey_server"
version = "0.2.1"
version = "0.2.2"
edition = "2018"
license = "MIT"
homepage = "https://github.com/w-henderson/Humphrey"
Expand Down
20 changes: 11 additions & 9 deletions humphrey-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Humphrey is a very fast, robust and flexible HTTP/1.1 web server, with support f
To install the binary, run `cargo install humphrey_server` and it will be automatically downloaded, compiled and added to your path as `humphrey`. Alternatively, you can find precompiled binaries from the [latest GitHub release](https://github.com/w-henderson/Humphrey/releases).

## Configuration
The Humphrey executable is run with a maximum of one argument, which specifies the path to the configuration file (defaulting to `humphrey.conf` in the current directory). The configuration file is where all configuration for Humphrey and any plugins is stored. The syntax is similar to Nginx, with comments starting with a `#`. Below is an example of a configuration file with every supported field specified. Unless specified otherwise, all fields are optional.
The Humphrey executable is run with a maximum of one argument, which specifies the path to the configuration file (defaulting to `humphrey.conf` in the current directory). The configuration file is where all configuration for Humphrey and any plugins is stored. The syntax is similar to Nginx, with comments starting with a `#`. Other configuration files can be included with the `include` directive, like in Nginx. Below is an example of a configuration file with every supported field specified. Unless specified otherwise, all fields are optional.

```conf
server {
Expand All @@ -21,6 +21,10 @@ server {
threads 32 # Number of threads to use for the server
websocket "localhost:1234" # Where to proxy WebSocket connections to
plugins {
include "php.conf" # Include PHP configuration (see below)
}
blacklist {
file "conf/blacklist.txt" # Text file containing blacklisted addresses, one per line
mode "block" # Method of enforcing the blacklist, "block" or "forbidden" (which returns 403 Forbidden)
Expand Down Expand Up @@ -51,15 +55,13 @@ server {
## Using with PHP
To use Humphrey with PHP, compile the [PHP plugin in the plugins folder](https://github.com/w-henderson/Humphrey/tree/master/plugins/php) and add the path to the output file to your plugin configuration (also available precompiled in the GitHub releases). You'll need Humphrey installed with plugins enabled (using `cargo install humphrey_server --features plugins`) and you'll also need PHP-CGI or PHP-FPM. Start the PHP server first, and specify its address in the Humphrey configuration file as specified below. Ensure your PHP configuration allows for multithreading if you set more than one thread in the configuration. Finally, you can start Humphrey in the normal way and it will work with PHP.

Add the following in the server section of your configuration file:
In the `php.conf` file which is included in the main configuration through the `include` directive:
```conf
plugins {
php {
library "plugins/php/target/release/php.dll" # Path to compiled library, `.dll` on Windows and `.so` on Linux
address "127.0.0.1" # Address to connect to the PHP-CGI interpreter
port 9000 # Port of the interpreter
threads 8 # Number of threads to connect to the interpreter with
}
php {
library "plugins/php/target/release/php.dll" # Path to compiled library, `.dll` on Windows and `.so` on Linux
address "127.0.0.1" # Address to connect to the PHP-CGI interpreter
port 9000 # Port of the interpreter
threads 8 # Number of threads to connect to the interpreter with
}
```

Expand Down
10 changes: 5 additions & 5 deletions humphrey-server/src/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ pub enum BlacklistMode {
impl Config {
/// Attempts to load the configuration.
pub fn load() -> Result<Self, String> {
if let Ok(config_string) = load_config_file() {
let tree = parse_conf(&config_string).map_err(|e| e.to_string())?;
if let Ok((filename, config_string)) = load_config_file() {
let tree = parse_conf(&config_string, &filename).map_err(|e| e.to_string())?;
let config = Self::from_tree(tree)?;

Ok(config)
Expand Down Expand Up @@ -280,17 +280,17 @@ impl Config {
}

/// Loads the configuration file.
fn load_config_file() -> Result<String, ()> {
fn load_config_file() -> Result<(String, String), ()> {
let path = args().nth(1).unwrap_or_else(|| "humphrey.conf".into());

if let Ok(mut file) = File::open(path) {
if let Ok(mut file) = File::open(&path) {
// The file can be opened

let mut string = String::new();
if file.read_to_string(&mut string).is_ok() {
// The file can be read

Ok(string)
Ok((path, string))
} else {
Err(())
}
Expand Down
15 changes: 10 additions & 5 deletions humphrey-server/src/config/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,27 @@ use std::fmt::Display;
#[derive(Debug, PartialEq, Eq)]
pub struct ConfigError {
message: &'static str,
line: i64,
file: String,
line: u64,
}

impl ConfigError {
/// Creates a new configuration error object.
pub fn new(message: &'static str, line: i64) -> Self {
Self { message, line }
pub fn new(message: &'static str, file: &str, line: u64) -> Self {
Self {
message,
file: file.to_string(),
line,
}
}
}

impl Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Configuration error on line {}: {}",
self.line, self.message
"Configuration error at {} line {}: {}",
self.file, self.line, self.message
)
}
}
Expand Down
4 changes: 2 additions & 2 deletions humphrey-server/src/config/traceback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ where
T: Iterator,
{
inner_iterator: T,
current_line: i64,
current_line: u64,
}

impl<T> TracebackIterator<T>
where
T: Iterator,
{
/// Gets the current index of the iterator.
pub fn current_line(&self) -> i64 {
pub fn current_line(&self) -> u64 {
self.current_line
}
}
Expand Down
107 changes: 84 additions & 23 deletions humphrey-server/src/config/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use crate::config::traceback::TracebackIterator;
use humphrey::krauss::wildcard_match;

use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use std::str::Lines;

/// Represents a node in the configuration syntax tree.
Expand Down Expand Up @@ -91,7 +93,7 @@ impl ConfigNode {
}

/// Parses an entire configuration string.
pub fn parse_conf(conf: &str) -> Result<ConfigNode, ConfigError> {
pub fn parse_conf(conf: &str, filename: &str) -> Result<ConfigNode, ConfigError> {
let mut lines = TracebackIterator::from(conf.lines());

// Attemps to find the start of the configuration
Expand All @@ -100,24 +102,28 @@ pub fn parse_conf(conf: &str) -> Result<ConfigNode, ConfigError> {
if let Some(line) = lines.next() {
line_content = clean_up(line);
} else {
return Err(ConfigError::new("Could not find `server` section", 0));
return Err(ConfigError::new(
"Could not find `server` section",
filename,
0,
));
}
}

// Parses the main section
parse_section("server", &mut lines)
parse_section("server", &mut lines, filename)
}

/// Recursively parses a section of the configuration.
fn parse_section(
name: &str,
lines: &mut TracebackIterator<Lines>,
filename: &str,
) -> Result<ConfigNode, ConfigError> {
let mut section_open: bool = true;
let mut values: Vec<ConfigNode> = Vec::new();

// While this section has not been closed
while section_open {
// While this section has not ended
loop {
// Attempt to read a line

if let Some(line) = lines.next() {
Expand All @@ -130,41 +136,59 @@ fn parse_section(
if section_name.starts_with("route ") && section_name != "route {" {
// If the section is a route section, parse it as such
let route_name = section_name.splitn(2, ' ').last().unwrap().trim();
let section = parse_section(route_name, lines)?;
let section = parse_section(route_name, lines, filename)?;
if let ConfigNode::Section(route_name, inner_values) = section {
values.push(ConfigNode::Route(route_name, inner_values));
}
} else {
// If the section is just a regular section, parse it in the normal way
values.push(parse_section(section_name, lines)?);
values.push(parse_section(section_name, lines, filename)?);
}
} else if line == "}" {
// If the line indicates the end of this section, return the parsed section

section_open = false;
break;
} else if !line.is_empty() {
// If the line is not empty, attempt to parse the value

let parts: Vec<&str> = line.splitn(2, ' ').collect();
quiet_assert(parts.len() == 2, "Syntax error", lines)?;
quiet_assert(parts.len() == 2, "Syntax error", filename, lines)?;

let key = parts[0].trim();
let value = parts[1].trim();

if wildcard_match("\"*\"", value) {
values.push(ConfigNode::String(
key.into(),
value[1..value.len() - 1].into(),
))
} else if value.parse::<i64>().is_ok() {
values.push(ConfigNode::Number(key.into(), value.into()))
} else if value.parse::<bool>().is_ok() {
values.push(ConfigNode::Boolean(key.into(), value.into()))
} else if let Ok(size) = parse_size(value) {
values.push(ConfigNode::Number(key.into(), size.to_string()))
// If this is just a regular value
if key != "include" {
if wildcard_match("\"*\"", value) {
values.push(ConfigNode::String(
key.into(),
value[1..value.len() - 1].into(),
))
} else if value.parse::<i64>().is_ok() {
values.push(ConfigNode::Number(key.into(), value.into()))
} else if value.parse::<bool>().is_ok() {
values.push(ConfigNode::Boolean(key.into(), value.into()))
} else if let Ok(size) = parse_size(value) {
values.push(ConfigNode::Number(key.into(), size.to_string()))
} else {
return Err(ConfigError::new(
"Could not parse value",
filename,
lines.current_line(),
));
}
} else if wildcard_match("\"*\"", value) {
let include_result =
include(&value[1..value.len() - 1], filename, lines.current_line());
if let Ok(included_nodes) = include_result {
values.extend(included_nodes);
} else {
return Err(include_result.unwrap_err());
}
} else {
return Err(ConfigError::new(
"Could not parse value",
"Invalid include value, it takes a file path in quotation marks as its value",
filename,
lines.current_line(),
));
}
Expand All @@ -174,6 +198,7 @@ fn parse_section(

return Err(ConfigError::new(
"Unexpected end of file, expected `}`",
filename,
lines.current_line(),
));
}
Expand All @@ -182,6 +207,41 @@ fn parse_section(
Ok(ConfigNode::Section(name.into(), values))
}

/// Attempts to include the configuration file at the specified path into the tree,
/// returning a `Vec` of `ConfigNode`s. If unsuccessful, returns a descriptive error.
fn include(path: &str, containing_file: &str, line: u64) -> Result<Vec<ConfigNode>, ConfigError> {
if let Ok(mut file) = File::open(path) {
let mut buf = String::new();
if file.read_to_string(&mut buf).is_ok() {
buf.push_str("\n}");

let mut iter = TracebackIterator::from(buf.lines());
let parsed_node = parse_section("temp_included_section", &mut iter, path)?;

match parsed_node {
ConfigNode::Section(_, children) => Ok(children),
_ => Err(ConfigError::new(
"Internal parser error",
containing_file,
line,
)),
}
} else {
Err(ConfigError::new(
"Could not read included file",
containing_file,
line,
))
}
} else {
Err(ConfigError::new(
"Could not open included file",
containing_file,
line,
))
}
}

/// Cleans up a line by removing comments and trailing whitespace.
fn clean_up(line: &str) -> &str {
line.splitn(2, '#').next().unwrap().trim()
Expand Down Expand Up @@ -217,13 +277,14 @@ fn parse_size(size: &str) -> Result<i64, ()> {
fn quiet_assert<T>(
condition: bool,
message: &'static str,
filename: &str,
iter: &mut TracebackIterator<T>,
) -> Result<(), ConfigError>
where
T: Iterator,
{
match condition {
true => Ok(()),
false => Err(ConfigError::new(message, iter.current_line())),
false => Err(ConfigError::new(message, filename, iter.current_line())),
}
}
2 changes: 1 addition & 1 deletion humphrey-server/src/tests/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::collections::HashMap;

#[test]
fn test_parse_config() {
let tree = parse_conf(CONF).unwrap();
let tree = parse_conf(CONF, "valid.conf").unwrap();
let conf = Config::from_tree(tree).unwrap();

#[cfg(feature = "plugins")]
Expand Down
Loading

0 comments on commit 1bfd55c

Please sign in to comment.