Skip to content

ferronweb/ferronconf

Repository files navigation

ferronconf

A Rust library for parsing ferron.conf configuration files — a domain-specific language for custom web server configurations.

Overview

This crate provides a reference implementation of the ferron.conf format, including:

  • Lexer — Tokenizes configuration files with support for comments, strings, numbers, booleans, and interpolation
  • Parser — Builds an AST from tokens with full error reporting
  • AST — Type-safe representation of configuration structures

For the complete format specification, see SPECIFICATION.md.

Installation

Add to your Cargo.toml:

[dependencies]
ferronconf = "0.1.0"

Quick start

use ferronconf::Config;
use std::str::FromStr;

let input = r#"
example.com {
    root /var/www/example
    tls {
        provider "acme"
        challenge http-01
        contact "admin@example.com"
    }
}

api.example.com:8080 {
    proxy http://localhost:3000
}
"#;

let config = Config::from_str(input)?;

Configuration format

The ferron.conf format supports five statement types:

1. Directives

Key-value pairs with optional nested blocks:

server_name example.com
max_connections 1000
enabled true
cert "{{env.TLS_CERT}}"

2. Host blocks

Configuration scoped to specific hosts (top-level only):

# Simple hostname
example.com {
    root /var/www/example
}

# Wildcard subdomains
*.example.com {
    tls {
        provider "acme"
        challenge http-01
        contact "admin@example.com"
    }
}

# With protocol and port
http api.example.com:8080 {
    proxy http://localhost:3000
}

# IPv6
[2001:db8::1]:8080 {
    root /ipv6-only
}

# Multiple hosts (comma-separated)
example.com, www.example.com {
    root /var/www/shared
}

3. Global blocks

Global configuration applied to all hosts (top-level only):

{
    runtime {
        io_uring true
    }

    tcp {
        listen "::"
        send_buf 65536
    }

    default_http_port 8080
    default_https_port 8443
}

4. Snippet blocks

Reusable configuration fragments:

snippet tls_defaults {
    tls {
        provider "acme"
        challenge http-01
        contact "admin@example.com"
    }

    http {
        protocols h1 h2
    }
}

5. Match blocks

Conditional logic based on request attributes:

match api_request {
    request.uri.path ~ "/api"
    request.method in "GET,POST"
}

match curl_client {
    request.header.user_agent ~ "curl"
}

Supported operators: ==, !=, ~ (regex), !~ (negated regex), in

Comments

# This is a comment
server_name example.com  # inline comment

Note: Comments are not allowed inside match blocks.

Data types

Type Example Description
String (quoted) "hello world" Supports escape sequences (\n, \t, \\, \")
String (bare) example.com Unquoted alphanumeric with _-.:/+*
Number 80, 3.14, -10 Integer or decimal
Boolean true, false Case-sensitive literals
Interpolation {{env.TLS_CERT}} Variable reference with dotted path

Library API

Parsing

use ferronconf::Config;
use std::str::FromStr;

let config = Config::from_str(input)?;

AST navigation

use ferronconf::Statement;

// Find all directives with a given name
let roots = config.find_directives("root");

// Find all host blocks
let hosts = config.find_host_blocks();

// Find all match blocks
let matchers = config.find_match_blocks();

// Navigate statements
for stmt in &config.statements {
    match stmt {
        Statement::Directive(d) => {
            println!("Directive: {}", d.name);
            if let Some(root) = d.get_string_arg(0) {
                println!("  Root: {}", root);
            }
        }
        Statement::HostBlock(hb) => {
            for pattern in &hb.hosts {
                println!("Host: {}", pattern.as_str());
            }
        }
        Statement::MatchBlock(mb) => {
            println!("Matcher: {}", mb.matcher);
        }
        Statement::GlobalBlock(gb) => {
            // Access global configuration
        }
        Statement::SnippetBlock(sb) => {
            println!("Snippet: {}", sb.name);
        }
    }
}

Block helpers

// Find directive inside a block
if let Some(block) = directive.block {
    if let Some(nested) = block.find_directive("ssl") {
        // ...
    }
}

Value extraction

use ferronconf::Value;

// From directive arguments
if let Some(root) = directive.get_string_arg(0) {
    // ...
}

if let Some(port) = directive.get_integer_arg(1) {
    // ...
}

if let Some(enabled) = directive.get_boolean_arg(2) {
    // ...
}

// From Value directly
match &value {
    Value::String(s, _) => { /* ... */ }
    Value::Integer(i, _) => { /* ... */ }
    Value::Float(f, _) => { /* ... */ }
    Value::Boolean(b, _) => { /* ... */ }
    Value::InterpolatedString(parts, _) => {
        for part in parts {
            match part {
                StringPart::Literal(s) => { /* ... */ }
                StringPart::Expression(path) => { /* ... */ }
            }
        }
    }
}

Host pattern matching

use ferronconf::ast::HostLabels;

for host_block in config.find_host_blocks() {
    for pattern in &host_block.hosts {
        match &pattern.labels {
            HostLabels::Wildcard => { /* matches any host */ }
            HostLabels::Hostname(labels) => { /* e.g., ["example", "com"] */ }
            HostLabels::IpAddr(ip) => { /* IPv4 or IPv6 */ }
        }
        
        if let Some(port) = pattern.port {
            // ...
        }
        
        if let Some(protocol) = &pattern.protocol {
            // e.g., "http", "tcp"
        }
    }
    
    // Check if block matches a specific host
    if host_block.matches_host("example.com") {
        // ...
    }
}

Match block inspection

for match_block in config.find_match_blocks() {
    println!("Matcher: {}", match_block.matcher);
    
    for expr in &match_block.expr {
        match &expr.left {
            Operand::Identifier(path, _) => {
                println!("  Path: {}", path.join("."));
            }
            Operand::String(s, _) => {
                println!("  String: {}", s);
            }
            _ => {}
        }
        
        println!("  Operator: {}", expr.op.as_str());
        
        // Check operator type
        if expr.is_equality() { /* ... */ }
        if expr.is_regex() { /* ... */ }
    }
}

Error handling

Parse errors include line and column information:

use ferronconf::{Config, ParseError};
use std::str::FromStr;

match Config::from_str(input) {
    Ok(config) => { /* ... */ }
    Err(ParseError { message, span }) => {
        eprintln!("Error at line {}, column {}: {}", 
                  span.line, span.column, message);
    }
}

Syntax highlighting

A TextMate grammar is provided in ferron.tmLanguage.json for editor syntax highlighting. Copy it to your editor's grammar directory or use it with tools like bat or syntect.

Example configuration

# Global defaults
{
    runtime {
        io_uring true
    }

    tcp {
        listen "::"
    }

    default_http_port 80
    default_https_port 443

    admin {
        listen 127.0.0.1:8081
        health true
        status true
    }
}

# Reusable TLS configuration
snippet tls_acme {
    tls {
        provider "acme"
        challenge http-01
        contact "admin@example.com"
    }
}

# Reusable HTTP settings
snippet common_http {
    http {
        protocols h1 h2
    }
}

# Main site with static file serving
example.com:443 {
    use tls_acme
    use common_http

    root /var/www/example
    index index.html index.htm
    directory_listing
    compressed

    log "access" {
        format "combined"
    }
}

# Wildcard subdomains with ACME TLS
*.example.com {
    tls {
        provider "acme"
        challenge dns-01
        contact "admin@example.com"
        dns "cloudflare" {
            api_key "EXAMPLE_API_KEY"
        }
    }

    root /var/www/multi-tenant
}

# API reverse proxy
api.example.com {
    proxy http://localhost:3000 http://localhost:3001 {
        lb_algorithm two_random
        keepalive true
        http2 true

        request_header +X-Real-IP "{{remote_address}}"
        request_header X-Forwarded-Proto "{{scheme}}"
    }

    rate_limit {
        rate 100
        burst 50
        key remote_address
    }

    cors {
        origins "https://app.example.com"
        methods GET POST PUT DELETE
        headers "Content-Type" "Authorization"
        credentials true
    }
}

# Conditional routing
match api_request {
    request.uri.path ~ "/api"
    request.method in "GET,POST"
}

match curl_client {
    request.header.user_agent ~ "curl"
}

# Location-based configuration
example.com {
    root /var/www/example

    location /static {
        file_cache_control "public, max-age=31536000"
    }

    location /admin {
        if curl_client {
            status 403 {
                body "Forbidden"
            }
        }
    }
}

# Protocol-specific configuration
http * {
    header X-Powered-By "Ferron"
}

# TCP service
tcp *:5432 {
    proxy localhost:5432
}

Limitations

  • Bare strings after identifiers may be ambiguous with host blocks at the top level
  • Comments are not supported inside match blocks
  • IPv4 octets are validated to be in range 0–255

License

MIT

About

A Rust library for parsing `ferron.conf` configuration files — a domain-specific language for custom web server configurations.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages